diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index 13e8ea330da..b33a176b010 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -102,7 +102,6 @@ review-build-cng: name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} on_stop: review-stop - auto_stop_in: 6 hours review-deploy: extends: @@ -168,7 +167,6 @@ review-deploy-sample-projects: extends: .review-workflow-base environment: action: stop - dependencies: [] variables: # We're cloning the repo instead of downloading the script for now # because some repos are private and CI_JOB_TOKEN cannot access files. @@ -178,21 +176,20 @@ review-deploy-sample-projects: - source ./scripts/utils.sh - source ./scripts/review_apps/review-apps.sh - !reference [".use-kube-context", before_script] + script: + - delete_helm_release review-delete-deployment: extends: - .review-stop-base - .review:rules:review-delete-deployment + dependencies: [] stage: prepare - script: - - delete_helm_release review-stop: extends: - .review-stop-base - .review:rules:review-stop - resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment + resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment stage: deploy needs: [] - script: - - delete_helm_release diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index b6c273aeb99..c331671d180 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -58,6 +58,21 @@ start-review-app-pipeline: - job: e2e-test-pipeline-generate - job: build-assets-image artifacts: false + # We do not want to have ALL global variables passed as trigger variables, + # as they cannot be overridden. See this issue for more context: + # + # https://gitlab.com/gitlab-org/gitlab/-/issues/387183 + inherit: + variables: + - CHROME_VERSION + - REGISTRY_GROUP + - REGISTRY_HOST + - REVIEW_APPS_DOMAIN + - REVIEW_APPS_GCP_PROJECT + - REVIEW_APPS_GCP_REGION + - REVIEW_APPS_IMAGE + - RUBY_VERSION + # These variables are set in the pipeline schedules. # They need to be explicitly passed on to the child pipeline. # https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#pass-cicd-variables-to-a-downstream-pipeline-by-using-the-variables-keyword diff --git a/.gitlab/merge_request_templates/Revert To Resolve Incident.md b/.gitlab/merge_request_templates/Revert To Resolve Incident.md index 17ff239bbd4..ab6e65b1388 100644 --- a/.gitlab/merge_request_templates/Revert To Resolve Incident.md +++ b/.gitlab/merge_request_templates/Revert To Resolve Incident.md @@ -1,14 +1,31 @@ -## Purpose of Revert + + +## Purpose of revert -### Check-list +### Checklist - [ ] Create an issue to reinstate the merge request and assign it to the author of the reverted merge request. -- [ ] If the revert is to resolve a ['broken master' incident](https://about.gitlab.com/handbook/engineering/workflow/#broken-master), please read through the [Responsibilities of the Broken 'Master' resolution DRI](https://about.gitlab.com/handbook/engineering/workflow/#responsibilities-of-the-resolution-dri) -- [ ] Add the appropriate labels **before** the MR is created (we can only skip CI/CD jobs if the labels are added **before** the CI/CD pipeline gets created) +- [ ] If the revert is to resolve a [broken `master' incident](https://about.gitlab.com/handbook/engineering/workflow/#broken-master), please read through the [Responsibilities of the Broken `master` resolution DRI](https://about.gitlab.com/handbook/engineering/workflow/#responsibilities-of-the-resolution-dri). +- [ ] Add the appropriate labels **before** the MR is created. We can skip CI/CD jobs only if the labels are added **before** the CI/CD pipeline is created. + +### Milestone info + +- [ ] I am reverting something in the **current** milestone. No changelog is needed, and I've added a `~"regression:*"` label. +- [ ] I am reverting something in a **different** milestone. A changelog is needed, and I've removed the `~"regression:*"` label. + +### Related issues and merge requests + /label ~"pipeline:expedite" ~"master:broken" - + + diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 9fe784e1be9..066f17aabe0 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -356,7 +356,6 @@ Gitlab/StrongMemoizeAttr: - 'ee/app/models/ee/list.rb' - 'ee/app/models/ee/merge_request.rb' - 'ee/app/models/ee/namespace.rb' - - 'ee/app/models/ee/namespace/storage/notification.rb' - 'ee/app/models/ee/project.rb' - 'ee/app/models/ee/snippet.rb' - 'ee/app/models/ee/user.rb' diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml index b774d92d4ee..319dd967481 100644 --- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml +++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml @@ -71,7 +71,6 @@ Layout/LineEndStringConcatenationIndentation: - 'ee/app/models/app_sec/fuzzing/api/ci_configuration.rb' - 'ee/app/models/ci/minutes/notification.rb' - 'ee/app/models/ee/group_group_link.rb' - - 'ee/app/models/ee/namespace/storage/notification.rb' - 'ee/app/models/ee/vulnerability.rb' - 'ee/app/services/boards/epic_lists/destroy_service.rb' - 'ee/app/services/ee/admin/set_feature_flag_service.rb' diff --git a/.rubocop_todo/rspec/described_class.yml b/.rubocop_todo/rspec/described_class.yml index b60fca01288..ca81738e93c 100644 --- a/.rubocop_todo/rspec/described_class.yml +++ b/.rubocop_todo/rspec/described_class.yml @@ -16,7 +16,6 @@ RSpec/DescribedClass: - 'ee/spec/models/ee/group_spec.rb' - 'ee/spec/models/ee/iteration_spec.rb' - 'ee/spec/models/ee/merge_request_diff_spec.rb' - - 'ee/spec/models/ee/namespace/storage/notification_spec.rb' - 'ee/spec/models/ee/vulnerability_spec.rb' - 'ee/spec/models/epic_issue_spec.rb' - 'ee/spec/models/epic_spec.rb' diff --git a/.rubocop_todo/rspec/expect_in_hook.yml b/.rubocop_todo/rspec/expect_in_hook.yml index 7fc5bdbdb6d..d25c4137c1a 100644 --- a/.rubocop_todo/rspec/expect_in_hook.yml +++ b/.rubocop_todo/rspec/expect_in_hook.yml @@ -51,7 +51,6 @@ RSpec/ExpectInHook: - 'ee/spec/models/concerns/geo/replicable_model_spec.rb' - 'ee/spec/models/container_repository_spec.rb' - 'ee/spec/models/dora/daily_metrics_spec.rb' - - 'ee/spec/models/ee/namespace/storage/notification_spec.rb' - 'ee/spec/models/ee/namespace_spec.rb' - 'ee/spec/models/gitlab_subscription_spec.rb' - 'ee/spec/models/license_spec.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index 97647e95e3a..a76717d8d3f 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -1382,7 +1382,6 @@ RSpec/MissingFeatureCategory: - 'ee/spec/models/ee/merge_request/metrics_spec.rb' - 'ee/spec/models/ee/merge_request_diff_spec.rb' - 'ee/spec/models/ee/namespace/root_storage_statistics_spec.rb' - - 'ee/spec/models/ee/namespace/storage/notification_spec.rb' - 'ee/spec/models/ee/namespace_ci_cd_setting_spec.rb' - 'ee/spec/models/ee/namespace_spec.rb' - 'ee/spec/models/ee/namespace_statistics_spec.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index d18cd4d24a8..39ddc5419c5 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -198,7 +198,6 @@ Style/FormatString: - 'ee/app/models/dast_site_profile.rb' - 'ee/app/models/dast_site_validation.rb' - 'ee/app/models/ee/member.rb' - - 'ee/app/models/ee/namespace/storage/notification.rb' - 'ee/app/models/geo/upload_registry.rb' - 'ee/app/models/integrations/github.rb' - 'ee/app/models/iterations/cadence.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8ab3273840e..9452776f1b7 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -5b5abda2e69a93c5898609cd9c9aa02954c10556 +69e486270838efbbb78e6736ac6aecde5ccd8caa diff --git a/GITLAB_METRICS_EXPORTER_VERSION b/GITLAB_METRICS_EXPORTER_VERSION index e0c3287cb8c..01c8db86a25 100644 --- a/GITLAB_METRICS_EXPORTER_VERSION +++ b/GITLAB_METRICS_EXPORTER_VERSION @@ -1 +1 @@ -65bbfa0b62518691961de096e4a27d7c76307b7c +27d39b816071e9630436f8bf19740c173594631d diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue index 4e4c21328ca..6d94af66092 100644 --- a/app/assets/javascripts/contributors/components/contributors.vue +++ b/app/assets/javascripts/contributors/components/contributors.vue @@ -1,19 +1,33 @@ @@ -197,48 +226,67 @@ export default { diff --git a/app/assets/javascripts/contributors/index.js b/app/assets/javascripts/contributors/index.js index f66133a074d..1bb7360547c 100644 --- a/app/assets/javascripts/contributors/index.js +++ b/app/assets/javascripts/contributors/index.js @@ -7,18 +7,19 @@ export default () => { if (!el) return null; - const { projectGraphPath, projectBranch, defaultBranch } = el.dataset; + const { projectGraphPath, projectBranch, defaultBranch, projectId, commitsPath } = el.dataset; const store = createStore(defaultBranch); return new Vue({ el, store, - render(createElement) { return createElement(ContributorsGraphs, { props: { endpoint: projectGraphPath, branch: projectBranch, + projectId, + commitsPath, }, }); }, diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index 87d869cc996..324acb177b0 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -777,7 +777,7 @@ "properties": { "value": { "type": "string", - "markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#prefill-variables-in-manual-pipelines)" + "markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesvalue)" }, "options": { "type": "array", @@ -786,7 +786,7 @@ }, "minItems": 1, "uniqueItems": true, - "markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#configure-a-list-of-selectable-values-for-a-prefilled-variable)" + "markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesoptions)" }, "description": { "type": "string", diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index d0ccc8fd599..91a0ef07bc4 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -3,6 +3,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import initTransferGroupForm from '~/groups/init_transfer_group_form'; import { initGroupSelects } from '~/vue_shared/components/entity_select/init_group_selects'; +import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects'; import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import projectSelect from '~/project_select'; @@ -22,6 +23,9 @@ mountBadgeSettings(GROUP_BADGE); // Initialize Subgroups selector initGroupSelects(); +// Initialize project selectors +initProjectSelects(); + projectSelect(); initSearchSettings(); diff --git a/app/assets/javascripts/pages/groups/usage_quotas/index.js b/app/assets/javascripts/pages/groups/usage_quotas/index.js new file mode 100644 index 00000000000..dab2d0b17d2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/usage_quotas/index.js @@ -0,0 +1,3 @@ +import initUsageQuotas from '~/usage_quotas'; + +initUsageQuotas(); diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index 10967fb84ed..70b8604b3f1 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -87,6 +87,12 @@ export default { required: false, default: '', }, + + toggleButtonClass: { + type: [String, Object, Array], + required: false, + default: null, + }, }, data() { return { @@ -130,11 +136,21 @@ export default { showSectionHeaders() { return this.enabledRefTypes.length > 1; }, - toggleButtonClass() { - return { - 'gl-inset-border-1-red-500!': !this.state, - 'gl-font-monospace': Boolean(this.selectedRef), - }; + extendedToggleButtonClass() { + const classes = [ + { + 'gl-inset-border-1-red-500!': !this.state, + 'gl-font-monospace': Boolean(this.selectedRef), + }, + ]; + + if (Array.isArray(this.toggleButtonClass)) { + classes.push(...this.toggleButtonClass); + } else { + classes.push(this.toggleButtonClass); + } + + return classes; }, footerSlotProps() { return { @@ -239,7 +255,7 @@ export default {
+import { GlSprintf, GlTab, GlTabs } from '@gitlab/ui'; +import { USAGE_QUOTAS_TITLE, USAGE_QUOTAS_SUBTITLE } from '../constants'; + +export default { + name: 'UsageQuotasApp', + components: { GlSprintf, GlTab, GlTabs }, + inject: ['namespaceName'], + computed: { + placeholder() { + return `storage_app_placeholder`; + }, + }, + USAGE_QUOTAS_TITLE, + USAGE_QUOTAS_SUBTITLE, +}; + + + diff --git a/app/assets/javascripts/usage_quotas/constants.js b/app/assets/javascripts/usage_quotas/constants.js new file mode 100644 index 00000000000..f637d241778 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/constants.js @@ -0,0 +1,7 @@ +import { s__ } from '~/locale'; + +export const USAGE_QUOTAS_TITLE = s__('UsageQuota|Usage Quotas'); + +export const USAGE_QUOTAS_SUBTITLE = s__( + 'UsageQuota|Usage of group resources across the projects in the %{namespaceName} group', +); diff --git a/app/assets/javascripts/usage_quotas/index.js b/app/assets/javascripts/usage_quotas/index.js new file mode 100644 index 00000000000..e1032cd8d54 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/index.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import UsageQuotasApp from './components/usage_quotas_app.vue'; + +export default () => { + const el = document.getElementById('js-usage-quotas-view'); + + if (!el) { + return false; + } + + const { namespaceName } = el.dataset; + + return new Vue({ + el, + name: 'UsageQuotasView', + provide: { + namespaceName, + }, + render(createElement) { + return createElement(UsageQuotasApp); + }, + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/entity_select/constants.js b/app/assets/javascripts/vue_shared/components/entity_select/constants.js index a48bba3be8c..0fb5a2d5534 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/constants.js +++ b/app/assets/javascripts/vue_shared/components/entity_select/constants.js @@ -1,8 +1,16 @@ -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; -export const TOGGLE_TEXT = __('Search for a group'); -export const HEADER_TEXT = __('Select a group'); export const RESET_LABEL = __('Reset'); +export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.'); + +// Groups +export const GROUP_TOGGLE_TEXT = __('Search for a group'); +export const GROUP_HEADER_TEXT = __('Select a group'); export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.'); export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.'); -export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.'); + +// Projects +export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project'); +export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project'); +export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.'); +export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.'); diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue index a5fc438e932..ff137d764ee 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue @@ -5,7 +5,12 @@ import axios from '~/lib/utils/axios_utils'; import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils'; import Api, { DEFAULT_PER_PAGE } from '~/api'; import { groupsPath } from './utils'; -import { TOGGLE_TEXT, HEADER_TEXT, FETCH_GROUPS_ERROR, FETCH_GROUP_ERROR } from './constants'; +import { + GROUP_TOGGLE_TEXT, + GROUP_HEADER_TEXT, + FETCH_GROUPS_ERROR, + FETCH_GROUP_ERROR, +} from './constants'; import EntitySelect from './entity_select.vue'; export default { @@ -58,7 +63,7 @@ export default { let groups = []; let totalPages = 0; try { - const { data, headers } = await axios.get( + const { data = [], headers } = await axios.get( Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)), { params: { @@ -68,7 +73,7 @@ export default { }, }, ); - groups = (data.length ? data : data.results || []).map((group) => ({ + groups = data.map((group) => ({ ...group, text: group.full_name, value: String(group.id), @@ -99,8 +104,8 @@ export default { }, }, i18n: { - toggleText: TOGGLE_TEXT, - selectGroup: HEADER_TEXT, + toggleText: GROUP_TOGGLE_TEXT, + selectGroup: GROUP_HEADER_TEXT, }, }; diff --git a/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js b/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js new file mode 100644 index 00000000000..eee90b0f4f7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import ProjectSelect from './project_select.vue'; + +const SELECTOR = '.js-vue-project-select'; + +export const initProjectSelects = () => { + if (process.env.NODE_ENV !== 'production' && document.querySelector(SELECTOR) === null) { + // eslint-disable-next-line no-console + console.warn(`Attempted to initialize ProjectSelect but '${SELECTOR}' not found in the page`); + } + + document.querySelectorAll(SELECTOR).forEach((el) => { + const { label, inputName, inputId, groupId, selected: initialSelection } = el.dataset; + const clearable = parseBoolean(el.dataset.clearable); + + return new Vue({ + el, + name: 'ProjectSelectRoot', + render(createElement) { + return createElement(ProjectSelect, { + props: { + label, + inputName, + inputId, + groupId, + initialSelection, + clearable, + }, + }); + }, + }); + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue new file mode 100644 index 00000000000..c71fb713f1c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue @@ -0,0 +1,113 @@ + + + diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index e86edff3f13..c0fe8ca6f76 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -36,6 +36,24 @@ input[type='number'].hide-spinners { } /* stylelint-enable property-no-vendor-prefix */ +/** + * When form input type is search, browsers add a clear input button inside + * the input field. This overlaps with the input field we have already added. + */ + +/* stylelint-disable property-no-vendor-prefix */ +input[type='search'] { + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; + + &::-webkit-search-cancel-button, + &::-webkit-search-results-button { + @include gl-display-none; + } +} +/* stylelint-enable property-no-vendor-prefix */ + .datetime-controls { select { width: 100px; diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss index 10da541ed8d..7e3b8f1284a 100644 --- a/app/assets/stylesheets/page_bundles/search.scss +++ b/app/assets/stylesheets/page_bundles/search.scss @@ -324,13 +324,20 @@ $border-radius-medium: 3px; } } -// Disable Webkit's search input styles +/** + * When form input type is search, browsers add a clear input button inside + * the input field. This overlaps with the input field we have already added. + */ + +/* stylelint-disable property-no-vendor-prefix */ input[type='search'] { - /* stylelint-disable-next-line property-no-vendor-prefix */ -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; &::-webkit-search-cancel-button, &::-webkit-search-results-button { @include gl-display-none; } } +/* stylelint-enable property-no-vendor-prefix */ diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index bb83a91bc57..c9425b1103c 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -761,6 +761,9 @@ input { color: #ececef; background-color: #333238; } +input[type="search"] { + appearance: textfield; +} .form-control { border-radius: 4px; padding: 6px 10px; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 9e1c6b065a0..40a6e9eb9fe 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -761,6 +761,9 @@ input { color: #333238; background-color: #fff; } +input[type="search"] { + appearance: textfield; +} .form-control { border-radius: 4px; padding: 6px 10px; diff --git a/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb b/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb index eebc40f33f4..b0220b17cf9 100644 --- a/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb +++ b/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb @@ -7,9 +7,11 @@ module Analytics extend ActiveSupport::Concern included do + extend ::Gitlab::Utils::Override include CycleAnalyticsParams - before_action :validate_params, only: %i[median] + before_action :validate_params, except: %i[index] + before_action :authorize_stage, except: %i[index] end def index @@ -44,11 +46,11 @@ module Analytics private - def parent + def namespace raise NotImplementedError end - def value_stream_class + def authorize_stage raise NotImplementedError end @@ -64,7 +66,7 @@ module Analytics end def stage - @stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute + @stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: namespace, stage_id: params[:id]).execute end def data_collector @@ -75,7 +77,7 @@ module Analytics end def value_stream - @value_stream ||= value_stream_class.build_default_value_stream(parent) + @value_stream ||= Analytics::CycleAnalytics::ValueStream.build_default_value_stream(namespace) end def list_params @@ -83,7 +85,7 @@ module Analytics end def list_service - Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, current_user: current_user, params: list_params) + Analytics::CycleAnalytics::Stages::ListService.new(parent: namespace, current_user: current_user, params: list_params) end def cycle_analytics_configuration(stages) @@ -94,3 +96,5 @@ module Analytics end end end + +Analytics::CycleAnalytics::StageActions.prepend_mod_with('Analytics::CycleAnalytics::StageActions') diff --git a/app/controllers/groups/usage_quotas_controller.rb b/app/controllers/groups/usage_quotas_controller.rb index b660eb3af99..4f858cd130a 100644 --- a/app/controllers/groups/usage_quotas_controller.rb +++ b/app/controllers/groups/usage_quotas_controller.rb @@ -4,6 +4,7 @@ module Groups class UsageQuotasController < Groups::ApplicationController before_action :authorize_read_usage_quotas! before_action :verify_usage_quotas_enabled! + before_action :push_frontend_feature_flags feature_category :subscription_cost_management urgency :low @@ -15,6 +16,10 @@ module Groups private + def push_frontend_feature_flags + push_frontend_feature_flag(:usage_quotas_for_all_editions, @group) + end + def verify_usage_quotas_enabled! render_404 unless group.usage_quotas_enabled? end diff --git a/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb index ab2cf3abdde..a61b774f9c8 100644 --- a/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb +++ b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb @@ -3,7 +3,6 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController include ::Analytics::CycleAnalytics::StageActions include Gitlab::Utils::StrongMemoize - extend ::Gitlab::Utils::Override respond_to :json @@ -11,20 +10,14 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat before_action :authorize_read_cycle_analytics! before_action :only_default_value_stream_is_allowed! - before_action :authorize_stage!, only: [:median, :count, :average, :records] urgency :low private - override :parent - def parent - @project - end - - override :value_stream_class - def value_stream_class - Analytics::CycleAnalytics::ProjectValueStream + override :namespace + def namespace + @project.project_namespace end override :cycle_analytics_configuration @@ -33,7 +26,9 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat end def only_default_value_stream_is_allowed! - render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME + return if requests_default_value_stream? + + render_403 end def permitted_stage?(stage) @@ -42,11 +37,20 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat def permissions strong_memoize(:permissions) do - Gitlab::CycleAnalytics::Permissions.new(user: current_user, project: parent).get + Gitlab::CycleAnalytics::Permissions.new(user: current_user, project: @project).get end end - def authorize_stage! + def authorize_stage render_403 unless permitted_stage?(stage) end + + def requests_default_value_stream? + default_name = Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME + + params[:value_stream_id] == default_name + end end + +mod = 'Projects::Analytics::CycleAnalytics::StagesController' +Projects::Analytics::CycleAnalytics::StagesController.prepend_mod_with(mod) # rubocop: disable Cop/InjectEnterpriseEditionModule diff --git a/app/finders/namespaces/projects_finder.rb b/app/finders/namespaces/projects_finder.rb index 589a9696ea6..c96f9527dd8 100644 --- a/app/finders/namespaces/projects_finder.rb +++ b/app/finders/namespaces/projects_finder.rb @@ -12,6 +12,8 @@ # search: string # include_subgroups: boolean # ids: int[] +# with_issues_enabled: boolean +# with_merge_requests_enabled: boolean # module Namespaces class ProjectsFinder @@ -30,7 +32,9 @@ module Namespaces namespace.projects.with_route end - filter_projects(collection) + collection = filter_projects(collection) + + sort(collection) end private @@ -39,7 +43,8 @@ module Namespaces def filter_projects(collection) collection = by_ids(collection) - by_similarity(collection) + collection = by_similarity(collection) + by_feature_availability(collection) end def by_ids(items) @@ -51,11 +56,26 @@ module Namespaces def by_similarity(items) return items unless params[:search].present? - if params[:sort] == :similarity - items = items.sorted_by_similarity_desc(params[:search], include_in_select: true) + items.merge(Project.search(params[:search])) + end + + def by_feature_availability(items) + items = items.with_issues_available_for_user(current_user) if params[:with_issues_enabled].present? + if params[:with_merge_requests_enabled].present? + items = items.with_merge_requests_available_for_user(current_user) end - items.merge(Project.search(params[:search])) + items + end + + def sort(items) + return items.projects_order_id_desc unless params[:sort] + + if params[:sort] == :similarity && params[:search].present? + return items.sorted_by_similarity_desc(params[:search], include_in_select: true) + end + + items.sort_by_attribute(params[:sort]) end end end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 1afd5adeada..7226a4c0d82 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -56,11 +56,7 @@ class ProjectsFinder < UnionFinder collection = Project.wrap_with_cte(collection) if use_cte collection = filter_projects(collection) - if params[:sort] == 'similarity' && params[:search] - collection.sorted_by_similarity_desc(params[:search]) - else - sort(collection) - end + sort(collection) end private @@ -90,6 +86,7 @@ class ProjectsFinder < UnionFinder collection = by_last_activity_after(collection) collection = by_last_activity_before(collection) collection = by_language(collection) + collection = by_feature_availability(collection) by_repository_storage(collection) end @@ -247,11 +244,13 @@ class ProjectsFinder < UnionFinder end def sort(items) - if params[:sort].present? - items.sort_by_attribute(params[:sort]) - else - items.projects_order_id_desc + return items.projects_order_id_desc unless params[:sort] + + if params[:sort] == 'similarity' && params[:search].present? + return items.sorted_by_similarity_desc(params[:search], include_in_select: true) end + + items.sort_by_attribute(params[:sort]) end def by_archived(projects) @@ -270,6 +269,12 @@ class ProjectsFinder < UnionFinder end end + def by_feature_availability(items) + items = items.with_issues_available_for_user(current_user) if params[:with_issues_enabled] + items = items.with_merge_requests_available_for_user(current_user) if params[:with_merge_requests_enabled] + items + end + def finder_params return {} unless min_access_level? diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index c3c61d31e8d..726e78f9971 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -8,9 +8,9 @@ module Resolvers description: 'Include also subgroup projects.' argument :search, GraphQL::Types::String, - required: false, - default_value: nil, - description: 'Search project with most similar names or paths.' + required: false, + default_value: nil, + description: 'Search project with most similar names or paths.' argument :sort, Types::Projects::NamespaceProjectSortEnum, required: false, @@ -22,6 +22,14 @@ module Resolvers default_value: nil, description: 'Filter projects by IDs.' + argument :with_issues_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with issues enabled." + + argument :with_merge_requests_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with merge requests enabled." + type Types::ProjectType, null: true def resolve(args) @@ -54,7 +62,9 @@ module Resolvers include_subgroups: args.dig(:include_subgroups), sort: args.dig(:sort), search: args.dig(:search), - ids: parse_gids(args.dig(:ids)) + ids: parse_gids(args.dig(:ids)), + with_issues_enabled: args[:with_issues_enabled], + with_merge_requests_enabled: args[:with_merge_requests_enabled] } end diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index 0bdba53c7af..08981f2c441 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -15,14 +15,30 @@ module Resolvers description: "Sort order of results. Format: `_`, " \ "for example: `id_desc` or `name_asc`" + argument :with_issues_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with issues enabled." + + argument :with_merge_requests_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with merge requests enabled." + def resolve(**args) ProjectsFinder - .new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids])) + .new(current_user: current_user, params: finder_params(args), project_ids_relation: parse_gids(args[:ids])) .execute end private + def finder_params(args) + { + **project_finder_params(args), + with_issues_enabled: args[:with_issues_enabled], + with_merge_requests_enabled: args[:with_merge_requests_enabled] + } + end + def parse_gids(gids) gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id } end diff --git a/app/graphql/types/projects/namespace_project_sort_enum.rb b/app/graphql/types/projects/namespace_project_sort_enum.rb index bd7058196dd..7c7b54226d3 100644 --- a/app/graphql/types/projects/namespace_project_sort_enum.rb +++ b/app/graphql/types/projects/namespace_project_sort_enum.rb @@ -8,6 +8,7 @@ module Types value 'SIMILARITY', 'Most similar to the search query.', value: :similarity value 'STORAGE', 'Sort by storage size.', value: :storage + value 'ACTIVITY_DESC', 'Sort by latest activity, in descending order.', value: :latest_activity_desc end end end diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb index 2953ec6cbe5..ef0dd4fd2f9 100644 --- a/app/mailers/emails/service_desk.rb +++ b/app/mailers/emails/service_desk.rb @@ -17,24 +17,30 @@ module Emails email_sender = sender( @support_bot.id, send_from_user_email: false, - sender_name: @project.service_desk_setting&.outgoing_name + sender_name: @service_desk_setting&.outgoing_name, + sender_email: service_desk_sender_email_address ) options = service_desk_options(email_sender, 'thank_you', @issue.external_author) .merge(subject: "Re: #{subject_base}") - mail_new_thread(@issue, options) + inject_service_desk_custom_email(mail_new_thread(@issue, options)) end def service_desk_new_note_email(issue_id, note_id, recipient) @note = Note.find(note_id) setup_service_desk_mail(issue_id) - email_sender = sender(@note.author_id) + email_sender = sender( + @note.author_id, + send_from_user_email: false, + sender_email: service_desk_sender_email_address + ) + add_uploads_as_attachments if Feature.enabled?(:service_desk_new_note_email_native_attachments, @note.project) options = service_desk_options(email_sender, 'new_note', recipient) .merge(subject: subject_base) - mail_answer_thread(@issue, options) + inject_service_desk_custom_email(mail_answer_thread(@issue, options)) end private @@ -44,6 +50,8 @@ module Emails @project = @issue.project @support_bot = User.support_bot + @service_desk_setting = @project.service_desk_setting + @sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key) end @@ -59,6 +67,22 @@ module Emails end end + def inject_service_desk_custom_email(mail) + return mail unless service_desk_custom_email_enabled? + + mail.delivery_method(::Mail::SMTP, @service_desk_setting.custom_email_delivery_options) + end + + def service_desk_custom_email_enabled? + Feature.enabled?(:service_desk_custom_email, @project) && @service_desk_setting&.custom_email_enabled? + end + + def service_desk_sender_email_address + return unless service_desk_custom_email_enabled? + + @service_desk_setting.custom_email + end + def template_content(email_type) template = Gitlab::Template::ServiceDeskTemplate.find(email_type, @project) text = substitute_template_replacements(template.content) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 31726563662..28ef6d8d6c6 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -68,14 +68,16 @@ class Notify < ApplicationMailer private # Return an email address that displays the name of the sender. - # Only the displayed name changes; the actual email address is always the same. - def sender(sender_id, send_from_user_email: false, sender_name: nil) + # Override sender_email if you want to hard replace the sender address (e.g. custom email for Service Desk) + def sender(sender_id, send_from_user_email: false, sender_name: nil, sender_email: nil) return unless sender = User.find(sender_id) address = default_sender_address address.display_name = sender_name.presence || "#{sender.name} (#{sender.to_reference})" - if send_from_user_email && can_send_from_user_email?(sender) + if sender_email + address.address = sender_email + elsif send_from_user_email && can_send_from_user_email?(sender) address.address = sender.email end diff --git a/app/models/analytics/cycle_analytics/stage.rb b/app/models/analytics/cycle_analytics/stage.rb new file mode 100644 index 00000000000..fb9918e195d --- /dev/null +++ b/app/models/analytics/cycle_analytics/stage.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class Stage < ApplicationRecord + self.table_name = :analytics_cycle_analytics_group_stages + + include DatabaseEventTracking + include Analytics::CycleAnalytics::Stageable + include Analytics::CycleAnalytics::Parentable + + validates :name, uniqueness: { scope: [:group_id, :group_value_stream_id] } + belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ValueStream', +foreign_key: :group_value_stream_id, inverse_of: :stages + + alias_attribute :parent, :namespace + alias_attribute :parent_id, :group_id + alias_attribute :value_stream_id, :group_value_stream_id + + def self.distinct_stages_within_hierarchy(namespace) + with_preloaded_labels + .where(group_id: namespace.self_and_descendants.select(:id)) + .select("DISTINCT ON(stage_event_hash_id) #{quoted_table_name}.*") + end + + SNOWPLOW_ATTRIBUTES = %i[ + id + created_at + updated_at + relative_position + start_event_identifier + end_event_identifier + group_id + start_event_label_id + end_event_label_id + hidden + custom + name + group_value_stream_id + ].freeze + end + end +end diff --git a/app/models/analytics/cycle_analytics/stage_event_hash.rb b/app/models/analytics/cycle_analytics/stage_event_hash.rb index 0e1e9b3ef67..043a5d45e3d 100644 --- a/app/models/analytics/cycle_analytics/stage_event_hash.rb +++ b/app/models/analytics/cycle_analytics/stage_event_hash.rb @@ -4,6 +4,7 @@ module Analytics module CycleAnalytics class StageEventHash < ApplicationRecord has_many :cycle_analytics_project_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :stage_event_hash + has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::Stage', inverse_of: :stage_event_hash validates :hash_sha256, presence: true @@ -33,10 +34,14 @@ module Analytics end def self.unused_hashes_for(id) - exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1) - where.not('EXISTS (?)', exists_query) + project_stage_exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1) + stage_exists_query = ::Analytics::CycleAnalytics::Stage.where(stage_event_hash_id: id).select('1').limit(1) + + where + .not('EXISTS (?)', project_stage_exists_query) + .where + .not('EXISTS (?)', stage_exists_query) end end end end -Analytics::CycleAnalytics::StageEventHash.prepend_mod_with('Analytics::CycleAnalytics::StageEventHash') diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb new file mode 100644 index 00000000000..3d8a0a53f5e --- /dev/null +++ b/app/models/analytics/cycle_analytics/value_stream.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class ValueStream < ApplicationRecord + self.table_name = :analytics_cycle_analytics_group_value_streams + + include Analytics::CycleAnalytics::Parentable + + has_many :stages, -> { ordered }, + class_name: 'Analytics::CycleAnalytics::Stage', + foreign_key: :group_value_stream_id, + index_errors: true, + inverse_of: :value_stream + + validates :name, presence: true + validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :group_id } + + accepts_nested_attributes_for :stages, allow_destroy: true + + scope :preload_associated_models, -> { + includes(:namespace, + stages: [ + :namespace, + :end_event_label, + :start_event_label + ]) + } + + after_save :ensure_aggregation_record_presence + + def custom? + persisted? || name != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME + end + + def self.build_default_value_stream(namespace) + new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, namespace: namespace) + end + + private + + def ensure_aggregation_record_presence + Analytics::CycleAnalytics::Aggregation.safe_create_for_namespace(namespace) + end + end + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index cf638f9b16c..3fe84b42739 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -85,6 +85,8 @@ class Namespace < ApplicationRecord has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory' has_many :achievements, class_name: 'Achievements::Achievement' has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail' + has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::Stage', foreign_key: :group_id, inverse_of: :namespace + has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ValueStream', foreign_key: :group_id, inverse_of: :namespace validates :owner, presence: true, if: ->(n) { n.owner_required? } validates :name, diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb index 738f18ca5e3..5152746abb4 100644 --- a/app/models/service_desk_setting.rb +++ b/app/models/service_desk_setting.rb @@ -3,6 +3,14 @@ class ServiceDeskSetting < ApplicationRecord include Gitlab::Utils::StrongMemoize + attribute :custom_email_enabled, default: false + attr_encrypted :custom_email_smtp_password, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_32, + encode: false, + encode_iv: false + belongs_to :project validates :project_id, presence: true validate :valid_issue_template @@ -13,8 +21,42 @@ class ServiceDeskSetting < ApplicationRecord allow_blank: true, format: { with: /\A[a-z0-9_]+\z/, message: -> (setting, data) { _("can contain only lowercase letters, digits, and '_'.") } } + validates :custom_email, + length: { maximum: 255 }, + uniqueness: true, + allow_nil: true, + format: /\A[\w\-._]+@[\w\-.]+\.{1}[a-zA-Z]{2,}\z/ + validates :custom_email_smtp_address, length: { maximum: 255 } + validates :custom_email_smtp_username, length: { maximum: 255 } + + validates :custom_email, + presence: true, + devise_email: true, + if: :custom_email_enabled? + validates :custom_email_smtp_address, + presence: true, + hostname: { allow_numeric_hostname: true, require_valid_tld: true }, + if: :custom_email_enabled? + validates :custom_email_smtp_username, + presence: true, + if: :custom_email_enabled? + validates :custom_email_smtp_port, + presence: true, + numericality: { only_integer: true, greater_than: 0 }, + if: :custom_email_enabled? + scope :with_project_key, ->(key) { where(project_key: key) } + def custom_email_delivery_options + { + user_name: custom_email_smtp_username, + password: custom_email_smtp_password, + address: custom_email_smtp_address, + domain: Mail::Address.new(custom_email).domain, + port: custom_email_smtp_port || 587 + } + end + def issue_template_content strong_memoize(:issue_template_content) do next unless issue_template_key.present? diff --git a/app/models/todo.rb b/app/models/todo.rb index 7bbdf321269..47dabc1533d 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -177,7 +177,7 @@ class Todo < ApplicationRecord end def resource_parent - project + project || group end def unmergeable? diff --git a/app/services/analytics/cycle_analytics/stages/base_service.rb b/app/services/analytics/cycle_analytics/stages/base_service.rb index b676eff0a0b..0f5415c9f9e 100644 --- a/app/services/analytics/cycle_analytics/stages/base_service.rb +++ b/app/services/analytics/cycle_analytics/stages/base_service.rb @@ -37,7 +37,7 @@ module Analytics end def value_stream - @value_stream ||= params[:value_stream] + @value_stream ||= params.fetch(:value_stream) end end end diff --git a/app/services/analytics/cycle_analytics/stages/list_service.rb b/app/services/analytics/cycle_analytics/stages/list_service.rb index a6b94ef8295..1cd7d3f5c6d 100644 --- a/app/services/analytics/cycle_analytics/stages/list_service.rb +++ b/app/services/analytics/cycle_analytics/stages/list_service.rb @@ -13,7 +13,7 @@ module Analytics private def allowed? - can?(current_user, :read_cycle_analytics, parent) + can?(current_user, :read_cycle_analytics, parent.project) end def success(stages) diff --git a/app/views/admin/application_settings/_runner_registrars_form.html.haml b/app/views/admin/application_settings/_runner_registrars_form.html.haml index 08486a808bf..baf7c5de7b9 100644 --- a/app/views/admin/application_settings/_runner_registrars_form.html.haml +++ b/app/views/admin/application_settings/_runner_registrars_form.html.haml @@ -4,7 +4,7 @@ %fieldset .gl-form-group %span.form-text.gl-mb-3.gl-mt-0 - = _('If no options are selected, only administrators can register runners.') + = s_('Runners|If both settings are disabled, new runners cannot be registered.') = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer' = hidden_field_tag "application_setting[valid_runner_registrars][]", nil - ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES.each do |type| diff --git a/app/views/authentication/_register.html.haml b/app/views/authentication/_register.html.haml index 5eed969ed35..d6fe20e48bf 100644 --- a/app/views/authentication/_register.html.haml +++ b/app/views/authentication/_register.html.haml @@ -9,13 +9,18 @@ - if current_user.two_factor_otp_enabled? .row.gl-mb-3 .col-md-5 - %button#js-setup-token-2fa-device.gl-button.btn.btn-confirm= _("Set up new device") + = render Pajamas::ButtonComponent.new(variant: :confirm, + button_options: { id: 'js-setup-token-2fa-device' }) do + = _("Set up new device") .col-md-7 %p= _("Your device needs to be set up. Plug it in (if needed) and click the button on the left.") - else .row.gl-mb-3 .col-md-4 - %button#js-setup-token-2fa-device.gl-button.btn.btn-confirm.btn-block{ disabled: true }= _("Set up new device") + = render Pajamas::ButtonComponent.new(variant: :confirm, + disabled: true, + button_options: { id: 'js-setup-token-2fa-device' }) do + = _("Set up new device") .col-md-8 %p= _("You need to register a two-factor authentication app before you can set up a device.") @@ -24,7 +29,8 @@ %div %p %span <%= error_message %> (<%= error_name %>) - %a.btn.btn-default.gl-button#js-token-2fa-try-again= _("Try again?") + = render Pajamas::ButtonComponent.new(button_options: { id: 'js-token-2fa-try-again' }) do + = _("Try again?") -# haml-lint:disable InlineJavaScript %script#js-register-token-2fa-registered{ type: "text/template" } @@ -37,4 +43,5 @@ = text_field_tag 'device_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name") .col-md-3 = hidden_field_tag 'device_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" - = submit_tag _("Register device"), class: "gl-button btn btn-confirm" + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do + = _("Register device") diff --git a/app/views/groups/usage_quotas/index.html.haml b/app/views/groups/usage_quotas/index.html.haml index a8c1071b876..253894e5fb8 100644 --- a/app/views/groups/usage_quotas/index.html.haml +++ b/app/views/groups/usage_quotas/index.html.haml @@ -1,7 +1,3 @@ - page_title s_("UsageQuota|Usage") -.gl-alert.gl-alert-no-icon.gl-alert-info.gl-mt-6 - %h2.gl-alert-title - Development - .gl-alert-content - Placeholder for usage quotas Vue app +#js-usage-quotas-view{ data: { namespace_name: @group.name } } diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 4c045574834..3add3af3c65 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -125,7 +125,12 @@ %span.gl-text-gray-500 = _("no name set") %td= registration[:created_at].to_date.to_s(:medium) - %td= link_to _('Delete'), registration[:delete_path], method: :delete, class: "gl-button btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.'), confirm_btn_variant: "danger" }, aria: { label: _('Delete') } + %td + = render Pajamas::ButtonComponent.new(variant: :danger, + href: registration[:delete_path], + method: :delete, + button_options: { class: 'float-right', data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.'), confirm_btn_variant: "danger" }, aria: { label: _('Delete') }}) do + = _('Delete') - else .settings-message.text-center diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index a27f076d5dd..f921f70dd0b 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -6,9 +6,4 @@ - graph_path = project_graph_path(@project, current_ref, format: :json) - commits_path = project_commits_path(@project, current_ref) -.sub-header-block.gl-bg-gray-10.gl-p-5 - .tree-ref-holder.gl-display-inline-block.gl-vertical-align-middle.gl-mr-3> - = render 'shared/ref_switcher', destination: 'graphs' - = link_to s_('Commits|History'), commits_path, class: 'btn gl-button btn-default' - -.js-contributors-graph{ class: container_class, data: { project_graph_path: graph_path, project_branch: current_ref, default_branch: @project.default_branch } } +.js-contributors-graph{ class: container_class, data: { project_graph_path: graph_path, project_branch: current_ref, default_branch: @project.default_branch, project_id: @project.id, commits_path: commits_path } } diff --git a/config/feature_flags/development/service_desk_custom_email.yml b/config/feature_flags/development/service_desk_custom_email.yml new file mode 100644 index 00000000000..42bac76033c --- /dev/null +++ b/config/feature_flags/development/service_desk_custom_email.yml @@ -0,0 +1,9 @@ +--- +name: service_desk_custom_email +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108017 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329990 +milestone: '15.9' +type: development +group: group::incubation +default_enabled: false +log_state_changes: true diff --git a/db/migrate/20230102131000_add_smtp_credentials_to_service_desk_settings.rb b/db/migrate/20230102131000_add_smtp_credentials_to_service_desk_settings.rb new file mode 100644 index 00000000000..8d3e3860ceb --- /dev/null +++ b/db/migrate/20230102131000_add_smtp_credentials_to_service_desk_settings.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class AddSmtpCredentialsToServiceDeskSettings < Gitlab::Database::Migration[2.1] + def up + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20230102131100_add_text_limits_to_smtp_credentials_on_service_desk_settings.rb + add_column :service_desk_settings, :custom_email_enabled, :boolean, default: false, null: false + # Unique constraint/index is added in 20230102131050_add_unique_constraint_for_custom_email_to_... + add_column :service_desk_settings, :custom_email, :text + add_column :service_desk_settings, :custom_email_smtp_address, :text + add_column :service_desk_settings, :custom_email_smtp_port, :integer + add_column :service_desk_settings, :custom_email_smtp_username, :text + # Encrypted attribute via attr_encrypted needs these two columns + add_column :service_desk_settings, :encrypted_custom_email_smtp_password, :binary + add_column :service_desk_settings, :encrypted_custom_email_smtp_password_iv, :binary + # rubocop:enable Migration/AddLimitToTextColumns + end + + def down + remove_column :service_desk_settings, :custom_email_enabled + remove_column :service_desk_settings, :custom_email + remove_column :service_desk_settings, :custom_email_smtp_address + remove_column :service_desk_settings, :custom_email_smtp_port + remove_column :service_desk_settings, :custom_email_smtp_username + remove_column :service_desk_settings, :encrypted_custom_email_smtp_password + remove_column :service_desk_settings, :encrypted_custom_email_smtp_password_iv + end +end diff --git a/db/migrate/20230102131050_add_unique_constraint_for_custom_email_to_service_desk_settings.rb b/db/migrate/20230102131050_add_unique_constraint_for_custom_email_to_service_desk_settings.rb new file mode 100644 index 00000000000..cfee56ececd --- /dev/null +++ b/db/migrate/20230102131050_add_unique_constraint_for_custom_email_to_service_desk_settings.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddUniqueConstraintForCustomEmailToServiceDeskSettings < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'custom_email_unique_constraint' + + def up + # Force custom_email to be unique instance-wide. This is neccessary because we will match + # incoming service desk emails with a custom email by the custom_email field. + # This also adds the corresponding index + add_concurrent_index(:service_desk_settings, :custom_email, unique: true, name: INDEX_NAME) + end + + def down + remove_concurrent_index_by_name(:service_desk_settings, INDEX_NAME) + end +end diff --git a/db/migrate/20230102131100_add_text_limits_to_smtp_credentials_on_service_desk_settings.rb b/db/migrate/20230102131100_add_text_limits_to_smtp_credentials_on_service_desk_settings.rb new file mode 100644 index 00000000000..60c96bd6bdb --- /dev/null +++ b/db/migrate/20230102131100_add_text_limits_to_smtp_credentials_on_service_desk_settings.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTextLimitsToSmtpCredentialsOnServiceDeskSettings < Gitlab::Database::Migration[2.1] + MAXIMUM_LIMIT = 255 + + disable_ddl_transaction! + + def up + add_text_limit :service_desk_settings, :custom_email, MAXIMUM_LIMIT + add_text_limit :service_desk_settings, :custom_email_smtp_address, MAXIMUM_LIMIT + add_text_limit :service_desk_settings, :custom_email_smtp_username, MAXIMUM_LIMIT + end + + def down + remove_text_limit :service_desk_settings, :custom_email + remove_text_limit :service_desk_settings, :custom_email_smtp_address + remove_text_limit :service_desk_settings, :custom_email_smtp_username + end +end diff --git a/db/post_migrate/20230117114739_clear_duplicate_jobs_cookies.rb b/db/post_migrate/20230117114739_clear_duplicate_jobs_cookies.rb new file mode 100644 index 00000000000..ce587e6c902 --- /dev/null +++ b/db/post_migrate/20230117114739_clear_duplicate_jobs_cookies.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# This is workaround for +# https://gitlab.com/gitlab-org/gitlab/-/issues/388253. During a +# zero-downtime upgrade, duplicate jobs cookies can fail to get deleted. +# This post-deployment migration deletes all such cookies. This can +# cause some jobs that normally would have been deduplicated to twice +# instead of once. +class ClearDuplicateJobsCookies < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + Gitlab::Redis::Queues.with do |redis| # rubocop:disable Cop/RedisQueueUsage + redis.scan_each(match: "resque:gitlab:duplicate:*:cookie:v2").each_slice(100) do |keys| + redis.del(keys) + end + end + end + + def down; end +end diff --git a/db/schema_migrations/20230102131000 b/db/schema_migrations/20230102131000 new file mode 100644 index 00000000000..2c0fa2c3f2a --- /dev/null +++ b/db/schema_migrations/20230102131000 @@ -0,0 +1 @@ +13b992cf6f30efc7a82062c5184f3e8398704c01e73618c6dd38071ee67595e1 \ No newline at end of file diff --git a/db/schema_migrations/20230102131050 b/db/schema_migrations/20230102131050 new file mode 100644 index 00000000000..03e9292d7e5 --- /dev/null +++ b/db/schema_migrations/20230102131050 @@ -0,0 +1 @@ +4933fd938c23b99963542c2f7e1f50e0270f6817ce49b0864fc7bdad63ea98b3 \ No newline at end of file diff --git a/db/schema_migrations/20230102131100 b/db/schema_migrations/20230102131100 new file mode 100644 index 00000000000..86af671bfe6 --- /dev/null +++ b/db/schema_migrations/20230102131100 @@ -0,0 +1 @@ +889e814bc9633481afeae8e63bfe080bfc956839fd5f97c0d39725f3acdff100 \ No newline at end of file diff --git a/db/schema_migrations/20230117114739 b/db/schema_migrations/20230117114739 new file mode 100644 index 00000000000..cb9fabfe4c4 --- /dev/null +++ b/db/schema_migrations/20230117114739 @@ -0,0 +1 @@ +f4ba0d1de73da2b7a912c06ca458898f3404235025089efc74aee9fc4caa511a \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index be11c779d29..7c0a50a174e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -21807,7 +21807,17 @@ CREATE TABLE service_desk_settings ( issue_template_key character varying(255), outgoing_name character varying(255), project_key character varying(255), - file_template_project_id bigint + file_template_project_id bigint, + custom_email_enabled boolean DEFAULT false NOT NULL, + custom_email text, + custom_email_smtp_address text, + custom_email_smtp_port integer, + custom_email_smtp_username text, + encrypted_custom_email_smtp_password bytea, + encrypted_custom_email_smtp_password_iv bytea, + CONSTRAINT check_57a79552e1 CHECK ((char_length(custom_email) <= 255)), + CONSTRAINT check_b283637a9e CHECK ((char_length(custom_email_smtp_address) <= 255)), + CONSTRAINT check_e3535d46ee CHECK ((char_length(custom_email_smtp_username) <= 255)) ); CREATE TABLE shards ( @@ -28334,6 +28344,8 @@ CREATE UNIQUE INDEX commit_user_mentions_on_commit_id_and_note_id_unique_index O CREATE INDEX composer_cache_files_index_on_deleted_at ON packages_composer_cache_files USING btree (delete_at, id); +CREATE UNIQUE INDEX custom_email_unique_constraint ON service_desk_settings USING btree (custom_email); + CREATE UNIQUE INDEX dast_scanner_profiles_builds_on_ci_build_id ON dast_scanner_profiles_builds USING btree (ci_build_id); CREATE UNIQUE INDEX dast_site_profiles_builds_on_ci_build_id ON dast_site_profiles_builds USING btree (ci_build_id); diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md index 2093d55d8c0..86c80c06f16 100644 --- a/doc/administration/incoming_email.md +++ b/doc/administration/incoming_email.md @@ -798,20 +798,19 @@ incoming_email: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11. GitLab can read incoming email using the Microsoft Graph API instead of -IMAP. Because [Microsoft is deprecating IMAP usage with Basic Authentication](https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-support-for-imap-and-smtp-auth-protocols-in/ba-p/1330432), the Microsoft Graph API will soon be required for new Microsoft Exchange Online -mailboxes. +IMAP. Because [Microsoft is deprecating IMAP usage with Basic Authentication](https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-support-for-imap-and-smtp-auth-protocols-in/ba-p/1330432), the Microsoft Graph API is be required for new Microsoft Exchange Online mailboxes. -To configure GitLab for Microsoft Graph, you will need to register an -OAuth2 application in your Azure Active Directory that has the +To configure GitLab for Microsoft Graph, you need to register an +OAuth 2.0 application in your Azure Active Directory that has the `Mail.ReadWrite` permission for all mailboxes. See the [MailRoom step-by-step guide](https://github.com/tpitale/mail_room/#microsoft-graph-configuration) and [Microsoft instructions](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) for more details. -Record the following when you configure your OAuth2 application: +Record the following when you configure your OAuth 2.0 application: - Tenant ID for your Azure Active Directory -- Client ID for your OAuth2 application -- Client secret your OAuth2 application +- Client ID for your OAuth 2.0 application +- Client secret your OAuth 2.0 application ##### Restrict mailbox access diff --git a/doc/administration/postgresql/replication_and_failover.md b/doc/administration/postgresql/replication_and_failover.md index ececa12f3ad..46890b0b2ca 100644 --- a/doc/administration/postgresql/replication_and_failover.md +++ b/doc/administration/postgresql/replication_and_failover.md @@ -26,7 +26,7 @@ replication failover requires: - A minimum of three PgBouncer nodes that track and handle primary database reads and writes. - An internal load balancer (TCP) to balance requests between the PgBouncer nodes. - [Database Load Balancing](database_load_balancing.md) enabled. - - A local PgBouncer service configured on each PostgreSQL node. Note that this is separate from the main PgBouncer cluster that tracks the primary. + - A local PgBouncer service configured on each PostgreSQL node. This is separate from the main PgBouncer cluster that tracks the primary. ```plantuml @startuml @@ -356,7 +356,7 @@ If you enable Monitoring, it must be enabled on **all** database servers. #### Enable TLS support for the Patroni API -By default, Patroni's [REST API](https://patroni.readthedocs.io/en/latest/rest_api.html#rest-api) is served over HTTP. +By default, the Patroni [REST API](https://patroni.readthedocs.io/en/latest/rest_api.html#rest-api) is served over HTTP. You have the option to enable TLS and use HTTPS over the same [port](../package_information/defaults.md). To enable TLS, you need PEM-formatted certificate and private key files. Both files must be readable by the PostgreSQL user (`gitlab-psql` by default, or the one set by `postgresql['username']`): @@ -1009,7 +1009,7 @@ Here are a few key facts that you must consider before upgrading PostgreSQL: GitLab deployment is down for the duration of database upgrade or, at least, as long as your leader node is upgraded. This can be **a significant downtime depending on the size of your database**. -- Upgrading PostgreSQL creates a new data directory with a new control data. From Patroni's perspective this is a new cluster that needs to be bootstrapped again. Therefore, as part of the upgrade procedure, the cluster state (stored in Consul) is wiped out. After the upgrade is complete, Patroni bootstraps a new cluster. **This changes your _cluster ID_**. +- Upgrading PostgreSQL creates a new data directory with a new control data. From the perspective of Petroni, this is a new cluster that needs to be bootstrapped again. Therefore, as part of the upgrade procedure, the cluster state (stored in Consul) is wiped out. After the upgrade is complete, Patroni bootstraps a new cluster. **This changes your _cluster ID_**. - The procedures for upgrading leader and replicas are not the same. That is why it is important to use the right procedure on each node. @@ -1017,7 +1017,7 @@ Here are a few key facts that you must consider before upgrading PostgreSQL: configured replication method (`pg_basebackup` is the only available option). It might take some time for replica to catch up with the leader, depending on the size of your database. -- An overview of the upgrade procedure is outlined in [Patroni's documentation](https://patroni.readthedocs.io/en/latest/existing_data.html#major-upgrade-of-postgresql-version). +- An overview of the upgrade procedure is outlined in [the Patroni documentation](https://patroni.readthedocs.io/en/latest/existing_data.html#major-upgrade-of-postgresql-version). You can still use `gitlab-ctl pg-upgrade` which implements this procedure with a few adjustments. Considering these, you should carefully plan your PostgreSQL upgrade: @@ -1322,7 +1322,7 @@ If a replica cannot start or rejoin the cluster, or when it lags behind and cann +-------------------------------------+--------------+---------+--------------+----+-----------+ ``` -1. Sign in to the broken server and reinitialize the database and replication. Patroni will shut +1. Sign in to the broken server and reinitialize the database and replication. Patroni shuts down PostgreSQL on that server, remove the data directory, and reinitialize it from scratch: ```shell @@ -1330,7 +1330,7 @@ If a replica cannot start or rejoin the cluster, or when it lags behind and cann ``` This can be run on any Patroni node, but be aware that `sudo gitlab-ctl patroni - reinitialize-replica` without `--member` will reinitialize the server it is run on. + reinitialize-replica` without `--member` restarts the server it is run on. It is recommended to run it locally on the broken server to reduce the risk of unintended data loss. 1. Monitor the logs: diff --git a/doc/administration/system_hooks.md b/doc/administration/system_hooks.md index 038c26a9c2e..ddfa9fe9860 100644 --- a/doc/administration/system_hooks.md +++ b/doc/administration/system_hooks.md @@ -133,7 +133,7 @@ X-Gitlab-Event: System Hook } ``` -Note that `project_rename` is not triggered if the namespace changes. +`project_rename` is not triggered if the namespace changes. Refer to `group_rename` and `user_rename` for that case. **Project transferred:** diff --git a/doc/api/environments.md b/doc/api/environments.md index 5ecefee7eff..eeaadb6ccdc 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -15,12 +15,12 @@ Get all environments for a given project. GET /projects/:id/environments ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `name` | string | no | Return the environment with this name. Mutually exclusive with `search` | -| `search` | string | no | Return list of environments matching the search criteria. Mutually exclusive with `name`. Must be at least 3 characters long. | -| `states` | string | no | List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments. | +| Attribute | Type | Required | Description | +|-----------|----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded](rest/index.md#namespaced-path-encoding) path of the project. | +| `name` | string | no | Return the environment with this name. Mutually exclusive with `search`. | +| `search` | string | no | Return list of environments matching the search criteria. Mutually exclusive with `name`. Must be at least 3 characters long. | +| `states` | string | no | List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments. | ```shell curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/environments?name=review%2Ffix-foo" @@ -133,10 +133,10 @@ Example response: GET /projects/:id/environments/:environment_id ``` -| Attribute | Type | Required | Description | -|-----------|---------|----------|---------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `environment_id` | integer | yes | The ID of the environment | +| Attribute | Type | Required | Description | +|------------------|----------------|----------|--------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | +| `environment_id` | integer | yes | The ID of the environment. | ```shell curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/environments/1" @@ -252,12 +252,12 @@ It returns `201` if the environment was successfully created, `400` for wrong pa POST /projects/:id/environments ``` -| Attribute | Type | Required | Description | -| ------------- | ------- | -------- | ---------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `name` | string | yes | The name of the environment | -| `external_url` | string | no | Place to link to for this environment | -| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other` | +| Attribute | Type | Required | Description | +|----------------|----------------|----------|---------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | +| `name` | string | yes | The name of the environment. | +| `external_url` | string | no | Place to link to for this environment. | +| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`. | ```shell curl --data "name=deploy&external_url=https://deploy.gitlab.example.com" \ @@ -289,13 +289,13 @@ It returns `200` if the environment was successfully updated. In case of an erro PUT /projects/:id/environments/:environments_id ``` -| Attribute | Type | Required | Description | -| --------------- | ------- | --------------------------------- | ------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `environment_id` | integer | yes | The ID of the environment | -| `name` | string | no | [Deprecated and will be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338897) | -| `external_url` | string | no | The new `external_url` | -| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other` | +| Attribute | Type | Required | Description | +|------------------|----------------|----------|---------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | +| `environment_id` | integer | yes | The ID of the environment. | +| `name` | string | no | [Deprecated and will be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338897). | +| `external_url` | string | no | The new `external_url`. | +| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`. | ```shell curl --request PUT --data "name=staging&external_url=https://staging.gitlab.example.com" \ @@ -325,10 +325,10 @@ It returns `204` if the environment was successfully deleted, and `404` if the e DELETE /projects/:id/environments/:environment_id ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | --------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `environment_id` | integer | yes | The ID of the environment | +| Attribute | Type | Required | Description | +|------------------|----------------|----------|--------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | +| `environment_id` | integer | yes | The ID of the environment. | ```shell curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/environments/1" @@ -348,12 +348,12 @@ By default, it only deletes environments 30 days or older. You can change this d DELETE /projects/:id/environments/review_apps ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | --------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | -| `before` | datetime | no | The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). | -| `limit` | integer | no | Maximum number of environments to delete. Defaults to 100. | -| `dry_run` | boolean | no | Defaults to `true` for safety reasons. It performs a dry run where no actual deletion will be performed. Set to `false` to actually delete the environment. | +| Attribute | Type | Required | Description | +|-----------|----------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | +| `before` | datetime | no | The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). | +| `limit` | integer | no | Maximum number of environments to delete. Defaults to 100. | +| `dry_run` | boolean | no | Defaults to `true` for safety reasons. It performs a dry run where no actual deletion is performed. Set to `false` to actually delete the environment. | ```shell curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/environments/review_apps" @@ -389,11 +389,11 @@ It returns `200` if the environment was successfully stopped, and `404` if the e POST /projects/:id/environments/:environment_id/stop ``` -| Attribute | Type | Required | Description | -|------------------|----------------|----------|----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `environment_id` | integer | yes | The ID of the environment | -| `force` | boolean | no | Force environment to stop without executing `on_stop` actions | +| Attribute | Type | Required | Description | +|------------------|----------------|----------|--------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | +| `environment_id` | integer | yes | The ID of the environment. | +| `force` | boolean | no | Force environment to stop without executing `on_stop` actions. | ```shell curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/environments/1/stop" @@ -423,7 +423,7 @@ POST /projects/:id/environments/stop_stale | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | | `before` | date | yes | Stop environments that have been modified or deployed to before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Valid inputs are between 10 years ago and 1 week ago | ```shell diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9e4391b8abe..cadff0bbdf6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -389,6 +389,8 @@ four standard [pagination arguments](#connection-pagination-arguments): | `searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. | | `sort` | [`String`](#string) | Sort order of results. Format: `_`, for example: `id_desc` or `name_asc`. | | `topics` | [`[String!]`](#string) | Filter projects by topics. | +| `withIssuesEnabled` | [`Boolean`](#boolean) | Return only projects with issues enabled. | +| `withMergeRequestsEnabled` | [`Boolean`](#boolean) | Return only projects with merge requests enabled. | ### `Query.queryComplexity` @@ -14343,6 +14345,8 @@ four standard [pagination arguments](#connection-pagination-arguments): | `includeSubgroups` | [`Boolean`](#boolean) | Include also subgroup projects. | | `search` | [`String`](#string) | Search project with most similar names or paths. | | `sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. | +| `withIssuesEnabled` | [`Boolean`](#boolean) | Return only projects with issues enabled. | +| `withMergeRequestsEnabled` | [`Boolean`](#boolean) | Return only projects with merge requests enabled. | ##### `Group.runners` @@ -16548,6 +16552,8 @@ four standard [pagination arguments](#connection-pagination-arguments): | `includeSubgroups` | [`Boolean`](#boolean) | Include also subgroup projects. | | `search` | [`String`](#string) | Search project with most similar names or paths. | | `sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. | +| `withIssuesEnabled` | [`Boolean`](#boolean) | Return only projects with issues enabled. | +| `withMergeRequestsEnabled` | [`Boolean`](#boolean) | Return only projects with merge requests enabled. | ##### `Namespace.scanExecutionPolicies` @@ -18718,6 +18724,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `iid` | [`String`](#string) | IID of the issue. For example, "1". | | `iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. | | `in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. | +| `requirementLegacyWidget` **{warning-solid}** | [`RequirementLegacyFilterInput`](#requirementlegacyfilterinput) | **Deprecated** in 15.9. Use work item IID filter instead. | | `search` | [`String`](#string) | Search query for title or description. | | `sort` | [`WorkItemSort`](#workitemsort) | Sort work items by this criteria. | | `state` | [`IssuableState`](#issuablestate) | Current state of this work item. | @@ -22693,6 +22700,7 @@ Values for sorting projects. | Value | Description | | ----- | ----------- | +| `ACTIVITY_DESC` | Sort by latest activity, in descending order. | | `SIMILARITY` | Most similar to the search query. | | `STORAGE` | Sort by storage size. | @@ -25277,6 +25285,14 @@ Fields that are available when modifying release assets. | ---- | ---- | ----------- | | `links` | [`[ReleaseAssetLinkInput!]`](#releaseassetlinkinput) | List of asset links to associate to the release. | +### `RequirementLegacyFilterInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `legacyIids` | [`[String!]!`](#string) | List of legacy requirement IIDs of work items. or example `["1", "2"]`. | + ### `SastCiConfigurationAnalyzersEntityInput` Represents the analyzers entity in SAST CI configuration. diff --git a/doc/api/product_analytics.md b/doc/api/product_analytics.md index 90df1090f62..1856dd07b4a 100644 --- a/doc/api/product_analytics.md +++ b/doc/api/product_analytics.md @@ -79,3 +79,15 @@ GET /projects/:id/product_analytics/request/meta | Attribute | Type | Required | Description | | --------- |------------------| -------- |---------------------------------------------------------------| | `id` | integer | yes | The ID of a project that the current user has read access to. | + +## List a project's funnels + +List all funnels for a project. For example: + +```plaintext +GET /projects/:id/product_analytics/funnels +``` + +| Attribute | Type | Required | Description | +| --------- |------------------| -------- |--------------------------------------------------------------------| +| `id` | integer | yes | The ID of a project that the current user has the Developer role for. | diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index d31bba73900..2b930760011 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -163,32 +163,32 @@ information such as what the variable is used for, and what the acceptable value Job-level variables cannot be pre-filled. In manually-triggered pipelines, the **Run pipeline** page displays all pipeline-level variables -with a `description` defined in the `.gitlab-ci.yml` file. The description displays +that have a `description` defined in the `.gitlab-ci.yml` file. The description displays below the variable. You can change the prefilled value, which overrides the value for that single pipeline run. -If you do not define a `value` for the variable in the configuration file, the variable still displays, +If you do not define a `value` for the variable in the configuration file, the variable name is still listed, but the value field is blank. For example: ```yaml variables: - TEST_SUITE: - description: "The test suite that will run. Valid options are: 'default', 'short', 'full'." - value: "default" + DEPLOY_CREDENTIALS: + description: "The deployment credentials." DEPLOY_ENVIRONMENT: description: "Select the deployment target. Valid options are: 'canary', 'staging', 'production', or a stable branch of your choice." + value: "canary" ``` In this example: -- `TEST_SUITE` is pre-filled in the **Run pipeline** page with `default`, - and the message explains the other options. -- `DEPLOY_ENVIRONMENT` is listed in the **Run pipeline** page, but with no value set. +- `DEPLOY_CREDENTIALS` is listed in the **Run pipeline** page, but with no value set. The user is expected to define the value each time the pipeline is run manually. +- `DEPLOY_ENVIRONMENT` is pre-filled in the **Run pipeline** page with `canary` as the default value, + and the message explains the other options. -##### Configure a list of selectable values for a prefilled variable +#### Configure a list of selectable prefilled variable values > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. Disabled by default. > - The `options` keyword was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105502) in GitLab 15.7. diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 9e2562b8dc3..6165cce9d46 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -4398,35 +4398,89 @@ deploy_review_job: #### `variables:description` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7. -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5, `variables:value` can contain an array of values. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7. -Use the `description` keyword to define a [pipeline-level (global) variable that is prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines) -when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually). - -If used with `value`, the variable value is also prefilled when running a pipeline manually. +Use the `description` keyword to define a description for a pipeline-level (global) variable. +The description displays with [the prefilled variable name when running a pipeline manually](../pipelines/index.md#prefill-variables-in-manual-pipelines). **Keyword type**: Global keyword. You cannot use it for job-level variables. **Possible inputs**: - A string. -- An array of strings. **Example of `variables:description`**: ```yaml variables: - DEPLOY_ENVIRONMENT: - description: "The deployment target. Change this variable to 'canary' or 'production' if needed." - value: "staging" + DEPLOY_NOTE: + description: "The deployment note. Explain the reason for this deployment." ``` **Additional details**: -- A global variable defined with `value` but no `description` behaves the same as - [`variables`](#variables). -- `variables:value` can [contain an array of selectable values](../pipelines/index.md#configure-a-list-of-selectable-values-for-a-prefilled-variable). +- When used without `value`, the variable exists in pipelines that were not triggered manually, + and the default value is an empty string (`''`). + +#### `variables:value` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7. + +Use the `value` keyword to define a pipeline-level (global) variable's value. When used with +[`variables: description`](#variablesdescription), the variable value is [prefilled when running a pipeline manually](../pipelines/index.md#prefill-variables-in-manual-pipelines). + +**Keyword type**: Global keyword. You cannot use it for job-level variables. + +**Possible inputs**: + +- A string. + +**Example of `variables:value`**: + +```yaml +variables: + DEPLOY_ENVIRONMENT: + value: "staging" + description: "The deployment target. Change this variable to 'canary' or 'production' if needed." +``` + +**Additional details**: + +- If used without [`variables: description`](#variablesdescription), the behavior is + the same as [`variables`](#variables). + +#### `variables:options` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105502) in GitLab 15.7. + +Use `variables:options` to define an array of values that are [selectable in the UI when running a pipeline manually](../pipelines/index.md#configure-a-list-of-selectable-prefilled-variable-values). + +Must be used with `variables: value`, and the string defined for `value`: + +- Must also be one of the strings in the `options` array. +- Is the default selection. + +If there is no [`description`](#variablesdescription), +this keyword has no effect. + +**Keyword type**: Global keyword. You cannot use it for job-level variables. + +**Possible inputs**: + +- An array of strings. + +**Example of `variables:options`**: + +```yaml +variables: + DEPLOY_ENVIRONMENT: + value: "staging" + options: + - "production" + - "staging" + - "canary" + description: "The deployment target. Set to 'staging' by default." +``` #### `variables:expand` diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 60c81b26c05..073a0351ec5 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -45,7 +45,7 @@ By default, this task does not delete anything but shows how many file reference delete. Run the command with `DRY_RUN=false` if you actually want to delete the references. You can also use `LIMIT={number}` parameter to limit the number of deleted references. -Note that this Rake task only removes the references to LFS files. Unreferenced LFS files are garbage-collected +This Rake task only removes the references to LFS files. Unreferenced LFS files are garbage-collected later (once a day). If you need to garbage collect them immediately, run `rake gitlab:cleanup:orphan_lfs_files` described below. diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index 05c51fa06d8..5ab62215570 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -23,7 +23,7 @@ migrate projects using either: - [Direct transfer](../user/group/import/index.md#migrate-groups-by-direct-transfer-recommended). - [An export file](../user/project/settings/import_export.md). -Note that: +When you import a repository: - The owner of the project is the first administrator. - The groups are created as needed, including subgroups. @@ -159,7 +159,7 @@ project.set_full_path ``` In a Rails console session, run the following to migrate all of a namespace's -projects (this may take a while if there are 1000s of projects in a namespace): +projects (this may take a while if there are thousands of projects in a namespace): ```ruby namespace = Namespace.find_by_full_path('gitlab-org') diff --git a/doc/update/background_migrations.md b/doc/update/background_migrations.md index 38ea82667ca..1f9ef9d430b 100644 --- a/doc/update/background_migrations.md +++ b/doc/update/background_migrations.md @@ -275,7 +275,7 @@ arguments until the status query returns no rows. ##### For a no-downtime deployment As the failing migrations are post-deployment migrations, you can remain on a running instance of the upgraded -version and wait for the batched background migrations to finish normally. +version and wait for the batched background migrations to finish. 1. [Check the status](#check-the-status-of-batched-background-migrations) of the batched background migration from the error message, and make sure it is listed as finished. If it is still active, either wait until it is done, diff --git a/doc/user/admin_area/settings/external_authorization.md b/doc/user/admin_area/settings/external_authorization.md index 09ac477b062..94d9ec73640 100644 --- a/doc/user/admin_area/settings/external_authorization.md +++ b/doc/user/admin_area/settings/external_authorization.md @@ -96,7 +96,7 @@ When denying access, a `reason` can be optionally specified in the JSON body: Any other status code than 200, 401 or 403 also deny access to the user, but the response isn't cached. -If the service times out (after 500ms), a message "External Policy Server did +If the service times out (after 500 ms), a message "External Policy Server did not respond" is displayed. ## Classification labels diff --git a/doc/user/application_security/dast/authentication.md b/doc/user/application_security/dast/authentication.md index 867194fdc81..de3bdad3c42 100644 --- a/doc/user/application_security/dast/authentication.md +++ b/doc/user/application_security/dast/authentication.md @@ -8,10 +8,14 @@ type: reference, howto # DAST authentication **(ULTIMATE)** WARNING: -**Never** run an authenticated scan against a production server. -Authenticated scans may perform *any* function that the authenticated user can, +**DO NOT** use credentials that are valid for production systems, production servers, or any that +contain production data. + +WARNING: +**DO NOT** run an authenticated scan against a production server. +Authenticated scans may perform **any** function that the authenticated user can, including modifying or deleting data, submitting forms, and following links. -Only run an authenticated scan against a test server. +Only run an authenticated scan against non-production systems or servers. Authentication logs a user in before a DAST scan so that the analyzer can test as much of the application as possible when searching for vulnerabilities. diff --git a/doc/user/application_security/iac_scanning/index.md b/doc/user/application_security/iac_scanning/index.md index 24448dc9668..3aee1ae6d3b 100644 --- a/doc/user/application_security/iac_scanning/index.md +++ b/doc/user/application_security/iac_scanning/index.md @@ -16,7 +16,7 @@ IaC Scanning supports configuration files for Terraform, Ansible, AWS CloudForma IaC Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required. -We recommend a minimum of 4GB RAM to ensure consistent performance. +We recommend a minimum of 4 GB RAM to ensure consistent performance. To run IaC Scanning jobs, by default, you need GitLab Runner with the [`docker`](https://docs.gitlab.com/runner/executors/docker.html) or @@ -222,13 +222,13 @@ To override the automatic update behavior, set the `SAST_ANALYZER_IMAGE_TAG` CI/ in your CI/CD configuration file after you include the [`SAST-IaC.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/SAST-IaC.gitlab-ci.yml). Only set this variable in a specific job. -If you set it [at the top level](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file), the version you set will be used for other SAST analyzers. +If you set it [at the top level](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file), the version you set is used for other SAST analyzers. You can set the tag to: -- A major version, like `3`. Your pipelines will use any minor or patch updates that are released within this major version. -- A minor version, like `3.7`. Your pipelines will use any patch updates that are released within this minor version. -- A patch version, like `3.7.0`. Your pipelines won't receive any updates. +- A major version, like `3`. Your pipelines use any minor or patch updates that are released within this major version. +- A minor version, like `3.7`. Your pipelines use any patch updates that are released within this minor version. +- A patch version, like `3.7.0`. Your pipelines don't receive any updates. This example uses a specific minor version of the `KICS` analyzer: diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md index de4e9c3d36c..28f536972ea 100644 --- a/doc/user/product_analytics/index.md +++ b/doc/user/product_analytics/index.md @@ -117,3 +117,77 @@ The example below includes three dashboards and one visualization that applies t ├── visualizations │ └── example_line_chart.yaml ``` + +## Funnel analysis + +Funnel analysis can be used to understand the flow of users through your application and where +users drop out of a predefined flow (for example, a checkout process or ticket purchase). + +Each product can also define an unlimited number of funnels. +These funnels are defined using our YAML schema and stored in the `.gitlab/product_analytics/funnels/` directory of a project repository. + +Funnel definitions must include the keys `name`, `seconds_to_convert`, and an array of `steps`. + +| Key | Description | +|----------------------|----------------------------------------------------------| +| `name` | The name of the funnel. | +| `seconds_to_convert` | The number of seconds a user has to complete the funnel. | +| `steps` | An array of funnel steps. | + +Each step must include the keys `name`, `target`, and `action`. + +| Key | Description | +|----------|------------------------------------------------------------------------------------------| +| `name` | The name of the step. This should be a unique slug. | +| `action` | The action performed. (Only `pageview` is supported.) | +| `target` | The target of the step. (Because only `pageview` is supported, this should be a path.) | + +### Example funnel definition + +```yaml +name: completed_purchase +seconds_to_convert: 3600 +steps: + - name: view_page_1 + target: '/page1.html' + action: 'pageview' + - name: view_page_2 + target: '/page2.html' + action: 'pageview' + - name: view_page_3 + target: '/page3.html' + action: 'pageview' +``` + +### Query a funnel + +You can [query the funnel data with the REST API](../../api/product_analytics.md#send-query-request-to-cube). +To do this, you can use the example query body below, where you need to replace `FUNNEL_NAME` with your funnel's name. + +NOTE: +The `afterDate` filter is not supported. Please use `beforeDate` or `inDateRange`. + +```json +{ + "query": { + "measures": [ + "FUNNEL_NAME.count" + ], + "order": { + "completed_purchase.count": "desc" + }, + "filters": [ + { + "member": "FUNNEL_NAME.date", + "operator": "beforeDate", + "values": [ + "2023-02-01" + ] + } + ], + "dimensions": [ + "FUNNEL_NAME.step" + ] + } +} +``` diff --git a/doc/user/project/badges.md b/doc/user/project/badges.md index 8a855c02b8d..bc8205a57cb 100644 --- a/doc/user/project/badges.md +++ b/doc/user/project/badges.md @@ -19,21 +19,24 @@ project maintainers. Badges can be added to a project by Maintainers or Owners, and are visible on the project's overview page. If you find that you have to add the same badges to several projects, you may want to add them at the [group level](#group-badges). +### Add a badge to a project + To add a new badge to a project: 1. On the top bar, select **Main menu > Projects** and find your project. 1. On the left sidebar, select **Settings > General**. 1. Expand **Badges**. -1. Under "Link", enter the URL that the badges should point to and under - "Badge image URL" the URL of the image that should be displayed. +1. Under **Link**, enter the URL that the badges should point to. +1. Under **Badge image URL**, enter the URL of the image that should be displayed. 1. Select **Add badge**. -After adding a badge to a project, you can see it in the list below the form. -You can edit the badge by selecting **Edit** (**{pencil}**) next to it or delete it by -selecting **Delete** (**{remove}**). +After adding a badge to a project, you can see the badge in the list below the form. -Badges associated with a group can only be edited or deleted on the -[group level](#group-badges). +### Edit or delete a project badge + +To edit a badge, select **Edit** (**{pencil}**). + +To delete a badge, select **Delete** (**{remove}**). ### Example project badge: Pipeline Status @@ -66,6 +69,8 @@ If you need individual badges for each project, either: - Add the badge at the [project level](#project-badges). - Use [placeholders](#placeholders). +### Add a badge to a group + To add a new badge to a group: 1. On the top bar, select **Main menu > Groups** and find your group. @@ -76,17 +81,20 @@ To add a new badge to a group: 1. Select **Add badge**. After adding a badge to a group, you can see it in the list below the form. -You can edit the badge by selecting **Edit** (**{pencil}**) next to it or delete it by -selecting **Delete** (**{remove}**). -Badges directly associated with a project can be configured on the -[project level](#project-badges). +### Edit or delete a group badge + +To edit a badge, select **Edit** (**{pencil}**). + +To delete a badge, select **Delete** (**{remove}**). + +Badges associated with a group can be edited or deleted only at the [group level](#group-badges). ## Placeholders -Both the URL a badge points to and the image URL can contain placeholders -which are evaluated when displaying the badge. The following placeholders -are available: +Both the URL a badge points to and the image URL can contain placeholders, +which are evaluated when displaying the badge. +The following placeholders are available: - `%{project_path}`: Path of a project including the parent groups - `%{project_title}`: Title of a project @@ -99,7 +107,7 @@ are available: NOTE: Placeholders allow badges to expose otherwise-private information, such as the default branch or commit SHA when the project is configured to have a private -repository. This is by design, as badges are intended to be used publicly. Avoid +repository. This behavior is intentional, as badges are intended to be used publicly. Avoid using these placeholders if the information is sensitive. ## Use custom badge images @@ -118,7 +126,7 @@ Using placeholders, here is an example badge image URL referring to a raw image https://gitlab.example.com//-/raw//my-image.svg ``` -To add a new badge to a group or project with a custom image: +To add a new badge with a custom image to a group or project: 1. On the top bar, select **Main menu** and find your group or project. 1. On the left sidebar, select **Settings > General**. @@ -129,11 +137,11 @@ To add a new badge to a group or project with a custom image: displayed. 1. Select **Add badge**. -To learn how to use custom images generated via a pipeline, see our documentation on +To learn how to use custom images generated through a pipeline, see the documentation on [accessing the latest job artifacts by URL](../../ci/pipelines/job_artifacts.md#access-the-latest-job-artifacts). -## API +## Configure badges through the API You can also configure badges via the GitLab API. As in the settings, there is -a distinction between endpoints for badges on the +a distinction between endpoints for badges at the [project level](../../api/project_badges.md) and [group level](../../api/group_badges.md). diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md index 4ef3a847ef1..afefe80271e 100644 --- a/doc/user/project/integrations/prometheus_library/index.md +++ b/doc/user/project/integrations/prometheus_library/index.md @@ -35,7 +35,7 @@ GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment. -In order to isolate and only display relevant metrics for a given environment, +To isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that, GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md index e6f2ac2753a..f8057866592 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md @@ -42,6 +42,6 @@ Managing these settings depends on how NGINX Ingress has been deployed. If you h ## Specifying the Environment label -In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab searches for metrics with appropriate labels. In this case, the `upstream` label must be of the form `--*`. +To isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab searches for metrics with appropriate labels. In this case, the `upstream` label must be of the form `--*`. If you have used [Auto Deploy](../../../../topics/autodevops/stages.md#auto-deploy) to deploy your app, this format is used automatically and metrics are detected with no action on your part. diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md index fde839c3ba4..67b9eaac4c8 100644 --- a/doc/user/report_abuse.md +++ b/doc/user/report_abuse.md @@ -28,6 +28,7 @@ To report abuse from a user's profile page: 1. Anywhere in GitLab, select the name of the user. 1. In the top right corner of the user's profile, select **Report abuse to administrator** (**{information-o}**). +1. Select a reason for reporting the user. 1. Complete an abuse report. 1. Select **Send report**. @@ -37,6 +38,7 @@ To report abuse from a user's comment: 1. In the comment, in the top right corner, select **More actions** (**{ellipsis_v}**). 1. Select **Report abuse to administrator**. +1. Select a reason for reporting the user. 1. Complete an abuse report. 1. Select **Send report**. @@ -48,14 +50,16 @@ A URL to the reported user's comment is pre-filled in the abuse report's 1. On the issue, in the top right corner, select the vertical ellipsis (**{ellipsis_v}**). 1. Select **Report abuse to administrator**. -1. Submit an abuse report. +1. Select a reason for reporting the user. +1. Complete an abuse report. 1. Select **Send report**. ## Report abuse from a merge request 1. On the merge request, in the top right corner, select the vertical ellipsis (**{ellipsis_v}**). 1. Select **Report abuse to administrator**. -1. Submit an abuse report. +1. Select a reason for reporting this user. +1. Complete an abuse report. 1. Select **Send report**. ## Related topics diff --git a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb index c7987d63153..ca8b4a3a890 100644 --- a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb +++ b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb @@ -57,9 +57,14 @@ module Gitlab end def add_parent_model_params!(finder_params) - raise(ArgumentError, "unknown parent_class: #{parent_class}") unless parent_class.eql?(Project) - - finder_params[:project_id] = stage.parent_id + case stage.parent + when Namespaces::ProjectNamespace + finder_params[:project_id] = stage.parent.project.id + when Project + finder_params[:project_id] = stage.parent_id + else + raise(ArgumentError, "unknown parent_class: #{parent_class}") + end end def add_time_range_params!(finder_params, from, to) diff --git a/lib/gitlab/database/load_balancing/load_balancer.rb b/lib/gitlab/database/load_balancing/load_balancer.rb index cb3a378ad64..23476e1f5e9 100644 --- a/lib/gitlab/database/load_balancing/load_balancer.rb +++ b/lib/gitlab/database/load_balancing/load_balancer.rb @@ -105,11 +105,9 @@ module Gitlab def read_write connection = nil transaction_open = nil - attempts = 3 - if prevent_load_balancer_retries_in_transaction? - attempts = 1 if pool.connection.transaction_open? - end + # Retry only once when in a transaction (see https://gitlab.com/gitlab-org/gitlab/-/issues/220242) + attempts = pool.connection.transaction_open? ? 1 : 3 # In the event of a failover the primary may be briefly unavailable. # Instead of immediately grinding to a halt we'll retry the operation @@ -348,10 +346,6 @@ module Gitlab row = ar_connection.select_all(sql).first row['location'] if row end - - def prevent_load_balancer_retries_in_transaction? - Gitlab::Utils.to_boolean(ENV['PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION'], default: false) - end end end end diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index b3559bde988..fc5aab861ca 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -26,6 +26,7 @@ namespace :tw do CodeOwnerRule.new('Certify', '@msedlakjakubowski'), CodeOwnerRule.new('Code Review', '@aqualls'), CodeOwnerRule.new('Compliance', '@eread'), + CodeOwnerRule.new('Commerce Integrations', '@drcatherinepope'), CodeOwnerRule.new('Composition Analysis', '@rdickenson'), CodeOwnerRule.new('Configure', '@phillipwells'), CodeOwnerRule.new('Container Registry', '@claytoncornell'), @@ -50,11 +51,11 @@ namespace :tw do CodeOwnerRule.new('Knowledge', '@aqualls'), CodeOwnerRule.new('Application Performance', '@jglassman1'), CodeOwnerRule.new('Monitor', '@msedlakjakubowski'), - CodeOwnerRule.new('Observability', '@msedlakjakubowski'), + CodeOwnerRule.new('Observability', '@drcatherinepope'), CodeOwnerRule.new('Optimize', '@lciutacu'), CodeOwnerRule.new('Package Registry', '@claytoncornell'), CodeOwnerRule.new('Pipeline Authoring', '@marcel.amirault'), - CodeOwnerRule.new('Pipeline Execution', '@marcel.amirault'), + CodeOwnerRule.new('Pipeline Execution', '@drcatherinepope'), CodeOwnerRule.new('Pipeline Insights', '@marcel.amirault'), CodeOwnerRule.new('Portfolio Management', '@msedlakjakubowski'), CodeOwnerRule.new('Product Analytics', '@lciutacu'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 856ac295614..17bfd0f8592 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10145,9 +10145,6 @@ msgstr "" msgid "Commits|An error occurred while fetching merge requests data." msgstr "" -msgid "Commits|History" -msgstr "" - msgid "Commits|No related merge requests found" msgstr "" @@ -14289,9 +14286,6 @@ msgstr "" msgid "Developer" msgstr "" -msgid "Development" -msgstr "" - msgid "Devices (optional)" msgstr "" @@ -21074,9 +21068,6 @@ msgstr "" msgid "If enabled, only protected branches will be mirrored." msgstr "" -msgid "If no options are selected, only administrators can register runners." -msgstr "" - msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation." msgstr "" @@ -36477,6 +36468,9 @@ msgstr "" msgid "Runners|Idle" msgstr "" +msgid "Runners|If both settings are disabled, new runners cannot be registered." +msgstr "" + msgid "Runners|Install a runner" msgstr "" @@ -44856,6 +44850,12 @@ msgstr "" msgid "Unable to fetch groups. Reload the page to try again." msgstr "" +msgid "Unable to fetch project. Reload the page to try again." +msgstr "" + +msgid "Unable to fetch projects. Reload the page to try again." +msgstr "" + msgid "Unable to fetch upstream and downstream pipelines." msgstr "" @@ -45462,6 +45462,9 @@ msgstr "" msgid "UsageQuota|Usage by project" msgstr "" +msgid "UsageQuota|Usage of group resources across the projects in the %{namespaceName} group" +msgstr "" + msgid "UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group" msgstr "" diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline index aef2447e800..c612a700f90 100755 --- a/scripts/generate-e2e-pipeline +++ b/scripts/generate-e2e-pipeline @@ -22,16 +22,19 @@ fi qa_cache_key="qa-e2e-ruby-${RUBY_VERSION}-$(md5sum qa/Gemfile.lock | awk '{ print $1 }')" variables=$(cat < { it('allows registering a U2F device', () => { const setupButton = container.find('#js-setup-token-2fa-device'); - expect(setupButton.text()).toBe('Set up new device'); + expect(trimText(setupButton.text())).toBe('Set up new device'); setupButton.trigger('click'); const inProgressMessage = container.children('p'); diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js index 95cb993fc70..773481346fc 100644 --- a/spec/frontend/authentication/webauthn/register_spec.js +++ b/spec/frontend/authentication/webauthn/register_spec.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { trimText } from 'helpers/text_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import waitForPromises from 'helpers/wait_for_promises'; import WebAuthnRegister from '~/authentication/webauthn/register'; @@ -52,7 +53,7 @@ describe('WebAuthnRegister', () => { const findRetryButton = () => container.find('#js-token-2fa-try-again'); it('shows setup button', () => { - expect(findSetupButton().text()).toBe('Set up new device'); + expect(trimText(findSetupButton().text())).toBe('Set up new device'); }); describe('when unsupported', () => { diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap index 3f812d3cf4e..50cbef21fd5 100644 --- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap +++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap @@ -1,9 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Contributors charts should render charts when loading completed and there is chart data 1`] = ` +exports[`Contributors charts should render charts and a RefSelector when loading completed and there is chart data 1`] = `
+ +

- 2 commits (jawnnypoo@gmail.com) - + 2 commits (jawnnypoo@gmail.com) +

diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js index 2f0b5719326..c14bbc2f9f9 100644 --- a/spec/frontend/contributors/component/contributors_spec.js +++ b/spec/frontend/contributors/component/contributors_spec.js @@ -1,21 +1,29 @@ -import { mount } from '@vue/test-utils'; -import { GlLoadingIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import ContributorsCharts from '~/contributors/components/contributors.vue'; import { createStore } from '~/contributors/stores'; import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); let wrapper; let mock; let store; const Component = Vue.extend(ContributorsCharts); -const endpoint = 'contributors'; +const endpoint = 'contributors/-/graphs'; const branch = 'main'; const chartData = [ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' }, { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' }, ]; +const projectId = '23'; +const commitsPath = 'some/path'; function factory() { mock = new MockAdapter(axios); @@ -23,19 +31,27 @@ function factory() { mock.onGet().reply(200, chartData); store = createStore(); - wrapper = mount(Component, { + wrapper = mountExtended(Component, { propsData: { endpoint, branch, + projectId, + commitsPath, }, stubs: { GlLoadingIcon: true, GlAreaChart: true, + RefSelector: true, }, store, }); } +const findLoadingIcon = () => wrapper.findByTestId('loading-app-icon'); +const findRefSelector = () => wrapper.findComponent(RefSelector); +const findHistoryButton = () => wrapper.findByTestId('history-button'); +const findContributorsCharts = () => wrapper.findByTestId('contributors-charts'); + describe('Contributors charts', () => { beforeEach(() => { factory(); @@ -53,15 +69,46 @@ describe('Contributors charts', () => { it('should display loader whiled loading data', async () => { wrapper.vm.$store.state.loading = true; await nextTick(); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('should render charts when loading completed and there is chart data', async () => { + it('should render charts and a RefSelector when loading completed and there is chart data', async () => { wrapper.vm.$store.state.loading = false; wrapper.vm.$store.state.chartData = chartData; await nextTick(); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find('.contributors-charts').exists()).toBe(true); + + expect(findLoadingIcon().exists()).toBe(false); + expect(findRefSelector().exists()).toBe(true); + expect(findRefSelector().props()).toMatchObject({ + enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS], + value: branch, + projectId, + translations: { dropdownHeader: 'Switch branch/tag' }, + useSymbolicRefNames: false, + state: true, + name: '', + }); + expect(findContributorsCharts().exists()).toBe(true); expect(wrapper.element).toMatchSnapshot(); }); + + it('should have a history button with a set href attribute', async () => { + wrapper.vm.$store.state.loading = false; + wrapper.vm.$store.state.chartData = chartData; + await nextTick(); + + const historyButton = findHistoryButton(); + expect(historyButton.exists()).toBe(true); + expect(historyButton.attributes('href')).toBe(commitsPath); + }); + + it('visits a URL when clicking on a branch/tag', async () => { + wrapper.vm.$store.state.loading = false; + wrapper.vm.$store.state.chartData = chartData; + await nextTick(); + + findRefSelector().vm.$emit('input', branch); + + expect(visitUrl).toHaveBeenCalledWith(`${endpoint}/${branch}`); + }); }); diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js index bfbf3e234f4..ca9a72663d2 100644 --- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js +++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js @@ -34,6 +34,7 @@ describe('projects/settings/components/default_branch_selector', () => { projectId, refType: null, state: true, + toggleButtonClass: null, translations: { dropdownHeader: expect.any(String), searchPlaceholder: expect.any(String), diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index e12659a0eff..ec14d8bacb2 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -715,7 +715,7 @@ describe('Ref selector component', () => { describe('validation state', () => { const invalidClass = 'gl-inset-border-1-red-500!'; const isInvalidClassApplied = () => - wrapper.findComponent(GlDropdown).props('toggleClass')[invalidClass]; + wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass]; describe('valid state', () => { describe('when the state prop is not provided', () => { diff --git a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js new file mode 100644 index 00000000000..cb70ea4e72d --- /dev/null +++ b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js @@ -0,0 +1,39 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import UsageQuotasApp from '~/usage_quotas/components/usage_quotas_app.vue'; +import { USAGE_QUOTAS_TITLE } from '~/usage_quotas/constants'; +import { defaultProvide } from '../mock_data'; + +describe('UsageQuotasApp', () => { + let wrapper; + + const createComponent = ({ provide = {} } = {}) => { + wrapper = shallowMountExtended(UsageQuotasApp, { + provide: { + ...defaultProvide, + ...provide, + }, + stubs: { + GlSprintf, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle'); + + it('renders the view title', () => { + expect(wrapper.text()).toContain(USAGE_QUOTAS_TITLE); + }); + + it('renders the view subtitle', () => { + expect(findSubTitle().text()).toContain(defaultProvide.namespaceName); + }); +}); diff --git a/spec/frontend/usage_quotas/mock_data.js b/spec/frontend/usage_quotas/mock_data.js new file mode 100644 index 00000000000..a9d2a7ad1db --- /dev/null +++ b/spec/frontend/usage_quotas/mock_data.js @@ -0,0 +1,3 @@ +export const defaultProvide = { + namespaceName: 'Group 1', +}; diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js index 2c94b34971d..b9479b0d51d 100644 --- a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js +++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js @@ -6,8 +6,8 @@ import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; import GroupSelect from '~/vue_shared/components/entity_select/group_select.vue'; import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue'; import { - TOGGLE_TEXT, - HEADER_TEXT, + GROUP_TOGGLE_TEXT, + GROUP_HEADER_TEXT, FETCH_GROUPS_ERROR, FETCH_GROUP_ERROR, } from '~/vue_shared/components/entity_select/constants'; @@ -74,8 +74,8 @@ describe('GroupSelect', () => { ${'label'} | ${label} ${'inputName'} | ${inputName} ${'inputId'} | ${inputId} - ${'defaultToggleText'} | ${TOGGLE_TEXT} - ${'headerText'} | ${HEADER_TEXT} + ${'defaultToggleText'} | ${GROUP_TOGGLE_TEXT} + ${'headerText'} | ${GROUP_HEADER_TEXT} `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => { expect(findEntitySelect().props(prop)).toBe(expectedValue); }); diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js new file mode 100644 index 00000000000..ef5fa78f5cc --- /dev/null +++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js @@ -0,0 +1,152 @@ +import { GlCollapsibleListbox } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; +import ProjectSelect from '~/vue_shared/components/entity_select/project_select.vue'; +import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue'; +import { + PROJECT_TOGGLE_TEXT, + PROJECT_HEADER_TEXT, + FETCH_PROJECTS_ERROR, + FETCH_PROJECT_ERROR, +} from '~/vue_shared/components/entity_select/constants'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('ProjectSelect', () => { + let wrapper; + let mock; + + // Stubs + const GlAlert = { + template: '
', + }; + + // Props + const label = 'label'; + const inputName = 'inputName'; + const inputId = 'inputId'; + const groupId = '22'; + + // Mocks + const apiVersion = 'v4'; + const projectMock = { + name_with_namespace: 'selectedProject', + id: '1', + }; + const groupProjectEndpoint = `/api/${apiVersion}/groups/${groupId}/projects.json`; + const projectEndpoint = `/api/${apiVersion}/projects/${projectMock.id}`; + + // Finders + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findEntitySelect = () => wrapper.findComponent(EntitySelect); + const findAlert = () => wrapper.findComponent(GlAlert); + + // Helpers + const createComponent = ({ props = {} } = {}) => { + wrapper = mountExtended(ProjectSelect, { + propsData: { + label, + inputName, + inputId, + groupId, + ...props, + }, + stubs: { + GlAlert, + EntitySelect, + }, + }); + }; + const openListbox = () => findListbox().vm.$emit('shown'); + + beforeAll(() => { + gon.api_version = apiVersion; + }); + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('entity_select props', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + prop | expectedValue + ${'label'} | ${label} + ${'inputName'} | ${inputName} + ${'inputId'} | ${inputId} + ${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT} + ${'headerText'} | ${PROJECT_HEADER_TEXT} + `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => { + expect(findEntitySelect().props(prop)).toBe(expectedValue); + }); + }); + + describe('on mount', () => { + it('fetches projects when the listbox is opened', async () => { + createComponent(); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(0); + + openListbox(); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(1); + expect(mock.history.get[0].url).toBe(groupProjectEndpoint); + expect(mock.history.get[0].params).toEqual({ + include_subgroups: false, + order_by: 'similarity', + per_page: 20, + search: '', + simple: true, + with_shared: true, + }); + }); + + describe('with an initial selection', () => { + it("fetches the initially selected value's name", async () => { + mock.onGet(projectEndpoint).reply(HTTP_STATUS_OK, projectMock); + createComponent({ props: { initialSelection: projectMock.id } }); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(1); + expect(findListbox().props('toggleText')).toBe(projectMock.name_with_namespace); + }); + + it('show an error if fetching the individual project fails', async () => { + mock + .onGet(groupProjectEndpoint) + .reply(200, [{ full_name: 'notTheSelectedProject', id: '2' }]); + mock.onGet(projectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + createComponent({ props: { initialSelection: projectMock.id } }); + + expect(findAlert().exists()).toBe(false); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(FETCH_PROJECT_ERROR); + }); + }); + }); + + it('shows an error when fetching projects fails', async () => { + mock.onGet(groupProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + createComponent(); + openListbox(); + expect(findAlert().exists()).toBe(false); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(FETCH_PROJECTS_ERROR); + }); +}); diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index fcdb41eb4af..26951b0c1e7 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -40,7 +40,7 @@ RSpec.describe TodosHelper do end let_it_be(:group_todo) do - create(:todo, target: group) + create(:todo, target: group, group: group, project: nil, user: user) end let_it_be(:project_access_request_todo) do @@ -435,4 +435,21 @@ RSpec.describe TodosHelper do it { expect(result).to match("Due #{l(Date.tomorrow, format: Date::DATE_FORMATS[:medium])}") } end end + + describe '#todo_parent_path' do + context 'when todo resource parent is a group' do + subject(:result) { helper.todo_parent_path(group_todo) } + + it { expect(result).to eq(group_todo.group.name) } + end + + context 'when todo resource parent is not a group' do + it 'returns project title with namespace' do + result = helper.todo_parent_path(project_access_request_todo) + + expect(result).to include(project_access_request_todo.project.name) + expect(result).to include(project_access_request_todo.project.namespace.human_name) + end + end + end end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb index 271022e7c55..e83ee0c6b75 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb @@ -25,6 +25,14 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do freeze_time end + context 'when an unknown parent class is given' do + it 'raises error' do + stage = instance_double('Analytics::CycleAnalytics::Stage', parent: Issue.new) + + expect { described_class.new(stage: stage) }.to raise_error(/unknown parent_class: Issue/) + end + end + describe 'date range parameters' do context 'when filters by only the `from` parameter' do before do diff --git a/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb b/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb index 1eb077fe6ca..56fbaef031d 100644 --- a/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis, :delete do +RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis, :delete, feature_category: :database do # rubocop:disable Layout/LineLength include StubENV let(:model) { ActiveRecord::Base } let(:db_host) { model.connection_pool.db_config.host } @@ -55,50 +55,8 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis conn.execute("INSERT INTO #{test_table_name} (value) VALUES (2)") end - context 'with the PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION environment variable not set' do - it 'logs a warning when violating transaction semantics with writes' do - conn = model.connection - - expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:warn).with(hash_including(event: :transaction_leak)) - expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:warn).with(hash_including(event: :read_write_retry)) - - conn.transaction do - expect(conn).to be_transaction_open - - execute(conn) - - expect(conn).not_to be_transaction_open - end - - values = conn.execute("SELECT value FROM #{test_table_name}").to_a.map { |row| row['value'] } - expect(values).to contain_exactly(2) # Does not include 1 because the transaction was aborted and leaked - end - - it 'does not log a warning when no transaction is open to be leaked' do - conn = model.connection - - expect(::Gitlab::Database::LoadBalancing::Logger) - .not_to receive(:warn).with(hash_including(event: :transaction_leak)) - expect(::Gitlab::Database::LoadBalancing::Logger) - .to receive(:warn).with(hash_including(event: :read_write_retry)) - - expect(conn).not_to be_transaction_open - - execute(conn) - - expect(conn).not_to be_transaction_open - - values = conn.execute("SELECT value FROM #{test_table_name}").to_a.map { |row| row['value'] } - expect(values).to contain_exactly(1, 2) # Includes both rows because there was no transaction to roll back - end - end - - context 'with the PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION environment variable set' do - before do - stub_env('PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION' => '1') - end - - it 'raises an exception when a retry would occur during a transaction' do + context 'in a transaction' do + it 'raises an exception when a retry would occur' do expect(::Gitlab::Database::LoadBalancing::Logger) .not_to receive(:warn).with(hash_including(event: :transaction_leak)) @@ -108,8 +66,10 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis end end.to raise_error(ActiveRecord::StatementInvalid) { |e| expect(e.cause).to be_a(PG::ConnectionBad) } end + end - it 'retries when not in a transaction' do + context 'without a transaction' do + it 'retries' do expect(::Gitlab::Database::LoadBalancing::Logger) .not_to receive(:warn).with(hash_including(event: :transaction_leak)) expect(::Gitlab::Database::LoadBalancing::Logger) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index f196db709b7..7f838e0caf9 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1401,6 +1401,7 @@ RSpec.describe Notify do context 'for service desk issues' do before do + stub_feature_flags(service_desk_custom_email: false) issue.update!(external_author: 'service.desk@example.com') issue.issue_email_participants.create!(email: 'service.desk@example.com') end @@ -1411,6 +1412,7 @@ RSpec.describe Notify do it_behaves_like 'an unsubscribeable thread' it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' + it_behaves_like 'a mail with default delivery method' it 'has the correct recipient' do is_expected.to deliver_to('service.desk@example.com') @@ -1444,6 +1446,41 @@ RSpec.describe Notify do expect_sender(User.support_bot) end end + + context 'when service_desk_custom_email is active' do + before do + stub_feature_flags(service_desk_custom_email: true) + end + + it_behaves_like 'a mail with default delivery method' + + it 'uses service bot name by default' do + expect_sender(User.support_bot) + end + + context 'when custom email is enabled' do + let_it_be(:settings) do + create( + :service_desk_setting, + project: project, + custom_email_enabled: true, + custom_email: 'supersupport@example.com', + custom_email_smtp_address: 'smtp.example.com', + custom_email_smtp_port: 587, + custom_email_smtp_username: 'supersupport@example.com', + custom_email_smtp_password: 'supersecret' + ) + end + + it 'uses custom email and service bot name in "from" header' do + expect_sender(User.support_bot, sender_email: 'supersupport@example.com') + end + + it 'uses SMTP delivery method and has correct settings' do + expect_service_desk_custom_email_delivery_options(settings) + end + end + end end describe 'new note email' do @@ -1454,6 +1491,7 @@ RSpec.describe Notify do it_behaves_like 'an unsubscribeable thread' it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' + it_behaves_like 'a mail with default delivery method' it 'has the correct recipient' do is_expected.to deliver_to('service.desk@example.com') @@ -1469,6 +1507,41 @@ RSpec.describe Notify do is_expected.to have_body_text(first_note.note) end end + + context 'when service_desk_custom_email is active' do + before do + stub_feature_flags(service_desk_custom_email: true) + end + + it_behaves_like 'a mail with default delivery method' + + it 'uses author\'s name in "from" header' do + expect_sender(first_note.author) + end + + context 'when custom email is enabled' do + let_it_be(:settings) do + create( + :service_desk_setting, + project: project, + custom_email_enabled: true, + custom_email: 'supersupport@example.com', + custom_email_smtp_address: 'smtp.example.com', + custom_email_smtp_port: 587, + custom_email_smtp_username: 'supersupport@example.com', + custom_email_smtp_password: 'supersecret' + ) + end + + it 'uses custom email and author\'s name in "from" header' do + expect_sender(first_note.author, sender_email: project.service_desk_setting.custom_email) + end + + it 'uses SMTP delivery method and has correct settings' do + expect_service_desk_custom_email_delivery_options(settings) + end + end + end end end end @@ -2271,9 +2344,20 @@ RSpec.describe Notify do end end - def expect_sender(user) + def expect_sender(user, sender_email: nil) sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq("#{user.name} (@#{user.username})") - expect(sender.address).to eq(gitlab_sender) + expect(sender.address).to eq(sender_email.presence || gitlab_sender) + end + + def expect_service_desk_custom_email_delivery_options(service_desk_setting) + expect(subject.delivery_method).to be_a Mail::SMTP + expect(subject.delivery_method.settings).to include( + address: service_desk_setting.custom_email_smtp_address, + port: service_desk_setting.custom_email_smtp_port, + user_name: service_desk_setting.custom_email_smtp_username, + password: service_desk_setting.custom_email_smtp_password, + domain: service_desk_setting.custom_email.split('@').last + ) end end diff --git a/spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb b/spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb new file mode 100644 index 00000000000..5c572b49d3d --- /dev/null +++ b/spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ClearDuplicateJobsCookies, :migration, feature_category: :redis do + def with_redis(&block) + Gitlab::Redis::Queues.with(&block) + end + + it 'deletes duplicate jobs cookies' do + delete = ['resque:gitlab:duplicate:blabla:1:cookie:v2', 'resque:gitlab:duplicate:foobar:2:cookie:v2'] + keep = ['resque:gitlab:duplicate:something', 'something:cookie:v2'] + with_redis { |r| (delete + keep).each { |key| r.set(key, 'value') } } + + expect(with_redis { |r| r.exists(delete + keep) }).to eq(4) + + migrate! + + expect(with_redis { |r| r.exists(delete) }).to eq(0) + expect(with_redis { |r| r.exists(keep) }).to eq(2) + end +end diff --git a/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb b/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb index ffddaf1e1b2..a24f237fa9d 100644 --- a/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb +++ b/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do describe 'associations' do it { is_expected.to have_many(:cycle_analytics_project_stages) } + it { is_expected.to have_many(:cycle_analytics_stages) } end describe 'validations' do @@ -30,14 +31,14 @@ RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do end describe '.cleanup_if_unused' do - it 'removes the record' do + it 'removes the record if there is no project or group stages with given stage events hash' do described_class.cleanup_if_unused(stage_event_hash.id) expect(described_class.find_by_id(stage_event_hash.id)).to be_nil end - it 'does not remove the record' do - id = create(:cycle_analytics_project_stage).stage_event_hash_id + it 'does not remove the record if at least 1 group stage for the given stage events hash exists' do + id = create(:cycle_analytics_stage).stage_event_hash_id described_class.cleanup_if_unused(id) diff --git a/spec/models/analytics/cycle_analytics/stage_spec.rb b/spec/models/analytics/cycle_analytics/stage_spec.rb new file mode 100644 index 00000000000..e37edda80b1 --- /dev/null +++ b/spec/models/analytics/cycle_analytics/stage_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream_management do + describe 'uniqueness validation on name' do + subject { build(:cycle_analytics_stage) } + + it { is_expected.to validate_uniqueness_of(:name).scoped_to([:group_id, :group_value_stream_id]) } + end + + describe 'associations' do + it { is_expected.to belong_to(:namespace).required } + it { is_expected.to belong_to(:value_stream) } + end + + it_behaves_like 'value stream analytics namespace models' do + let(:factory_name) { :cycle_analytics_stage } + end + + it_behaves_like 'value stream analytics stage' do + let(:factory) { :cycle_analytics_stage } + let(:parent) { create(:group) } + let(:parent_name) { :namespace } + end + + describe '.distinct_stages_within_hierarchy' do + let_it_be(:group) { create(:group) } + let_it_be(:sub_group) { create(:group, parent: group) } + + before do + # event identifiers are the same + create(:cycle_analytics_stage, name: 'Stage A1', namespace: group, + start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + create(:cycle_analytics_stage, name: 'Stage A2', namespace: sub_group, + start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + create(:cycle_analytics_stage, name: 'Stage A3', namespace: sub_group, + start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + + create(:cycle_analytics_stage, + name: 'Stage B1', + namespace: group, + start_event_identifier: :merge_request_last_build_started, + end_event_identifier: :merge_request_last_build_finished) + end + + it 'returns distinct stages by the event identifiers' do + stages = described_class.distinct_stages_within_hierarchy(group).to_a + + expected_event_pairs = [ + %w[merge_request_created merge_request_merged], + %w[merge_request_last_build_started merge_request_last_build_finished] + ] + + current_event_pairs = stages.map do |stage| + [stage.start_event_identifier, stage.end_event_identifier] + end + + expect(current_event_pairs).to eq(expected_event_pairs) + end + end + + describe 'events tracking' do + let(:category) { described_class.to_s } + let(:label) { described_class.table_name } + let(:namespace) { create(:group) } + let(:action) { "database_event_#{property}" } + let(:value_stream) { create(:cycle_analytics_value_stream) } + let(:feature_flag_name) { :product_intelligence_database_event_tracking } + let(:stage) { described_class.create!(stage_params) } + let(:stage_params) do + { + namespace: namespace, + name: 'st1', + start_event_identifier: :merge_request_created, + end_event_identifier: :merge_request_merged, + group_value_stream_id: value_stream.id + } + end + + let(:record_tracked_attributes) do + { + "id" => stage.id, + "created_at" => stage.created_at, + "updated_at" => stage.updated_at, + "relative_position" => stage.relative_position, + "start_event_identifier" => stage.start_event_identifier, + "end_event_identifier" => stage.end_event_identifier, + "group_id" => stage.group_id, + "start_event_label_id" => stage.start_event_label_id, + "end_event_label_id" => stage.end_event_label_id, + "hidden" => stage.hidden, + "custom" => stage.custom, + "name" => stage.name, + "group_value_stream_id" => stage.group_value_stream_id + } + end + + describe '#create' do + it_behaves_like 'Snowplow event tracking' do + let(:property) { 'create' } + let(:extra) { record_tracked_attributes } + + subject(:new_group_stage) { stage } + end + end + + describe '#update', :freeze_time do + it_behaves_like 'Snowplow event tracking' do + subject(:create_group_stage) { stage.update!(name: 'st 2') } + + let(:extra) { record_tracked_attributes.merge('name' => 'st 2') } + let(:property) { 'update' } + end + end + + describe '#destroy' do + it_behaves_like 'Snowplow event tracking' do + subject(:delete_stage_group) { stage.destroy! } + + let(:extra) { record_tracked_attributes } + let(:property) { 'destroy' } + end + end + end +end diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb new file mode 100644 index 00000000000..e32fbef30ae --- /dev/null +++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_category: :value_stream_management do + describe 'associations' do + it { is_expected.to belong_to(:namespace).required } + it { is_expected.to have_many(:stages) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_length_of(:name).is_at_most(100) } + + it 'validates uniqueness of name' do + group = create(:group) + create(:cycle_analytics_value_stream, name: 'test', namespace: group) + + value_stream = build(:cycle_analytics_value_stream, name: 'test', namespace: group) + + expect(value_stream).to be_invalid + expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')]) + end + + it_behaves_like 'value stream analytics namespace models' do + let(:factory_name) { :cycle_analytics_value_stream } + end + end + + describe 'ordering of stages' do + let(:group) { create(:group) } + let(:value_stream) do + create(:cycle_analytics_value_stream, namespace: group, stages: [ + create(:cycle_analytics_stage, namespace: group, name: "stage 1", relative_position: 5), + create(:cycle_analytics_stage, namespace: group, name: "stage 2", relative_position: nil), + create(:cycle_analytics_stage, namespace: group, name: "stage 3", relative_position: 1) + ]) + end + + before do + value_stream.reload + end + + describe 'stages attribute' do + it 'sorts stages by relative position' do + names = value_stream.stages.map(&:name) + expect(names).to eq(['stage 3', 'stage 1', 'stage 2']) + end + end + end + + describe '#custom?' do + context 'when value stream is not persisted' do + subject(:value_stream) { build(:cycle_analytics_value_stream, name: value_stream_name) } + + context 'when the name of the value stream is default' do + let(:value_stream_name) { Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME } + + it { is_expected.not_to be_custom } + end + + context 'when the name of the value stream is not default' do + let(:value_stream_name) { 'value_stream_1' } + + it { is_expected.to be_custom } + end + end + + context 'when value stream is persisted' do + subject(:value_stream) { create(:cycle_analytics_value_stream, name: 'value_stream_1') } + + it { is_expected.to be_custom } + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index e05eeb7772b..63b8ebcecab 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -36,6 +36,8 @@ RSpec.describe Namespace, feature_category: :subgroups do it { is_expected.to have_many(:work_items) } it { is_expected.to have_many :achievements } it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') } + it { is_expected.to have_many(:cycle_analytics_stages) } + it { is_expected.to have_many(:value_streams) } it do is_expected.to have_one(:ci_cd_settings).class_name('NamespaceCiCdSetting').inverse_of(:namespace).autosave(true) diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb index c1ec35732b8..32c36375a3d 100644 --- a/spec/models/service_desk_setting_spec.rb +++ b/spec/models/service_desk_setting_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ServiceDeskSetting do +RSpec.describe ServiceDeskSetting, feature_category: :service_desk do describe 'validations' do subject(:service_desk_setting) { create(:service_desk_setting) } @@ -12,6 +12,48 @@ RSpec.describe ServiceDeskSetting do it { is_expected.to allow_value('abc123_').for(:project_key) } it { is_expected.not_to allow_value('abc 12').for(:project_key).with_message("can contain only lowercase letters, digits, and '_'.") } it { is_expected.not_to allow_value('Big val').for(:project_key) } + it { is_expected.to validate_length_of(:custom_email).is_at_most(255) } + it { is_expected.to validate_length_of(:custom_email_smtp_address).is_at_most(255) } + it { is_expected.to validate_length_of(:custom_email_smtp_username).is_at_most(255) } + + describe '#custom_email_enabled' do + it { expect(subject.custom_email_enabled).to be_falsey } + it { expect(described_class.new(custom_email_enabled: true).custom_email_enabled).to be_truthy } + end + + context 'when custom_email_enabled is true' do + before do + subject.custom_email_enabled = true + end + + it { is_expected.to validate_presence_of(:custom_email) } + it { is_expected.to validate_uniqueness_of(:custom_email).allow_nil } + it { is_expected.to allow_value('support@example.com').for(:custom_email) } + it { is_expected.to allow_value('support@xn--brggen-4ya.de').for(:custom_email) } # converted domain name with umlaut + it { is_expected.to allow_value('support1@shop.example.com').for(:custom_email) } + it { is_expected.to allow_value('support-shop_with.crazy-address@shop.example.com').for(:custom_email) } + it { is_expected.not_to allow_value('support@example@example.com').for(:custom_email) } + it { is_expected.not_to allow_value('support.example.com').for(:custom_email) } + it { is_expected.not_to allow_value('example.com').for(:custom_email) } + it { is_expected.not_to allow_value('example').for(:custom_email) } + it { is_expected.not_to allow_value('" "@example.org').for(:custom_email) } + it { is_expected.not_to allow_value('support+12@example.com').for(:custom_email) } + it { is_expected.not_to allow_value('user@[IPv6:2001:db8::1]').for(:custom_email) } + it { is_expected.not_to allow_value('">"@example.org').for(:custom_email) } + it { is_expected.not_to allow_value('file://example').for(:custom_email) } + it { is_expected.not_to allow_value('no email at all').for(:custom_email) } + + it { is_expected.to validate_presence_of(:custom_email_smtp_username) } + + it { is_expected.to validate_presence_of(:custom_email_smtp_port) } + it { is_expected.to validate_numericality_of(:custom_email_smtp_port).only_integer.is_greater_than(0) } + + it { is_expected.to validate_presence_of(:custom_email_smtp_address) } + it { is_expected.to allow_value('smtp.gmail.com').for(:custom_email_smtp_address) } + it { is_expected.not_to allow_value('https://example.com').for(:custom_email_smtp_address) } + it { is_expected.not_to allow_value('file://example').for(:custom_email_smtp_address) } + it { is_expected.not_to allow_value('/example').for(:custom_email_smtp_address) } + end describe '.valid_issue_template' do let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) } @@ -67,6 +109,27 @@ RSpec.describe ServiceDeskSetting do end end + describe 'encrypted password' do + let_it_be(:settings) do + create( + :service_desk_setting, + custom_email_enabled: true, + custom_email: 'supersupport@example.com', + custom_email_smtp_address: 'smtp.example.com', + custom_email_smtp_port: 587, + custom_email_smtp_username: 'supersupport@example.com', + custom_email_smtp_password: 'supersecret' + ) + end + + it 'saves and retrieves the encrypted custom email smtp password and iv correctly' do + expect(settings.encrypted_custom_email_smtp_password).not_to be_nil + expect(settings.encrypted_custom_email_smtp_password_iv).not_to be_nil + + expect(settings.custom_email_smtp_password).to eq('supersecret') + end + end + describe 'associations' do it { is_expected.to belong_to(:project) } end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index cc399d25429..df2c9c1a23e 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -168,5 +168,13 @@ build_service_desk_setting: # service_desk_setting - issue_template_key - file_template_project_id - outgoing_name + - custom_email_enabled + - custom_email + - custom_email_smtp_address + - custom_email_smtp_port + - custom_email_smtp_username + - encrypted_custom_email_smtp_password + - encrypted_custom_email_smtp_password_iv + - custom_email_smtp_password remapped_attributes: project_key: service_desk_address diff --git a/spec/requests/groups/usage_quotas_controller_spec.rb b/spec/requests/groups/usage_quotas_controller_spec.rb index 90fd08063f3..a329398aab3 100644 --- a/spec/requests/groups/usage_quotas_controller_spec.rb +++ b/spec/requests/groups/usage_quotas_controller_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Groups::UsageQuotasController, :with_license, feature_category: : request expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to match(/Placeholder for usage quotas Vue app/) + expect(response.body).to match(/js-usage-quotas-view/) end it 'renders 404 page if subgroup' do diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb index 24f0123ed3b..7bfae0cd9fc 100644 --- a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb +++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb @@ -5,11 +5,14 @@ require 'spec_helper' RSpec.describe Analytics::CycleAnalytics::Stages::ListService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let_it_be(:project_namespace) { project.project_namespace.reload } - let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) } + let(:value_stream) { Analytics::CycleAnalytics::ValueStream.build_default_value_stream(project_namespace) } let(:stages) { subject.payload[:stages] } - subject { described_class.new(parent: project, current_user: user).execute } + subject do + described_class.new(parent: project_namespace, current_user: user, params: { value_stream: value_stream }).execute + end before_all do project.add_reporter(user) diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index b0cbf0b0d65..2e182fb399d 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -280,6 +280,12 @@ RSpec.shared_examples 'no email is sent' do end end +RSpec.shared_examples 'a mail with default delivery method' do + it 'uses mailer default delivery method' do + expect(subject.delivery_method).to be_a ActionMailer::Base.delivery_methods[described_class.delivery_method] + end +end + RSpec.shared_examples 'does not render a manage notifications link' do it do aggregate_failures do diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb index 422923827a8..052e86e7f32 100644 --- a/spec/tooling/danger/specs_spec.rb +++ b/spec/tooling/danger/specs_spec.rb @@ -19,14 +19,16 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do let(:file_lines) do [ " describe 'foo' do", - " expect(foo).to match(['bar'])", + " expect(foo).to match(['bar', 'baz'])", " end", - " expect(foo).to match(['bar'])", # same line as line 1 above, we expect two different suggestions + " expect(foo).to match(['bar', 'baz'])", # same line as line 1 above, we expect two different suggestions " ", - " expect(foo).to match ['bar']", - " expect(foo).to eq(['bar'])", - " expect(foo).to eq ['bar']", - " expect(foo).to(match(['bar']))", + " expect(foo).to match ['bar', 'baz']", + " expect(foo).to eq(['bar', 'baz'])", + " expect(foo).to eq ['bar', 'baz']", + " expect(foo).to(match(['bar', 'baz']))", + " expect(foo).to(eq(['bar', 'baz']))", + " expect(foo).to(eq([bar, baz]))", " expect(foo).to(eq(['bar']))", " foo.eq(['bar'])" ] @@ -35,28 +37,30 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do let(:matching_lines) do [ "+ expect(foo).to match(['should not error'])", - "+ expect(foo).to match(['bar'])", - "+ expect(foo).to match(['bar'])", - "+ expect(foo).to match ['bar']", - "+ expect(foo).to eq(['bar'])", - "+ expect(foo).to eq ['bar']", - "+ expect(foo).to(match(['bar']))", - "+ expect(foo).to(eq(['bar']))" + "+ expect(foo).to match(['bar', 'baz'])", + "+ expect(foo).to match(['bar', 'baz'])", + "+ expect(foo).to match ['bar', 'baz']", + "+ expect(foo).to eq(['bar', 'baz'])", + "+ expect(foo).to eq ['bar', 'baz']", + "+ expect(foo).to(match(['bar', 'baz']))", + "+ expect(foo).to(eq(['bar', 'baz']))", + "+ expect(foo).to(eq([bar, baz]))" ] end let(:changed_lines) do [ - " expect(foo).to match(['bar'])", - " expect(foo).to match(['bar'])", - " expect(foo).to match ['bar']", - " expect(foo).to eq(['bar'])", - " expect(foo).to eq ['bar']", - "- expect(foo).to match(['bar'])", - "- expect(foo).to match(['bar'])", - "- expect(foo).to match ['bar']", - "- expect(foo).to eq(['bar'])", - "- expect(foo).to eq ['bar']", + " expect(foo).to match(['bar', 'baz'])", + " expect(foo).to match(['bar', 'baz'])", + " expect(foo).to match ['bar', 'baz']", + " expect(foo).to eq(['bar', 'baz'])", + " expect(foo).to eq ['bar', 'baz']", + "- expect(foo).to match(['bar', 'baz'])", + "- expect(foo).to match(['bar', 'baz'])", + "- expect(foo).to match ['bar', 'baz']", + "- expect(foo).to eq(['bar', 'baz'])", + "- expect(foo).to eq ['bar', 'baz']", + "- expect(foo).to eq [bar, foo]", "+ expect(foo).to eq([])" ] + matching_lines end @@ -118,13 +122,14 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do it 'adds suggestions at the correct lines' do [ - { suggested_line: " expect(foo).to match_array(['bar'])", number: 2 }, - { suggested_line: " expect(foo).to match_array(['bar'])", number: 4 }, - { suggested_line: " expect(foo).to match_array ['bar']", number: 6 }, - { suggested_line: " expect(foo).to match_array(['bar'])", number: 7 }, - { suggested_line: " expect(foo).to match_array ['bar']", number: 8 }, - { suggested_line: " expect(foo).to(match_array(['bar']))", number: 9 }, - { suggested_line: " expect(foo).to(match_array(['bar']))", number: 10 } + { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 2 }, + { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 4 }, + { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 6 }, + { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 7 }, + { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 8 }, + { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 9 }, + { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 10 }, + { suggested_line: " expect(foo).to(match_array([bar, baz]))", number: 11 } ].each do |test_case| comment = format(template, suggested_line: test_case[:suggested_line]) expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number]) diff --git a/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb b/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb index e4ebdd706d4..5ef9399487f 100644 --- a/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb +++ b/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'admin/application_settings/ci_cd.html.haml' do render expect(rendered).to have_content("Runner registration") - expect(rendered).to have_content("If no options are selected, only administrators can register runners.") + expect(rendered).to have_content(s_("Runners|If both settings are disabled, new runners cannot be registered.")) end end end diff --git a/tooling/danger/specs.rb b/tooling/danger/specs.rb index 6c0459a4344..04f3c9d4c9a 100644 --- a/tooling/danger/specs.rb +++ b/tooling/danger/specs.rb @@ -5,7 +5,7 @@ module Tooling module Specs SPEC_FILES_REGEX = 'spec/' EE_PREFIX = 'ee/' - MATCH_WITH_ARRAY_REGEX = /(?to\(?\s*)(?match|eq)(?[( ]?\[[^\]]+)/.freeze + MATCH_WITH_ARRAY_REGEX = /(?to\(?\s*)(?match|eq)(?[( ]?\[(?=.*,)[^\]]+)/.freeze MATCH_WITH_ARRAY_REPLACEMENT = '\kmatch_array\k' PROJECT_FACTORIES = %w[