From 5e450e9022861d03048cc733c20585ad0891f5aa Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 23 Feb 2021 21:10:44 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- CHANGELOG.md | 24 +++ GITALY_SERVER_VERSION | 2 +- .../components/alert_mapping_builder.vue | 43 +++--- .../components/alerts_integrations_list.vue | 13 +- .../components/alerts_settings_form.vue | 140 ++++++++++-------- .../components/alerts_settings_wrapper.vue | 109 +++++++++----- .../components/mocks/parsedMapping.json | 95 ------------ .../javascripts/alerts_settings/constants.js | 15 +- .../javascripts/alerts_settings/graphql.js | 16 +- .../http_integration_item.fragment.graphql | 7 + ..._integration_payload_data.fragment.graphql | 14 ++ .../create_http_integration.mutation.graphql | 4 +- .../destroy_http_integration.mutation.graphql | 4 +- .../reset_http_token.mutation.graphql | 4 +- ..._current_http_integration.mutation.graphql | 25 ++++ ...t_prometheus_integration.mutation.graphql} | 4 +- .../update_http_integration.mutation.graphql | 22 ++- .../get_http_integrations.query.graphql | 13 ++ .../parse_sample_payload.query.graphql | 9 ++ .../alerts_settings/utils/cache_updates.js | 34 +++++ .../utils/mapping_transformations.js | 28 ++-- .../shared/components/package_list_row.vue | 2 +- .../shared/wikis/components/wiki_alert.vue | 31 ++++ .../javascripts/pages/shared/wikis/index.js | 28 +++- .../pipelines_list/pipelines_table_row.vue | 8 +- .../components/pipelines_list/stage.vue | 14 +- app/assets/javascripts/pipelines/constants.js | 1 - .../explorer/components/delete_button.vue | 1 + app/controllers/concerns/wiki_actions.rb | 3 +- app/helpers/wiki_helper.rb | 18 --- app/models/iteration.rb | 54 +++---- app/models/namespace.rb | 9 ++ app/models/wiki_page.rb | 5 +- app/services/groups/create_service.rb | 2 +- app/views/shared/wikis/edit.html.haml | 3 +- .../294443-move-wiki-helper-error-to-vue.yml | 5 + ...-ui-make-action-buttons-in-list-items-.yml | 5 + .../unreleased/322198-fix-metrics-tab.yml | 5 - ...essing_migrate_delayed_project_removal.yml | 5 + ...-participants-migration-version-number.yml | 6 - .../fix-keep-artifacts-project-setting.yml | 5 - ...ssue_227753-remove_backup_labels_table.yml | 5 + .../iterations-dates-validations.yml | 5 + .../unreleased/iterations-in-the-past.yml | 5 + .../unreleased/lm-fix-authorization-lint.yml | 5 - changelogs/unreleased/psi-board-scroll.yml | 5 - changelogs/unreleased/puma_sigint.yml | 5 - .../unreleased/reset_template_cache_key.yml | 5 - changelogs/unreleased/revert-fb45e290.yml | 5 - .../sh-fix-export-csv-service-nplusone.yml | 5 - .../migrate_delayed_project_removal.yml | 8 + ...terations_cadence_date_range_constraint.rb | 30 ++++ ...e_iteration_group_date_range_constraint.rb | 30 ++++ ...d_project_removal_to_namespace_settings.rb | 9 ++ ...x_to_namespaces_delayed_project_removal.rb | 18 +++ ...ts_start_date_not_null_check_constraint.rb | 17 +++ ...ints_due_date_not_null_check_constraint.rb | 17 +++ ...l_from_namespaces_to_namespace_settings.rb | 28 ++++ ...85538_remove_backup_labels_foreign_keys.rb | 21 +++ ...210222192144_remove_backup_labels_table.rb | 36 +++++ db/schema_migrations/20210127152613 | 1 + db/schema_migrations/20210127202613 | 1 + db/schema_migrations/20210214201118 | 1 + db/schema_migrations/20210214205155 | 1 + db/schema_migrations/20210215095328 | 1 + db/schema_migrations/20210218144056 | 1 + db/schema_migrations/20210218144656 | 1 + db/schema_migrations/20210222185538 | 1 + db/schema_migrations/20210222192144 | 1 + db/structure.sql | 49 ++---- doc/api/merge_trains.md | 2 +- doc/ci/environments/index.md | 119 +++++---------- doc/ci/jobs/index.md | 2 +- doc/ci/yaml/gitlab_ci_yaml.md | 2 +- doc/topics/autodevops/customize.md | 2 +- doc/topics/autodevops/quick_start_guide.md | 2 +- doc/user/application_security/dast/index.md | 2 +- doc/user/group/custom_project_templates.md | 4 +- doc/user/group/saml_sso/index.md | 5 + locale/gitlab.pot | 10 +- qa/qa/flow/project.rb | 10 -- spec/factories/groups.rb | 2 +- .../components/alert_mapping_builder_spec.js | 28 ++-- .../components/alerts_settings_form_spec.js | 96 ++++++++---- .../alerts_settings_wrapper_spec.js | 75 +++++++++- .../components/mocks/apollo_mock.js | 17 ++- .../{alertFields.json => alert_fields.json} | 0 .../alerts_settings/mocks/parsed_mapping.json | 122 +++++++++++++++ .../utils/mapping_transformations_spec.js | 34 ++--- .../package_list_row_spec.js.snap | 2 +- .../components/package_list_row_spec.js | 22 ++- .../pages/shared/wikis/wiki_alert_spec.js | 40 +++++ spec/frontend/pipelines/stage_spec.js | 36 ++--- .../explorer/components/delete_button_spec.js | 1 + .../set_default_iteration_cadences_spec.rb | 5 +- ...m_namespaces_to_namespace_settings_spec.rb | 30 ++++ ...ule_set_default_iteration_cadences_spec.rb | 14 +- spec/models/iteration_spec.rb | 54 ++++--- spec/models/namespace_spec.rb | 24 ++- .../graphql/mutation_shared_examples.rb | 2 +- .../concerns/timebox_shared_examples.rb | 4 +- 101 files changed, 1260 insertions(+), 660 deletions(-) delete mode 100644 app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json create mode 100644 app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_item.fragment.graphql create mode 100644 app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql create mode 100644 app/assets/javascripts/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql rename app/assets/javascripts/alerts_settings/graphql/mutations/{update_current_intergration.mutation.graphql => update_current_prometheus_integration.mutation.graphql} (72%) create mode 100644 app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql create mode 100644 app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql create mode 100644 app/assets/javascripts/pages/shared/wikis/components/wiki_alert.vue create mode 100644 changelogs/unreleased/294443-move-wiki-helper-error-to-vue.yml create mode 100644 changelogs/unreleased/320746-update-the-package-list-item-ui-make-action-buttons-in-list-items-.yml delete mode 100644 changelogs/unreleased/322198-fix-metrics-tab.yml create mode 100644 changelogs/unreleased/dblessing_migrate_delayed_project_removal.yml delete mode 100644 changelogs/unreleased/fix-email-participants-migration-version-number.yml delete mode 100644 changelogs/unreleased/fix-keep-artifacts-project-setting.yml create mode 100644 changelogs/unreleased/issue_227753-remove_backup_labels_table.yml create mode 100644 changelogs/unreleased/iterations-dates-validations.yml create mode 100644 changelogs/unreleased/iterations-in-the-past.yml delete mode 100644 changelogs/unreleased/lm-fix-authorization-lint.yml delete mode 100644 changelogs/unreleased/psi-board-scroll.yml delete mode 100644 changelogs/unreleased/puma_sigint.yml delete mode 100644 changelogs/unreleased/reset_template_cache_key.yml delete mode 100644 changelogs/unreleased/revert-fb45e290.yml delete mode 100644 changelogs/unreleased/sh-fix-export-csv-service-nplusone.yml create mode 100644 config/feature_flags/development/migrate_delayed_project_removal.yml create mode 100644 db/migrate/20210127152613_add_iterations_cadence_date_range_constraint.rb create mode 100644 db/migrate/20210127202613_remove_iteration_group_date_range_constraint.rb create mode 100644 db/migrate/20210214201118_add_delayed_project_removal_to_namespace_settings.rb create mode 100644 db/migrate/20210214205155_add_index_to_namespaces_delayed_project_removal.rb create mode 100644 db/migrate/20210218144056_add_sprints_start_date_not_null_check_constraint.rb create mode 100644 db/migrate/20210218144656_add_sprints_due_date_not_null_check_constraint.rb create mode 100644 db/post_migrate/20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb create mode 100644 db/post_migrate/20210222185538_remove_backup_labels_foreign_keys.rb create mode 100644 db/post_migrate/20210222192144_remove_backup_labels_table.rb create mode 100644 db/schema_migrations/20210127152613 create mode 100644 db/schema_migrations/20210127202613 create mode 100644 db/schema_migrations/20210214201118 create mode 100644 db/schema_migrations/20210214205155 create mode 100644 db/schema_migrations/20210215095328 create mode 100644 db/schema_migrations/20210218144056 create mode 100644 db/schema_migrations/20210218144656 create mode 100644 db/schema_migrations/20210222185538 create mode 100644 db/schema_migrations/20210222192144 rename spec/frontend/alerts_settings/mocks/{alertFields.json => alert_fields.json} (100%) create mode 100644 spec/frontend/alerts_settings/mocks/parsed_mapping.json create mode 100644 spec/frontend/pages/shared/wikis/wiki_alert_spec.js create mode 100644 spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 98636c47da4..1bd602f975d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 13.9.1 (2021-02-23) + +### Fixed (6 changes, 1 of them is from the community) + +- Send SIGINT instead of SIGQUIT to puma. !54446 (Jörg Behrmann @behrmann) +- Reset description template names cache key to reload an updated templates structure. !54614 +- Restore missing horizontal scrollbar on issue boards. !54634 +- Fix keep latest artifacts checkbox being always disabled. !54669 +- Fix Metric tab not showing up on operations page. !54736 +- Fix S3 object storage failing when endpoint is not specified. !54868 + +### Changed (1 change) + +- Updates authorization for linting endpoint. !54492 + +### Performance (1 change) + +- Fix N+1 SQL regression in exporting issues to CSV. !54287 + +### Other (1 change) + +- Fix creating the idx_on_issues_where_service_desk_reply_to_is_not_null index before the post migration. !54346 + + ## 13.9.0 (2021-02-22) ### Security (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 21c95eb8520..28bbaf545da 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -3f702fcbdb3481eade8c18815a61f97d01886cef +40d46031c0a188ece3ed2574559321baf316a48c diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue index 1135562834a..07b2e59671e 100644 --- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -7,15 +7,12 @@ import { GlSearchBoxByType, GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEqual } from 'lodash'; import Vue from 'vue'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { s__, __ } from '~/locale'; -import { - getMappingData, - getPayloadFields, - transformForSave, -} from '../utils/mapping_transformations'; +import { mappingFields } from '../constants'; +import { getMappingData, transformForSave } from '../utils/mapping_transformations'; export const i18n = { columns: { @@ -33,6 +30,7 @@ export const i18n = { export default { i18n, + mappingFields, components: { GlIcon, GlFormInput, @@ -73,18 +71,15 @@ export default { }; }, computed: { - payloadFields() { - return getPayloadFields(this.parsedPayload); - }, mappingData() { - return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping); + return getMappingData(this.gitlabFields, this.parsedPayload, this.savedMapping); }, hasFallbackColumn() { return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks)); }, }, methods: { - setMapping(gitlabKey, mappingKey, valueKey) { + setMapping(gitlabKey, mappingKey, valueKey = mappingFields.mapping) { const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey); const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } }; Vue.set(this.gitlabFields, fieldIndex, updatedField); @@ -100,11 +95,11 @@ export default { return fields.filter((field) => field.label.toLowerCase().includes(search)); }, isSelected(fieldValue, mapping) { - return fieldValue === mapping; + return isEqual(fieldValue, mapping); }, - selectedValue(name) { + selectedValue(mapping) { return ( - this.payloadFields.find((item) => item.name === name)?.label || + this.parsedPayload.find((item) => isEqual(item.path, mapping))?.label || this.$options.i18n.makeSelection ); }, @@ -150,7 +145,7 @@ export default { :key="gitlabField.name" class="gl-display-table-row" > -
+
-
+
{{ mappingField.label }} @@ -188,7 +183,7 @@ export default {
-
+
{{ mappingField.label }} diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index 6cfb4601192..3b8febfbb29 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -120,14 +120,17 @@ export default { const { category, action } = trackAlertIntegrationsViewsOptions; Tracking.event(category, action); }, - setIntegrationToDelete({ name, id }) { - this.integrationToDelete.id = id; - this.integrationToDelete.name = name; + setIntegrationToDelete(integration) { + this.integrationToDelete = integration; }, deleteIntegration() { - this.$emit('delete-integration', { id: this.integrationToDelete.id }); + const { id, type } = this.integrationToDelete; + this.$emit('delete-integration', { id, type }); this.integrationToDelete = { ...integrationToDeleteDefault }; }, + editIntegration({ id, type }) { + this.$emit('edit-integration', { id, type }); + }, }, }; @@ -169,7 +172,7 @@ export default { diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue index b597b389d6e..9b82693ecb4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue @@ -15,7 +15,6 @@ import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/u import { deprecatedCreateFlash as Flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; -import { PIPELINES_TABLE } from '../../constants'; import eventHub from '../../event_hub'; import JobItem from '../graph/job_item.vue'; @@ -39,11 +38,6 @@ export default { required: false, default: false, }, - type: { - type: String, - required: false, - default: '', - }, }, data() { return { @@ -90,13 +84,11 @@ export default { return this.$el.classList.contains('show'); }, pipelineActionRequestComplete() { - if (this.type === PIPELINES_TABLE) { - // warn the pipelines table to update - eventHub.$emit('refreshPipelinesTable'); - return; - } // close the dropdown in MR widget this.$refs.stageGlDropdown.hide(); + + // warn the pipelines table to update + this.$emit('pipelineActionRequestComplete'); }, }, }; diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 757d285ef19..9699f57b4fc 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,7 +1,6 @@ import { s__, __ } from '~/locale'; export const CANCEL_REQUEST = 'CANCEL_REQUEST'; -export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const LAYOUT_CHANGE_DELAY = 300; export const FILTER_PIPELINES_SEARCH_DELAY = 200; export const ANY_TRIGGER_AUTHOR = 'Any'; diff --git a/app/assets/javascripts/registry/explorer/components/delete_button.vue b/app/assets/javascripts/registry/explorer/components/delete_button.vue index ee856a3e546..e4a1a1a8266 100644 --- a/app/assets/javascripts/registry/explorer/components/delete_button.vue +++ b/app/assets/javascripts/registry/explorer/components/delete_button.vue @@ -48,6 +48,7 @@ export default { :title="title" :aria-label="title" variant="danger" + category="secondary" icon="remove" @click="$emit('delete')" /> diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index 4014e4f0024..60ff0a12d0c 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -112,10 +112,11 @@ module WikiActions wiki_page_path(wiki, page) ) else + @error = response.message render 'shared/wikis/edit' end rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e - @error = e + @error = e.message render 'shared/wikis/edit' end # rubocop:enable Gitlab/ModuleWithInstanceVariables diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 786081ca815..3f82cf893a0 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -50,24 +50,6 @@ module WikiHelper end end - def wiki_page_errors(error) - return unless error - - content_tag(:div, class: 'alert alert-danger') do - case error - when WikiPage::PageChangedError - page_link = link_to s_("WikiPageConflictMessage|the page"), wiki_page_path(@wiki, @page), target: "_blank" - concat( - (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe - ) - when WikiPage::PageRenameError - s_("WikiEdit|There is already a page with the same title in that path.") - else - error.message - end - end - end - def wiki_attachment_upload_url case @wiki.container when Project diff --git a/app/models/iteration.rb b/app/models/iteration.rb index 012a062712f..ccf357a1f6b 100644 --- a/app/models/iteration.rb +++ b/app/models/iteration.rb @@ -13,6 +13,7 @@ class Iteration < ApplicationRecord }.with_indifferent_access.freeze include AtomicInternalId + include Timebox belongs_to :project belongs_to :group @@ -23,22 +24,22 @@ class Iteration < ApplicationRecord validates :start_date, presence: true validates :due_date, presence: true + validates :iterations_cadence, presence: true, unless: -> { project_id.present? } validate :dates_do_not_overlap, if: :start_or_due_dates_changed? validate :future_date, if: :start_or_due_dates_changed?, unless: :skip_future_date_validation validate :no_project, unless: :skip_project_validation validate :validate_group - before_create :set_iterations_cadence + before_validation :set_iterations_cadence, unless: -> { project_id.present? } + before_create :set_past_iteration_state scope :upcoming, -> { with_state(:upcoming) } scope :started, -> { with_state(:started) } scope :closed, -> { with_state(:closed) } scope :within_timeframe, -> (start_date, end_date) do - where('start_date IS NOT NULL OR due_date IS NOT NULL') - .where('start_date IS NULL OR start_date <= ?', end_date) - .where('due_date IS NULL OR due_date >= ?', start_date) + where('start_date <= ?', end_date).where('due_date >= ?', start_date) end scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) } @@ -106,30 +107,20 @@ class Iteration < ApplicationRecord start_date_changed? || due_date_changed? end - # ensure dates do not overlap with other Iterations in the same group/project tree + # ensure dates do not overlap with other Iterations in the same cadence tree def dates_do_not_overlap - iterations = if parent_group.present? && resource_parent.is_a?(Project) - Iteration.where(group: parent_group.self_and_ancestors).or(project.iterations) - elsif parent_group.present? - Iteration.where(group: parent_group.self_and_ancestors) - else - project.iterations - end + return unless iterations_cadence + return unless iterations_cadence.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists? - return unless iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists? - - errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations")) + # for now we only have a single default cadence within a group just to wrap the iterations into a set. + # once we introduce multiple cadences per group we need to change this message. + # related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/299312 + errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations within this group")) end - # ensure dates are in the future def future_date - if start_date_changed? - errors.add(:start_date, s_("Iteration|cannot be in the past")) if start_date < Date.current + if start_or_due_dates_changed? errors.add(:start_date, s_("Iteration|cannot be more than 500 years in the future")) if start_date > 500.years.from_now - end - - if due_date_changed? - errors.add(:due_date, s_("Iteration|cannot be in the past")) if due_date < Date.current errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now end end @@ -140,6 +131,12 @@ class Iteration < ApplicationRecord errors.add(:project_id, s_("is not allowed. We do not currently support project-level iterations")) end + def set_past_iteration_state + # if we create an iteration in the past, we set the state to closed right away, + # no need to wait for IterationsUpdateStatusWorker to do so. + self.state = :closed if due_date < Date.current + end + # TODO: this method should be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296099 def set_iterations_cadence return if iterations_cadence @@ -147,13 +144,20 @@ class Iteration < ApplicationRecord # issue to clarify project iterations: https://gitlab.com/gitlab-org/gitlab/-/issues/299864 return unless group - self.iterations_cadence = group.iterations_cadences.first || create_default_cadence + # we need this as we use the cadence to validate the dates overlap for this iteration, + # so in the case this runs before background migration we need to first set all iterations + # in this group to a cadence before we can validate the dates overlap. + default_cadence = find_or_create_default_cadence + group.iterations.where(iterations_cadence_id: nil).update_all(iterations_cadence_id: default_cadence.id) + + self.iterations_cadence = default_cadence end - def create_default_cadence + def find_or_create_default_cadence cadence_title = "#{group.name} Iterations" + start_date = self.start_date || Date.today - Iterations::Cadence.create!(group: group, title: cadence_title, start_date: start_date) + ::Iterations::Cadence.create_with(title: cadence_title, start_date: start_date).safe_find_or_create_by!(group: group) end # TODO: remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296100 diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 9bc7a6be2f0..4a5d8ae88c9 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -82,6 +82,8 @@ class Namespace < ApplicationRecord before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir + before_save :ensure_delayed_project_removal_assigned_to_namespace_settings, if: :delayed_project_removal_changed? + scope :for_user, -> { where('type IS NULL') } scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) } scope :include_route, -> { includes(:route) } @@ -408,6 +410,13 @@ class Namespace < ApplicationRecord private + def ensure_delayed_project_removal_assigned_to_namespace_settings + return if Feature.disabled?(:migrate_delayed_project_removal, default_enabled: true) + + self.namespace_settings || build_namespace_settings + namespace_settings.delayed_project_removal = delayed_project_removal + end + def all_projects_with_pages if all_projects.pages_metadata_not_migrated.exists? Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation( diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 989128987d5..3b9a7ded83e 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -205,14 +205,15 @@ class WikiPage last_commit_sha = attrs.delete(:last_commit_sha) if last_commit_sha && last_commit_sha != self.last_commit_sha - raise PageChangedError + raise PageChangedError, s_( + 'WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs.') end update_attributes(attrs) if title.present? && title_changed? && wiki.find_page(title).present? attributes[:title] = page.title - raise PageRenameError + raise PageRenameError, s_('WikiEdit|There is already a page with the same title in that path.') end save do diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 06a3b31c665..3ead2323588 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -33,7 +33,7 @@ module Groups Group.transaction do if @group.save @group.add_owner(current_user) - @group.create_namespace_settings + @group.create_namespace_settings unless @group.namespace_settings Service.create_from_active_default_integrations(@group, :group_id) OnboardingProgress.onboard(@group) end diff --git a/app/views/shared/wikis/edit.html.haml b/app/views/shared/wikis/edit.html.haml index c2b0e474c03..4bdeee3996f 100644 --- a/app/views/shared/wikis/edit.html.haml +++ b/app/views/shared/wikis/edit.html.haml @@ -1,7 +1,8 @@ - wiki_page_title @page, @page.persisted? ? _('Edit') : _('New') - add_page_specific_style 'page_bundles/wiki' -= wiki_page_errors(@error) +- if @error + #js-wiki-error{ data: { error: @error, wiki_page_path: wiki_page_path(@wiki, @page) } } .wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row = wiki_sidebar_toggle_button diff --git a/changelogs/unreleased/294443-move-wiki-helper-error-to-vue.yml b/changelogs/unreleased/294443-move-wiki-helper-error-to-vue.yml new file mode 100644 index 00000000000..d28401c2a2d --- /dev/null +++ b/changelogs/unreleased/294443-move-wiki-helper-error-to-vue.yml @@ -0,0 +1,5 @@ +--- +title: Move wiki helper alert to Vue +merge_request: 54517 +author: +type: other diff --git a/changelogs/unreleased/320746-update-the-package-list-item-ui-make-action-buttons-in-list-items-.yml b/changelogs/unreleased/320746-update-the-package-list-item-ui-make-action-buttons-in-list-items-.yml new file mode 100644 index 00000000000..795f54f8fa1 --- /dev/null +++ b/changelogs/unreleased/320746-update-the-package-list-item-ui-make-action-buttons-in-list-items-.yml @@ -0,0 +1,5 @@ +--- +title: 'Registry: make delete icon buttons secondary' +merge_request: 54545 +author: +type: changed diff --git a/changelogs/unreleased/322198-fix-metrics-tab.yml b/changelogs/unreleased/322198-fix-metrics-tab.yml deleted file mode 100644 index d7600177be7..00000000000 --- a/changelogs/unreleased/322198-fix-metrics-tab.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix Metric tab not showing up on operations page -merge_request: 54736 -author: -type: fixed diff --git a/changelogs/unreleased/dblessing_migrate_delayed_project_removal.yml b/changelogs/unreleased/dblessing_migrate_delayed_project_removal.yml new file mode 100644 index 00000000000..c1ed2293e86 --- /dev/null +++ b/changelogs/unreleased/dblessing_migrate_delayed_project_removal.yml @@ -0,0 +1,5 @@ +--- +title: Migrate namespaces delayed_project_removal to namespace_settings +merge_request: 53916 +author: +type: changed diff --git a/changelogs/unreleased/fix-email-participants-migration-version-number.yml b/changelogs/unreleased/fix-email-participants-migration-version-number.yml deleted file mode 100644 index c0333716789..00000000000 --- a/changelogs/unreleased/fix-email-participants-migration-version-number.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix creating the idx_on_issues_where_service_desk_reply_to_is_not_null index - before the post migration -merge_request: 54346 -author: -type: other diff --git a/changelogs/unreleased/fix-keep-artifacts-project-setting.yml b/changelogs/unreleased/fix-keep-artifacts-project-setting.yml deleted file mode 100644 index dfff4cf73da..00000000000 --- a/changelogs/unreleased/fix-keep-artifacts-project-setting.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix keep latest artifacts checkbox being always disabled -merge_request: 54669 -author: -type: fixed diff --git a/changelogs/unreleased/issue_227753-remove_backup_labels_table.yml b/changelogs/unreleased/issue_227753-remove_backup_labels_table.yml new file mode 100644 index 00000000000..17ebced5ccb --- /dev/null +++ b/changelogs/unreleased/issue_227753-remove_backup_labels_table.yml @@ -0,0 +1,5 @@ +--- +title: Remove backup_labels table +merge_request: 54856 +author: +type: other diff --git a/changelogs/unreleased/iterations-dates-validations.yml b/changelogs/unreleased/iterations-dates-validations.yml new file mode 100644 index 00000000000..f22ac76c72d --- /dev/null +++ b/changelogs/unreleased/iterations-dates-validations.yml @@ -0,0 +1,5 @@ +--- +title: Allow overlapping iteration dates with ancestor group iterations and restrict dates overlapping for iterations within same group +merge_request: 52403 +author: +type: changed diff --git a/changelogs/unreleased/iterations-in-the-past.yml b/changelogs/unreleased/iterations-in-the-past.yml new file mode 100644 index 00000000000..07386448d6c --- /dev/null +++ b/changelogs/unreleased/iterations-in-the-past.yml @@ -0,0 +1,5 @@ +--- +title: Allow creation of iterations in the past +merge_request: 52403 +author: +type: changed diff --git a/changelogs/unreleased/lm-fix-authorization-lint.yml b/changelogs/unreleased/lm-fix-authorization-lint.yml deleted file mode 100644 index cca59b7b43e..00000000000 --- a/changelogs/unreleased/lm-fix-authorization-lint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Updates authorization for linting endpoint -merge_request: 54492 -author: -type: changed diff --git a/changelogs/unreleased/psi-board-scroll.yml b/changelogs/unreleased/psi-board-scroll.yml deleted file mode 100644 index 3d68933b6f8..00000000000 --- a/changelogs/unreleased/psi-board-scroll.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Restore missing horizontal scrollbar on issue boards -merge_request: 54634 -author: -type: fixed diff --git a/changelogs/unreleased/puma_sigint.yml b/changelogs/unreleased/puma_sigint.yml deleted file mode 100644 index 9bc6fd856f1..00000000000 --- a/changelogs/unreleased/puma_sigint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Send SIGINT instead of SIGQUIT to puma -merge_request: 54446 -author: Jörg Behrmann @behrmann -type: fixed diff --git a/changelogs/unreleased/reset_template_cache_key.yml b/changelogs/unreleased/reset_template_cache_key.yml deleted file mode 100644 index 7ee55542bae..00000000000 --- a/changelogs/unreleased/reset_template_cache_key.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reset description template names cache key to reload an updated templates structure -merge_request: 54614 -author: -type: fixed diff --git a/changelogs/unreleased/revert-fb45e290.yml b/changelogs/unreleased/revert-fb45e290.yml deleted file mode 100644 index 79554e2a12d..00000000000 --- a/changelogs/unreleased/revert-fb45e290.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix S3 object storage failing when endpoint is not specified -merge_request: 54868 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-export-csv-service-nplusone.yml b/changelogs/unreleased/sh-fix-export-csv-service-nplusone.yml deleted file mode 100644 index f1da7ec9682..00000000000 --- a/changelogs/unreleased/sh-fix-export-csv-service-nplusone.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix N+1 SQL regression in exporting issues to CSV -merge_request: 54287 -author: -type: performance diff --git a/config/feature_flags/development/migrate_delayed_project_removal.yml b/config/feature_flags/development/migrate_delayed_project_removal.yml new file mode 100644 index 00000000000..2d4a7ef762e --- /dev/null +++ b/config/feature_flags/development/migrate_delayed_project_removal.yml @@ -0,0 +1,8 @@ +--- +name: migrate_delayed_project_removal +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53916 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300207 +milestone: '13.9' +type: development +group: group::access +default_enabled: true diff --git a/db/migrate/20210127152613_add_iterations_cadence_date_range_constraint.rb b/db/migrate/20210127152613_add_iterations_cadence_date_range_constraint.rb new file mode 100644 index 00000000000..95ecd167076 --- /dev/null +++ b/db/migrate/20210127152613_add_iterations_cadence_date_range_constraint.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class AddIterationsCadenceDateRangeConstraint < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + execute <<~SQL + ALTER TABLE sprints + ADD CONSTRAINT iteration_start_and_due_date_iterations_cadence_id_constraint + EXCLUDE USING gist + ( iterations_cadence_id WITH =, + daterange(start_date, due_date, '[]') WITH && + ) + WHERE (group_id IS NOT NULL) + SQL + end + end + + def down + with_lock_retries do + execute <<~SQL + ALTER TABLE sprints + DROP CONSTRAINT IF EXISTS iteration_start_and_due_date_iterations_cadence_id_constraint + SQL + end + end +end diff --git a/db/migrate/20210127202613_remove_iteration_group_date_range_constraint.rb b/db/migrate/20210127202613_remove_iteration_group_date_range_constraint.rb new file mode 100644 index 00000000000..e6c5eb1b411 --- /dev/null +++ b/db/migrate/20210127202613_remove_iteration_group_date_range_constraint.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class RemoveIterationGroupDateRangeConstraint < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + execute <<~SQL + ALTER TABLE sprints + DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_group_id_constraint + SQL + end + end + + def down + with_lock_retries do + execute <<~SQL + ALTER TABLE sprints + ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint + EXCLUDE USING gist + ( group_id WITH =, + daterange(start_date, due_date, '[]') WITH && + ) + WHERE (group_id IS NOT NULL) + SQL + end + end +end diff --git a/db/migrate/20210214201118_add_delayed_project_removal_to_namespace_settings.rb b/db/migrate/20210214201118_add_delayed_project_removal_to_namespace_settings.rb new file mode 100644 index 00000000000..1c6e0b0c27c --- /dev/null +++ b/db/migrate/20210214201118_add_delayed_project_removal_to_namespace_settings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddDelayedProjectRemovalToNamespaceSettings < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + add_column :namespace_settings, :delayed_project_removal, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20210214205155_add_index_to_namespaces_delayed_project_removal.rb b/db/migrate/20210214205155_add_index_to_namespaces_delayed_project_removal.rb new file mode 100644 index 00000000000..8d09a5c9269 --- /dev/null +++ b/db/migrate/20210214205155_add_index_to_namespaces_delayed_project_removal.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexToNamespacesDelayedProjectRemoval < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'tmp_idx_on_namespaces_delayed_project_removal' + + disable_ddl_transaction! + + def up + add_concurrent_index :namespaces, :id, name: INDEX_NAME, where: 'delayed_project_removal = TRUE' + end + + def down + remove_concurrent_index_by_name :namespaces, INDEX_NAME + end +end diff --git a/db/migrate/20210218144056_add_sprints_start_date_not_null_check_constraint.rb b/db/migrate/20210218144056_add_sprints_start_date_not_null_check_constraint.rb new file mode 100644 index 00000000000..243080f49b2 --- /dev/null +++ b/db/migrate/20210218144056_add_sprints_start_date_not_null_check_constraint.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddSprintsStartDateNotNullCheckConstraint < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_not_null_constraint(:sprints, :start_date, validate: false) + end + + def down + remove_not_null_constraint(:sprints, :start_date) + end +end diff --git a/db/migrate/20210218144656_add_sprints_due_date_not_null_check_constraint.rb b/db/migrate/20210218144656_add_sprints_due_date_not_null_check_constraint.rb new file mode 100644 index 00000000000..9f3ed6fd13a --- /dev/null +++ b/db/migrate/20210218144656_add_sprints_due_date_not_null_check_constraint.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddSprintsDueDateNotNullCheckConstraint < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_not_null_constraint(:sprints, :due_date, validate: false) + end + + def down + remove_not_null_constraint(:sprints, :due_date) + end +end diff --git a/db/post_migrate/20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb b/db/post_migrate/20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb new file mode 100644 index 00000000000..12e156d3b8a --- /dev/null +++ b/db/post_migrate/20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings < ActiveRecord::Migration[6.0] + DOWNTIME = false + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + + include ::EachBatch + end + + def up + Namespace.select(:id).where(delayed_project_removal: true).each_batch do |batch| + values = batch.map { |record| "(#{record.id}, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" } + + execute <<-EOF.strip_heredoc + INSERT INTO namespace_settings (namespace_id, delayed_project_removal, created_at, updated_at) + VALUES #{values.join(', ')} + ON CONFLICT (namespace_id) DO UPDATE + SET delayed_project_removal = TRUE + EOF + end + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20210222185538_remove_backup_labels_foreign_keys.rb b/db/post_migrate/20210222185538_remove_backup_labels_foreign_keys.rb new file mode 100644 index 00000000000..614ec4875d6 --- /dev/null +++ b/db/post_migrate/20210222185538_remove_backup_labels_foreign_keys.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class RemoveBackupLabelsForeignKeys < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + with_lock_retries do + remove_foreign_key_if_exists(:backup_labels, :projects) + remove_foreign_key_if_exists(:backup_labels, :namespaces) + end + end + + def down + add_concurrent_foreign_key(:backup_labels, :projects, column: :project_id, on_delete: :cascade) + add_concurrent_foreign_key(:backup_labels, :namespaces, column: :group_id, on_delete: :cascade) + end +end diff --git a/db/post_migrate/20210222192144_remove_backup_labels_table.rb b/db/post_migrate/20210222192144_remove_backup_labels_table.rb new file mode 100644 index 00000000000..1208c3c970f --- /dev/null +++ b/db/post_migrate/20210222192144_remove_backup_labels_table.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class RemoveBackupLabelsTable < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def up + drop_table :backup_labels + end + + def down + create_table :backup_labels, id: false do |t| + t.integer :id, null: false + t.string :title + t.string :color + t.integer :project_id + t.timestamps null: true # rubocop:disable Migration/Timestamps + t.boolean :template, default: false + t.string :description + t.text :description_html + t.string :type + t.integer :group_id + t.integer :cached_markdown_version + t.integer :restore_action + t.string :new_title + end + + execute 'ALTER TABLE backup_labels ADD PRIMARY KEY (id)' + + add_index :backup_labels, [:group_id, :project_id, :title], name: 'backup_labels_group_id_project_id_title_idx', unique: true + add_index :backup_labels, [:group_id, :title], where: 'project_id = NULL::integer', name: 'backup_labels_group_id_title_idx' + add_index :backup_labels, :project_id, name: 'backup_labels_project_id_idx' + add_index :backup_labels, :template, name: 'backup_labels_template_idx', where: 'template' + add_index :backup_labels, :title, name: 'backup_labels_title_idx' + add_index :backup_labels, [:type, :project_id], name: 'backup_labels_type_project_id_idx' + end +end diff --git a/db/schema_migrations/20210127152613 b/db/schema_migrations/20210127152613 new file mode 100644 index 00000000000..f5d42ea2bc5 --- /dev/null +++ b/db/schema_migrations/20210127152613 @@ -0,0 +1 @@ +32f636ffad4d2c6a002129d6e3eaeaf5d8f420dcc1273665129dc4d23f2e0dbe \ No newline at end of file diff --git a/db/schema_migrations/20210127202613 b/db/schema_migrations/20210127202613 new file mode 100644 index 00000000000..42d38d4c1d7 --- /dev/null +++ b/db/schema_migrations/20210127202613 @@ -0,0 +1 @@ +951f46f88c1b07505e0b560e802a8bd701db7d379342d97b0bff3ad90e81fb02 \ No newline at end of file diff --git a/db/schema_migrations/20210214201118 b/db/schema_migrations/20210214201118 new file mode 100644 index 00000000000..9551d2ddf50 --- /dev/null +++ b/db/schema_migrations/20210214201118 @@ -0,0 +1 @@ +8c1da1c7edba16993da93d9075ad2a3624b8c12ccf73a241e1a166014a99e254 \ No newline at end of file diff --git a/db/schema_migrations/20210214205155 b/db/schema_migrations/20210214205155 new file mode 100644 index 00000000000..583d7ca1167 --- /dev/null +++ b/db/schema_migrations/20210214205155 @@ -0,0 +1 @@ +7678d97de752e7a9a571d80febc74eb44c699c7b1967690d9a2391036caea5d2 \ No newline at end of file diff --git a/db/schema_migrations/20210215095328 b/db/schema_migrations/20210215095328 new file mode 100644 index 00000000000..b360aaf4ad8 --- /dev/null +++ b/db/schema_migrations/20210215095328 @@ -0,0 +1 @@ +25820a3d060826a082565f12a3ac96deafbbde750f5756d71e34d14801ec6148 \ No newline at end of file diff --git a/db/schema_migrations/20210218144056 b/db/schema_migrations/20210218144056 new file mode 100644 index 00000000000..566015ad8be --- /dev/null +++ b/db/schema_migrations/20210218144056 @@ -0,0 +1 @@ +545747e86481c74832a6df55764ab97ecfefc4446df9cc2366a8ce9d9c400ea4 \ No newline at end of file diff --git a/db/schema_migrations/20210218144656 b/db/schema_migrations/20210218144656 new file mode 100644 index 00000000000..622877a279e --- /dev/null +++ b/db/schema_migrations/20210218144656 @@ -0,0 +1 @@ +91969bfc791cd7bc78b940aa6fed345b13a3186db0b89828428b798aa4f7949e \ No newline at end of file diff --git a/db/schema_migrations/20210222185538 b/db/schema_migrations/20210222185538 new file mode 100644 index 00000000000..20229c9be20 --- /dev/null +++ b/db/schema_migrations/20210222185538 @@ -0,0 +1 @@ +0bccf1ff356a4b9c08d472e8b63070b497f331c2dfaded1bdb2cf01860df8903 \ No newline at end of file diff --git a/db/schema_migrations/20210222192144 b/db/schema_migrations/20210222192144 new file mode 100644 index 00000000000..b53b1668a1f --- /dev/null +++ b/db/schema_migrations/20210222192144 @@ -0,0 +1 @@ +b2508d46edbfbba24df65731f6e285886acbb6352a900dd1c6a985a686252ef0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 87e6d3e6cbb..6097ff0565b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9747,23 +9747,6 @@ CREATE SEQUENCE background_migration_jobs_id_seq ALTER SEQUENCE background_migration_jobs_id_seq OWNED BY background_migration_jobs.id; -CREATE TABLE backup_labels ( - id integer NOT NULL, - title character varying, - color character varying, - project_id integer, - created_at timestamp without time zone, - updated_at timestamp without time zone, - template boolean DEFAULT false, - description character varying, - description_html text, - type character varying, - group_id integer, - cached_markdown_version integer, - restore_action integer, - new_title character varying -); - CREATE TABLE badges ( id integer NOT NULL, link_url character varying NOT NULL, @@ -14370,6 +14353,7 @@ CREATE TABLE namespace_settings ( allow_mfa_for_subgroups boolean DEFAULT true NOT NULL, default_branch_name text, repository_read_only boolean DEFAULT false NOT NULL, + delayed_project_removal boolean DEFAULT false NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)) ); @@ -19823,9 +19807,6 @@ ALTER TABLE ONLY aws_roles ALTER TABLE ONLY background_migration_jobs ADD CONSTRAINT background_migration_jobs_pkey PRIMARY KEY (id); -ALTER TABLE ONLY backup_labels - ADD CONSTRAINT backup_labels_pkey PRIMARY KEY (id); - ALTER TABLE ONLY badges ADD CONSTRAINT badges_pkey PRIMARY KEY (id); @@ -19889,9 +19870,15 @@ ALTER TABLE ONLY chat_teams ALTER TABLE vulnerability_scanners ADD CONSTRAINT check_37608c9db5 CHECK ((char_length(vendor) <= 255)) NOT VALID; +ALTER TABLE sprints + ADD CONSTRAINT check_ccd8a1eae0 CHECK ((start_date IS NOT NULL)) NOT VALID; + ALTER TABLE group_import_states ADD CONSTRAINT check_cda75c7c3f CHECK ((user_id IS NOT NULL)) NOT VALID; +ALTER TABLE sprints + ADD CONSTRAINT check_df3816aed7 CHECK ((due_date IS NOT NULL)) NOT VALID; + ALTER TABLE ONLY ci_build_needs ADD CONSTRAINT ci_build_needs_pkey PRIMARY KEY (id); @@ -20400,7 +20387,7 @@ ALTER TABLE ONLY issues_self_managed_prometheus_alert_events ADD CONSTRAINT issues_self_managed_prometheus_alert_events_pkey PRIMARY KEY (issue_id, self_managed_prometheus_alert_event_id); ALTER TABLE ONLY sprints - ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint EXCLUDE USING gist (group_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL)); + ADD CONSTRAINT iteration_start_and_due_date_iterations_cadence_id_constraint EXCLUDE USING gist (iterations_cadence_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL)); ALTER TABLE ONLY sprints ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint EXCLUDE USING gist (project_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((project_id IS NOT NULL)); @@ -21271,18 +21258,6 @@ CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON approval_proj CREATE INDEX approval_mr_rule_index_merge_request_id ON approval_merge_request_rules USING btree (merge_request_id); -CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON backup_labels USING btree (group_id, project_id, title); - -CREATE INDEX backup_labels_group_id_title_idx ON backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer); - -CREATE INDEX backup_labels_project_id_idx ON backup_labels USING btree (project_id); - -CREATE INDEX backup_labels_template_idx ON backup_labels USING btree (template) WHERE template; - -CREATE INDEX backup_labels_title_idx ON backup_labels USING btree (title); - -CREATE INDEX backup_labels_type_project_id_idx ON backup_labels USING btree (type, project_id); - CREATE UNIQUE INDEX bulk_import_trackers_uniq_relation_by_entity ON bulk_import_trackers USING btree (bulk_import_entity_id, relation); CREATE INDEX ci_builds_gitlab_monitor_metrics ON ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text); @@ -23869,6 +23844,8 @@ CREATE UNIQUE INDEX term_agreements_unique_index ON term_agreements USING btree CREATE INDEX tmp_idx_deduplicate_vulnerability_occurrences ON vulnerability_occurrences USING btree (project_id, report_type, location_fingerprint, primary_identifier_id, id); +CREATE INDEX tmp_idx_on_namespaces_delayed_project_removal ON namespaces USING btree (id) WHERE (delayed_project_removal = true); + CREATE INDEX tmp_index_on_security_findings_scan_id ON security_findings USING btree (scan_id) WHERE (uuid IS NULL); CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING btree (id) WHERE (state <> 2); @@ -24461,9 +24438,6 @@ ALTER TABLE ONLY vulnerabilities ALTER TABLE ONLY labels ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; -ALTER TABLE ONLY backup_labels - ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; - ALTER TABLE ONLY merge_requests ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE; @@ -25979,9 +25953,6 @@ ALTER TABLE ONLY serverless_domain_cluster ALTER TABLE ONLY labels ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; -ALTER TABLE ONLY backup_labels - ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; - ALTER TABLE ONLY project_feature_usages ADD CONSTRAINT fk_rails_c22a50024b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/doc/api/merge_trains.md b/doc/api/merge_trains.md index d8c0acb3896..9fc17930c92 100644 --- a/doc/api/merge_trains.md +++ b/doc/api/merge_trains.md @@ -26,7 +26,7 @@ Read more on [pagination](README.md#pagination). Get all Merge Trains of the requested project: -```txt +```shell GET /projects/:id/merge_trains GET /projects/:id/merge_trains?scope=complete ``` diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index c11643c2405..c2ec00dfcad 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -10,41 +10,20 @@ disqus_identifier: 'https://docs.gitlab.com/ee/ci/environments.html' > Introduced in GitLab 8.9. -Environments allow control of the continuous deployment of your software, -all within GitLab. +Environments describe where code is deployed. -## Introduction - -There are many stages required in the software development process before the software is ready -for public consumption. - -For example: - -1. Develop your code. -1. Test your code. -1. Deploy your code into a testing or staging environment before you release it to the public. - -This helps find bugs in your software, and also in the deployment process as well. - -GitLab CI/CD is capable of not only testing or building your projects, but also -deploying them in your infrastructure, with the added benefit of giving you a -way to track your deployments. In other words, you always know what is -currently being deployed or has been deployed on your servers. - -It's important to know that: - -- Environments are like tags for your CI jobs, describing where code gets deployed. -- Deployments are created when [GitLab CI/CD](../yaml/README.md) is used to deploy versions of code to environments. +Each time [GitLab CI/CD](../yaml/README.md) deploys a version of code to an environment, +a deployment is created. GitLab: -- Provides a full history of your deployments for each environment. -- Keeps track of your deployments, so you always know what is currently being deployed on your +- Provides a full history of deployments to each environment. +- Tracks your deployments, so you always know what is currently deployed on your servers. -If you have a deployment service such as [Kubernetes](../../user/project/clusters/index.md) -associated with your project, you can use it to assist with your deployments, and -can even access a [web terminal](#web-terminals) for your environment from within GitLab! +If you have a deployment service like [Kubernetes](../../user/project/clusters/index.md) +associated with your project, you can use it to assist with your deployments. +You can even access a [web terminal](#web-terminals) for your environment from within GitLab. ## Configuring environments @@ -69,6 +48,10 @@ In the scenario: ### Defining environments +You can create environments manually in the web interface, but we recommend +that you define your environments in the `.gitlab-ci.yml` file. After the first +deploy, the environments are automatically created. + Let's consider the following `.gitlab-ci.yml` example: ```yaml @@ -539,8 +522,8 @@ So now, every branch: - Gets its own environment. - Is deployed to its own unique location, with the added benefit of: - - Having a [history of deployments](#viewing-deployment-history). - - Being able to [rollback changes](#retrying-and-rolling-back) if needed. + - Having a [history of deployments](#view-the-deployment-history). + - Being able to [roll back changes](#retry-or-roll-back-a-deployment) if needed. For more information, see [Using the environment URL](#using-the-environment-url). @@ -555,72 +538,46 @@ For more information, see [Protected environments](protected_environments.md). Once environments are configured, GitLab provides many features for working with them, as documented below. -### Viewing environments and deployments +### View environments and deployments -A list of environments and deployment statuses is available on each project's **Operations > Environments** page. +Prerequisites: -For example: +- You must have a minimum of [Reporter permission](../../user/permissions.md#project-members-permissions). -![Environment view](../img/environments_available_13_7.png) +To view a list of environments and deployment statuses: -This example shows: +- Go to the project's **Operations > Environments** page. -- The environment's name with a link to its deployments. -- The last deployment ID number and who performed it. -- The job ID of the last deployment with its respective job name. -- The commit information of the last deployment, such as who committed it, to what - branch, and the Git SHA of the commit. -- The exact time the last deployment was performed. -- The upcoming deployment, if a deployment for the environment is in progress. -- When the environment stops automatically. -- A button that takes you to the URL that you defined under the `environment` keyword - in `.gitlab-ci.yml`. -- A number of deployment actions, including: - - Prevent the environment from [stopping automatically](#automatically-stopping-an-environment). - - [Open the live environment](#using-the-environment-url). - - Trigger [a manual deployment to a different environment](#configuring-manual-deployments). - - [Retry the deployment](#retrying-and-rolling-back). - - [Stop the environment](#stopping-an-environment). +The **Environments** page shows the latest deployments. -The information shown in the **Environments** page is limited to the latest -deployments, but an environment can have multiple deployments. +- An environment can have multiple deployments. Some deployments may not be listed on the page. +- Only deploys that happen after your `.gitlab-ci.yml` is properly configured + show up in the **Environment** and **Last deployment** lists. -> **Notes:** -> -> - While you can create environments manually in the web interface, we recommend -> that you define your environments in `.gitlab-ci.yml` first. They will -> be automatically created for you after the first deploy. -> - The environments page can only be viewed by users with [Reporter permission](../../user/permissions.md#project-members-permissions) -> and above. For more information on permissions, see the [permissions documentation](../../user/permissions.md). -> - Only deploys that happen after your `.gitlab-ci.yml` is properly configured -> show up in the **Environment** and **Last deployment** lists. +### View the deployment history -### Viewing deployment history +GitLab tracks your deployments, so you: -GitLab keeps track of your deployments, so you: +- Always know what is currently deployed on your servers. +- Have the full history of your deployments for every environment. -- Always know what is currently being deployed on your servers. -- Can have the full history of your deployments for every environment. - -Clicking on an environment shows the history of its deployments. Here's an example **Environments** page -with multiple deployments: +- Go to the project's **Operations > Environments** page. ![Deployments](../img/deployments_view.png) -This view is similar to the **Environments** page, but all deployments are shown. Also in this view -is a **Rollback** button. For more information, see [Retrying and rolling back](#retrying-and-rolling-back). +This view is similar to the **Environments** page, but all deployments are shown. -### Retrying and rolling back +### Retry or roll back a deployment If there is a problem with a deployment, you can retry it or roll it back. To retry or rollback a deployment: -1. Navigate to **Operations > Environments**. -1. Click on the environment. -1. In the deployment history list for the environment, click the: - - **Retry** button next to the last deployment, to retry that deployment. - - **Rollback** button next to a previously successful deployment, to roll back to that deployment. +1. Go to the project's **Operations > Environments**. +1. Select the environment. +1. In the deployment history list for the environment: + - To retry a deployment, select **Retry**. + - to roll back to a deployment, next to a previously successful deployment, select **Rollback**. #### What to expect with a rollback @@ -662,7 +619,7 @@ from source files to public pages in the environment set for Review Apps. Stopping an environment: - Moves it from the list of **Available** environments to the list of **Stopped** - environments on the [**Environments** page](#viewing-environments-and-deployments). + environments on the [**Environments** page](#view-environments-and-deployments). - Executes an [`on_stop` action](../yaml/README.md#environmenton_stop), if defined. This is often used when multiple developers are working on a project at the same time, @@ -819,7 +776,7 @@ Environments can also be deleted by using the [Environments API](../../api/envir > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208655) in GitLab 13.2. -By default, GitLab creates a [deployment](#viewing-deployment-history) every time a +By default, GitLab creates a [deployment](#view-the-deployment-history) every time a build with the specified environment runs. Newer deployments can also [cancel older ones](deployment_safety.md#skip-outdated-deployment-jobs). @@ -897,7 +854,7 @@ severity is shown, so you can identify which environments need immediate attenti When the issue that triggered the alert is resolved, it is removed and is no longer visible on the environment page. -If the alert requires a [rollback](#retrying-and-rolling-back), you can select the +If the alert requires a [rollback](#retry-or-roll-back-a-deployment), you can select the deployment tab from the environment page and select which deployment to roll back to. #### Auto Rollback **(ULTIMATE)** diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md index 3eed9526e80..0c5fa59da8e 100644 --- a/doc/ci/jobs/index.md +++ b/doc/ci/jobs/index.md @@ -187,7 +187,7 @@ For example, if you start rolling out new code and: - Users do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%. - Users experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline - and [rolling](../environments/index.md#retrying-and-rolling-back) back to the last stable version. + and [rolling](../environments/index.md#retry-or-roll-back-a-deployment) back to the last stable version. ![Pipelines example](img/pipeline_incremental_rollout.png) diff --git a/doc/ci/yaml/gitlab_ci_yaml.md b/doc/ci/yaml/gitlab_ci_yaml.md index 765b982dbeb..25e8c0131ed 100644 --- a/doc/ci/yaml/gitlab_ci_yaml.md +++ b/doc/ci/yaml/gitlab_ci_yaml.md @@ -84,7 +84,7 @@ displayed by GitLab: ![pipeline status](img/pipeline_status.png) If anything goes wrong, you can -[roll back](../environments/index.md#retrying-and-rolling-back) the changes: +[roll back](../environments/index.md#retry-or-roll-back-a-deployment) the changes: ![rollback button](img/rollback.png) diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index 56d84657534..0c240e1ee75 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -593,7 +593,7 @@ required to go from `10%` to `100%`, you can jump to whatever job you want. You can also scale down by running a lower percentage job, just before hitting `100%`. Once you get to `100%`, you can't scale down, and you'd have to roll back by redeploying the old version using the -[rollback button](../../ci/environments/index.md#retrying-and-rolling-back) in the +[rollback button](../../ci/environments/index.md#retry-or-roll-back-a-deployment) in the environment page. Below, you can see how the pipeline appears if the rollout or staging diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 541f4ab57ab..283a4135d61 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -222,7 +222,7 @@ you to common environment tasks: - **Terminal** (**{terminal}**) - Opens a [web terminal](../../ci/environments/index.md#web-terminals) session inside the container where the application is running - **Re-deploy to environment** (**{repeat}**) - For more information, see - [Retrying and rolling back](../../ci/environments/index.md#retrying-and-rolling-back) + [Retrying and rolling back](../../ci/environments/index.md#retry-or-roll-back-a-deployment) - **Stop environment** (**{stop}**) - For more information, see [Stopping an environment](../../ci/environments/index.md#stopping-an-environment) diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 0cd7cb95f26..51d5f941311 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -475,7 +475,7 @@ URLs to scan can be specified by either of the following methods: To define the URLs to scan in a file, create a plain text file with one path per line. -```txt +```plaintext page1.html /page2.html category/shoes/page1.html diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md index a59b1f2e9b2..813d2b8e265 100644 --- a/doc/user/group/custom_project_templates.md +++ b/doc/user/group/custom_project_templates.md @@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w Custom project templates are useful for organizations that need to create many similar types of [projects](../project/index.md) and want to start from the same jumping-off point. -## Setting up Group-level Project Templates +## Setting up group-level project templates To use a custom project template for a new project you need to: @@ -30,7 +30,7 @@ To use a custom project template for a new project you need to: Here is a sample group/project structure for a hypothetical "Acme Co" for project templates: -```txt +```plaintext # GitLab instance and group gitlab.com/acmeco/ # Subgroups diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index 02c75a4deef..f4f9054d455 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -362,6 +362,11 @@ the user gets the highest access level from the groups. For example, if one grou is linked as `Guest` and another `Maintainer`, a user in both groups gets `Maintainer` access. +Users who are not members of any mapped SAML groups are removed from the GitLab group. + +You can prevent accidental member removal. For example, if you have a SAML group link for `Owner` level access +in a top-level group, you should also set up a group link for all other members. + ## Glossary | Term | Description | diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e883e9e2d48..4918563d8fa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16849,10 +16849,7 @@ msgstr "" msgid "Iterations" msgstr "" -msgid "Iteration|Dates cannot overlap with other existing Iterations" -msgstr "" - -msgid "Iteration|cannot be in the past" +msgid "Iteration|Dates cannot overlap with other existing Iterations within this group" msgstr "" msgid "Iteration|cannot be more than 500 years in the future" @@ -33642,10 +33639,7 @@ msgstr "" msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?" msgstr "" -msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs." -msgstr "" - -msgid "WikiPageConflictMessage|the page" +msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs." msgstr "" msgid "WikiPageCreate|Create %{pageTitle}" diff --git a/qa/qa/flow/project.rb b/qa/qa/flow/project.rb index db42a3a3594..8a9e2c86332 100644 --- a/qa/qa/flow/project.rb +++ b/qa/qa/flow/project.rb @@ -5,16 +5,6 @@ module QA module Project module_function - def add_member(project:, username:) - project.visit! - - Page::Project::Menu.perform(&:click_members) - - Page::Project::Members.perform do |member_settings| - member_settings.add_member(username) - end - end - def go_to_create_project_from_template if Page::Project::NewExperiment.perform(&:shown?) Page::Project::NewExperiment.perform(&:click_create_from_template_link) diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 17db69e4699..065eb36375a 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -15,7 +15,7 @@ FactoryBot.define do raise "Don't set owner for groups, use `group.add_owner(user)` instead" end - create(:namespace_settings, namespace: group) + create(:namespace_settings, namespace: group) unless group.namespace_settings end trait :public do diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js index 7e1d1acb62c..dba9c8be669 100644 --- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js +++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js @@ -1,10 +1,10 @@ import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue'; -import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; -import alertFields from '../mocks/alertFields.json'; +import alertFields from '../mocks/alert_fields.json'; +import parsedMapping from '../mocks/parsed_mapping.json'; describe('AlertMappingBuilder', () => { let wrapper; @@ -12,8 +12,8 @@ describe('AlertMappingBuilder', () => { function mountComponent() { wrapper = shallowMount(AlertMappingBuilder, { propsData: { - parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes, - savedMapping: parsedMapping.storedMapping.nodes, + parsedPayload: parsedMapping.payloadAlerFields, + savedMapping: parsedMapping.payloadAttributeMappings, alertFields, }, }); @@ -33,6 +33,15 @@ describe('AlertMappingBuilder', () => { const findColumnInRow = (row, column) => wrapper.findAll('.gl-display-table-row').at(row).findAll('.gl-display-table-cell ').at(column); + const getDropdownContent = (dropdown, types) => { + const searchBox = dropdown.findComponent(GlSearchBoxByType); + const dropdownItems = dropdown.findAllComponents(GlDropdownItem); + const mappingOptions = parsedMapping.payloadAlerFields.filter(({ type }) => + types.includes(type), + ); + return { searchBox, dropdownItems, mappingOptions }; + }; + it('renders column captions', () => { expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle); expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle); @@ -63,10 +72,7 @@ describe('AlertMappingBuilder', () => { it('renders mapping dropdown for each field', () => { alertFields.forEach(({ types }, index) => { const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); - const searchBox = dropdown.findComponent(GlSearchBoxByType); - const dropdownItems = dropdown.findAllComponents(GlDropdownItem); - const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const mappingOptions = nodes.filter(({ type }) => types.includes(type)); + const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types); expect(dropdown.exists()).toBe(true); expect(searchBox.exists()).toBe(true); @@ -80,11 +86,7 @@ describe('AlertMappingBuilder', () => { expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks)); if (numberOfFallbacks) { - const searchBox = dropdown.findComponent(GlSearchBoxByType); - const dropdownItems = dropdown.findAllComponents(GlDropdownItem); - const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const mappingOptions = nodes.filter(({ type }) => types.includes(type)); - + const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types); expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks)); expect(dropdownItems).toHaveLength(mappingOptions.length); } diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js index 02229b3d3da..511b3d2a059 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -11,7 +11,8 @@ import waitForPromises from 'helpers/wait_for_promises'; import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue'; import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; import { typeSet } from '~/alerts_settings/constants'; -import alertFields from '../mocks/alertFields.json'; +import alertFields from '../mocks/alert_fields.json'; +import parsedMapping from '../mocks/parsed_mapping.json'; import { defaultAlertSettingsConfig } from './util'; describe('AlertsSettingsForm', () => { @@ -39,6 +40,9 @@ describe('AlertsSettingsForm', () => { multiIntegrations, }, mocks: { + $apollo: { + query: jest.fn(), + }, $toast: { show: mockToastShow, }, @@ -146,7 +150,7 @@ describe('AlertsSettingsForm', () => { enableIntegration(0, integrationName); - const sampleMapping = { field: 'test' }; + const sampleMapping = parsedMapping.payloadAttributeMappings; findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping); findForm().trigger('submit'); @@ -157,7 +161,7 @@ describe('AlertsSettingsForm', () => { name: integrationName, active: true, payloadAttributeMappings: sampleMapping, - payloadExample: null, + payloadExample: '{}', }, }, ]); @@ -275,34 +279,47 @@ describe('AlertsSettingsForm', () => { }); describe('Test payload section for HTTP integration', () => { + const validSamplePayload = JSON.stringify(alertFields); + const emptySamplePayload = '{}'; + beforeEach(() => { createComponent({ multipleHttpIntegrationsCustomMapping: true, - props: { + data: { currentIntegration: { type: typeSet.http, + payloadExample: validSamplePayload, + payloadAttributeMappings: [], }, - alertFields, + active: false, + resetPayloadAndMappingConfirmed: false, }, + props: { alertFields }, }); }); describe.each` - active | resetSamplePayloadConfirmed | disabled - ${true} | ${true} | ${undefined} - ${false} | ${true} | ${'disabled'} - ${true} | ${false} | ${'disabled'} - ${false} | ${false} | ${'disabled'} - `('', ({ active, resetSamplePayloadConfirmed, disabled }) => { - const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed'; + active | resetPayloadAndMappingConfirmed | disabled + ${true} | ${true} | ${undefined} + ${false} | ${true} | ${'disabled'} + ${true} | ${false} | ${'disabled'} + ${false} | ${false} | ${'disabled'} + `('', ({ active, resetPayloadAndMappingConfirmed, disabled }) => { + const payloadResetMsg = resetPayloadAndMappingConfirmed + ? 'was confirmed' + : 'was not confirmed'; const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled'; const activeState = active ? 'active' : 'not active'; it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => { wrapper.setData({ - customMapping: { samplePayload: true }, + currentIntegration: { + type: typeSet.http, + payloadExample: validSamplePayload, + payloadAttributeMappings: [], + }, active, - resetSamplePayloadConfirmed, + resetPayloadAndMappingConfirmed, }); await wrapper.vm.$nextTick(); expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled); @@ -311,20 +328,27 @@ describe('AlertsSettingsForm', () => { describe('action buttons for sample payload', () => { describe.each` - resetSamplePayloadConfirmed | samplePayload | caption - ${false} | ${true} | ${'Edit payload'} - ${true} | ${false} | ${'Submit payload'} - ${true} | ${true} | ${'Submit payload'} - ${false} | ${false} | ${'Submit payload'} - `('', ({ resetSamplePayloadConfirmed, samplePayload, caption }) => { - const samplePayloadMsg = samplePayload ? 'was provided' : 'was not provided'; - const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed'; + resetPayloadAndMappingConfirmed | payloadExample | caption + ${false} | ${validSamplePayload} | ${'Edit payload'} + ${true} | ${emptySamplePayload} | ${'Submit payload'} + ${true} | ${validSamplePayload} | ${'Submit payload'} + ${false} | ${emptySamplePayload} | ${'Submit payload'} + `('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => { + const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided'; + const payloadResetMsg = resetPayloadAndMappingConfirmed + ? 'was confirmed' + : 'was not confirmed'; it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => { wrapper.setData({ selectedIntegration: typeSet.http, - customMapping: { samplePayload }, - resetSamplePayloadConfirmed, + currentIntegration: { + payloadExample, + type: typeSet.http, + active: true, + payloadAttributeMappings: [], + }, + resetPayloadAndMappingConfirmed, }); await wrapper.vm.$nextTick(); expect(findActionBtn().text()).toBe(caption); @@ -333,16 +357,20 @@ describe('AlertsSettingsForm', () => { }); describe('Parsing payload', () => { - it('displays a toast message on successful parse', async () => { - jest.useFakeTimers(); + beforeEach(() => { wrapper.setData({ selectedIntegration: typeSet.http, - customMapping: { samplePayload: false }, + resetPayloadAndMappingConfirmed: true, }); - await wrapper.vm.$nextTick(); + }); + it('displays a toast message on successful parse', async () => { + jest.spyOn(wrapper.vm.$apollo, 'query').mockResolvedValue({ + data: { + project: { alertManagementPayloadFields: [] }, + }, + }); findActionBtn().vm.$emit('click'); - jest.advanceTimersByTime(1000); await waitForPromises(); @@ -350,6 +378,16 @@ describe('AlertsSettingsForm', () => { 'Sample payload has been parsed. You can now map the fields.', ); }); + + it('displays an error message under payload field on unsuccessful parse', async () => { + const errorMessage = 'Error parsing paylod'; + jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({ message: errorMessage }); + findActionBtn().vm.$emit('click'); + + await waitForPromises(); + + expect(findTestPayloadSection().find('.invalid-feedback').text()).toBe(errorMessage); + }); }); }); diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js index 80293597ab6..409805fdbf7 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js @@ -14,6 +14,8 @@ import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutat import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql'; import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql'; import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql'; +import updateCurrentHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql'; +import updateCurrentPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql'; import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql'; import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; @@ -31,7 +33,8 @@ import { updateHttpVariables, createPrometheusVariables, updatePrometheusVariables, - ID, + HTTP_ID, + PROMETHEUS_ID, errorMsg, getIntegrationsQueryResponse, destroyIntegrationResponse, @@ -50,8 +53,30 @@ describe('AlertsSettingsWrapper', () => { let fakeApollo; let destroyIntegrationHandler; useMockIntersectionObserver(); + const httpMappingData = { + payloadExample: '{"test: : "field"}', + payloadAttributeMappings: [], + payloadAlertFields: [], + }; + const httpIntegrations = { + list: [ + { + id: mockIntegrations[0].id, + ...httpMappingData, + }, + { + id: mockIntegrations[1].id, + ...httpMappingData, + }, + { + id: mockIntegrations[2].id, + httpMappingData, + }, + ], + }; - const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon); + const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon); + const findIntegrationsList = () => wrapper.findComponent(IntegrationsList); const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr'); async function destroyHttpIntegration(localWrapper) { @@ -197,13 +222,13 @@ describe('AlertsSettingsWrapper', () => { }); wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { type: typeSet.http, - variables: { id: ID }, + variables: { id: HTTP_ID }, }); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: resetHttpTokenMutation, variables: { - id: ID, + id: HTTP_ID, }, }); }); @@ -232,7 +257,7 @@ describe('AlertsSettingsWrapper', () => { it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => { createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, + data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[3] }, loading: false, }); @@ -261,13 +286,13 @@ describe('AlertsSettingsWrapper', () => { }); wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { type: typeSet.prometheus, - variables: { id: ID }, + variables: { id: PROMETHEUS_ID }, }); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: resetPrometheusTokenMutation, variables: { - id: ID, + id: PROMETHEUS_ID, }, }); }); @@ -328,6 +353,42 @@ describe('AlertsSettingsWrapper', () => { mock.restore(); }); }); + + it('calls `$apollo.mutate` with `updateCurrentHttpIntegrationMutation` on HTTP integration edit', () => { + createComponent({ + data: { + integrations: { list: mockIntegrations }, + currentIntegration: mockIntegrations[0], + httpIntegrations, + }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + findIntegrationsList().vm.$emit('edit-integration', updateHttpVariables); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateCurrentHttpIntegrationMutation, + variables: { ...mockIntegrations[0], ...httpMappingData }, + }); + }); + + it('calls `$apollo.mutate` with `updateCurrentPrometheusIntegrationMutation` on PROMETHEUS integration edit', () => { + createComponent({ + data: { + integrations: { list: mockIntegrations }, + currentIntegration: mockIntegrations[3], + httpIntegrations, + }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + findIntegrationsList().vm.$emit('edit-integration', updatePrometheusVariables); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateCurrentPrometheusIntegrationMutation, + variables: mockIntegrations[3], + }); + }); }); describe('with mocked Apollo client', () => { diff --git a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js index e0eba1e8421..828580a436b 100644 --- a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js +++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js @@ -1,29 +1,34 @@ const projectPath = ''; -export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7'; +export const HTTP_ID = 'gid://gitlab/AlertManagement::HttpIntegration/7'; +export const PROMETHEUS_ID = 'gid://gitlab/PrometheusService/12'; export const errorMsg = 'Something went wrong'; export const createHttpVariables = { name: 'Test Pre', active: true, projectPath, + type: 'HTTP', }; export const updateHttpVariables = { name: 'Test Pre', active: true, - id: ID, + id: HTTP_ID, + type: 'HTTP', }; export const createPrometheusVariables = { apiUrl: 'https://test-pre.com', active: true, projectPath, + type: 'PROMETHEUS', }; export const updatePrometheusVariables = { apiUrl: 'https://test-pre.com', active: true, - id: ID, + id: PROMETHEUS_ID, + type: 'PROMETHEUS', }; export const getIntegrationsQueryResponse = { @@ -99,6 +104,9 @@ export const destroyIntegrationResponse = { 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', token: '89eb01df471d990ff5162a1c640408cf', apiUrl: null, + payloadExample: '{"field": "value"}', + payloadAttributeMappings: [], + payloadAlertFields: [], }, }, }, @@ -117,6 +125,9 @@ export const destroyIntegrationResponseWithErrors = { 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', token: '89eb01df471d990ff5162a1c640408cf', apiUrl: null, + payloadExample: '{"field": "value"}', + payloadAttributeMappings: [], + payloadAlertFields: [], }, }, }, diff --git a/spec/frontend/alerts_settings/mocks/alertFields.json b/spec/frontend/alerts_settings/mocks/alert_fields.json similarity index 100% rename from spec/frontend/alerts_settings/mocks/alertFields.json rename to spec/frontend/alerts_settings/mocks/alert_fields.json diff --git a/spec/frontend/alerts_settings/mocks/parsed_mapping.json b/spec/frontend/alerts_settings/mocks/parsed_mapping.json new file mode 100644 index 00000000000..e985671a923 --- /dev/null +++ b/spec/frontend/alerts_settings/mocks/parsed_mapping.json @@ -0,0 +1,122 @@ +{ + "payloadAlerFields": [ + { + "path": [ + "dashboardId" + ], + "label": "Dashboard Id", + "type": "string" + }, + { + "path": [ + "evalMatches" + ], + "label": "Eval Matches", + "type": "array" + }, + { + "path": [ + "createdAt" + ], + "label": "Created At", + "type": "datetime" + }, + { + "path": [ + "imageUrl" + ], + "label": "Image Url", + "type": "string" + }, + { + "path": [ + "message" + ], + "label": "Message", + "type": "string" + }, + { + "path": [ + "orgId" + ], + "label": "Org Id", + "type": "string" + }, + { + "path": [ + "panelId" + ], + "label": "Panel Id", + "type": "string" + }, + { + "path": [ + "ruleId" + ], + "label": "Rule Id", + "type": "string" + }, + { + "path": [ + "ruleName" + ], + "label": "Rule Name", + "type": "string" + }, + { + "path": [ + "ruleUrl" + ], + "label": "Rule Url", + "type": "string" + }, + { + "path": [ + "state" + ], + "label": "State", + "type": "string" + }, + { + "path": [ + "title" + ], + "label": "Title", + "type": "string" + }, + { + "path": [ + "tags", + "tag" + ], + "label": "Tags", + "type": "string" + } + ], + "payloadAttributeMappings": [ + { + "fieldName": "title", + "label": "Title", + "type": "STRING", + "path": ["title"] + }, + { + "fieldName": "description", + "label": "description", + "type": "STRING", + "path": ["description"] + }, + { + "fieldName": "hosts", + "label": "Host", + "type": "ARRAY", + "path": ["hosts", "host"] + }, + { + "fieldName": "startTime", + "label": "Created Atd", + "type": "STRING", + "path": ["time", "createdAt"] + } + ] +} diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js index 8c1977ffebe..62b95c6078b 100644 --- a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js +++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js @@ -1,29 +1,25 @@ -import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; -import { - getMappingData, - getPayloadFields, - transformForSave, -} from '~/alerts_settings/utils/mapping_transformations'; -import alertFields from '../mocks/alertFields.json'; +import { getMappingData, transformForSave } from '~/alerts_settings/utils/mapping_transformations'; +import alertFields from '../mocks/alert_fields.json'; +import parsedMapping from '../mocks/parsed_mapping.json'; describe('Mapping Transformation Utilities', () => { const nameField = { label: 'Name', path: ['alert', 'name'], - type: 'string', + type: 'STRING', }; const dashboardField = { label: 'Dashboard Id', path: ['alert', 'dashboardId'], - type: 'string', + type: 'STRING', }; describe('getMappingData', () => { it('should return mapping data', () => { const result = getMappingData( alertFields, - getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)), - parsedMapping.storedMapping.nodes.slice(0, 3), + parsedMapping.payloadAlerFields.slice(0, 3), + parsedMapping.payloadAttributeMappings.slice(0, 3), ); result.forEach((data, index) => { @@ -44,8 +40,8 @@ describe('Mapping Transformation Utilities', () => { const mockMappingData = [ { name: fieldName, - mapping: 'alert_name', - mappingFields: getPayloadFields([dashboardField, nameField]), + mapping: ['alert', 'name'], + mappingFields: [dashboardField, nameField], }, ]; const result = transformForSave(mockMappingData); @@ -61,21 +57,11 @@ describe('Mapping Transformation Utilities', () => { { name: fieldName, mapping: null, - mappingFields: getPayloadFields([nameField, dashboardField]), + mappingFields: [nameField, dashboardField], }, ]; const result = transformForSave(mockMappingData); expect(result).toEqual([]); }); }); - - describe('getPayloadFields', () => { - it('should add name field to each payload field', () => { - const result = getPayloadFields([nameField, dashboardField]); - expect(result).toEqual([ - { ...nameField, name: 'alert_name' }, - { ...dashboardField, name: 'alert_dashboardId' }, - ]); - }); - }); }); diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap index 73a64875026..77095f7c611 100644 --- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap @@ -102,7 +102,7 @@ exports[`packages_list_row renders 1`] = ` { }); describe('when is is group', () => { - beforeEach(() => { - mountComponent({ isGroup: true }); - }); - it('has a package path component', () => { + mountComponent({ isGroup: true }); + expect(findPackagePath().exists()).toBe(true); expect(findPackagePath().props()).toMatchObject({ path: 'foo/bar/baz' }); }); @@ -92,10 +90,22 @@ describe('packages_list_row', () => { }); }); - describe('delete event', () => { - beforeEach(() => mountComponent({ packageEntity: packageWithoutTags })); + describe('delete button', () => { + it('exists and has the correct props', () => { + mountComponent({ packageEntity: packageWithoutTags }); + + expect(findDeleteButton().exists()).toBe(true); + expect(findDeleteButton().attributes()).toMatchObject({ + icon: 'remove', + category: 'secondary', + variant: 'danger', + title: 'Remove package', + }); + }); it('emits the packageToDelete event when the delete button is clicked', async () => { + mountComponent({ packageEntity: packageWithoutTags }); + findDeleteButton().vm.$emit('click'); await wrapper.vm.$nextTick(); diff --git a/spec/frontend/pages/shared/wikis/wiki_alert_spec.js b/spec/frontend/pages/shared/wikis/wiki_alert_spec.js new file mode 100644 index 00000000000..6a18473b1a7 --- /dev/null +++ b/spec/frontend/pages/shared/wikis/wiki_alert_spec.js @@ -0,0 +1,40 @@ +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import WikiAlert from '~/pages/shared/wikis/components/wiki_alert.vue'; + +describe('WikiAlert', () => { + let wrapper; + const ERROR = 'There is already a page with the same title in that path.'; + const ERROR_WITH_LINK = 'Before text %{wikiLinkStart}the page%{wikiLinkEnd} after text.'; + const PATH = '/test'; + + function createWrapper(propsData = {}, stubs = {}) { + wrapper = shallowMount(WikiAlert, { + propsData: { wikiPagePath: PATH, ...propsData }, + stubs, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findGlLink = () => wrapper.findComponent(GlLink); + const findGlSprintf = () => wrapper.findComponent(GlSprintf); + + describe('Wiki Alert', () => { + it('shows an alert when there is an error', () => { + createWrapper({ error: ERROR }); + expect(findGlAlert().exists()).toBe(true); + expect(findGlSprintf().exists()).toBe(true); + expect(findGlSprintf().attributes('message')).toBe(ERROR); + }); + + it('shows a the link to the help path', () => { + createWrapper({ error: ERROR_WITH_LINK }, { GlAlert, GlSprintf }); + expect(findGlLink().attributes('href')).toBe(PATH); + }); + }); +}); diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js index bc4c5600d99..db15f6520ca 100644 --- a/spec/frontend/pipelines/stage_spec.js +++ b/spec/frontend/pipelines/stage_spec.js @@ -142,6 +142,8 @@ describe('Pipelines stage component', () => { beforeEach(() => { mock.onGet(dropdownPath).reply(200, stageReply); mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + + createComponent(); }); const clickCiAction = async () => { @@ -152,34 +154,22 @@ describe('Pipelines stage component', () => { await axios.waitForAll(); }; - describe('within pipeline table', () => { - beforeEach(() => { - createComponent({ type: 'PIPELINES_TABLE' }); - }); + it('closes dropdown when job item action is clicked', async () => { + const hidden = jest.fn(); - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { - await clickCiAction(); + wrapper.vm.$root.$on('bv::dropdown::hide', hidden); - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }); + expect(hidden).toHaveBeenCalledTimes(0); + + await clickCiAction(); + + expect(hidden).toHaveBeenCalledTimes(1); }); - describe('in MR widget', () => { - beforeEach(() => { - createComponent(); - }); + it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { + await clickCiAction(); - it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { - const hidden = jest.fn(); - - wrapper.vm.$root.$on('bv::dropdown::hide', hidden); - - expect(hidden).toHaveBeenCalledTimes(0); - - await clickCiAction(); - - expect(hidden).toHaveBeenCalledTimes(1); - }); + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/registry/explorer/components/delete_button_spec.js index a557d9afacc..4597c42add9 100644 --- a/spec/frontend/registry/explorer/components/delete_button_spec.js +++ b/spec/frontend/registry/explorer/components/delete_button_spec.js @@ -58,6 +58,7 @@ describe('delete_button', () => { title: 'Foo title', variant: 'danger', disabled: 'true', + category: 'secondary', }); }); diff --git a/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb b/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb index 1f1877f5d2b..46c919f0854 100644 --- a/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb +++ b/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb @@ -58,12 +58,10 @@ RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema: context 'when an iteration cadence exists for a group' do let!(:group) { namespaces.create!(name: 'group', path: 'group') } - let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 5.days.ago, title: 'Cadence 1') } - let!(:iterations_cadence_2) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 2') } + let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 1') } let!(:iteration_1) { iterations.create!(group_id: group.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) } let!(:iteration_2) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_1.id, iid: 2, title: 'Iteration 2', start_date: 5.days.ago, due_date: 3.days.ago) } - let!(:iteration_3) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_2.id, iid: 3, title: 'Iteration 3', start_date: 2.days.ago, due_date: 1.day.ago) } subject { described_class.new.perform(group.id) } @@ -76,7 +74,6 @@ RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema: expect(iteration_1.reload.iterations_cadence_id).to eq(iterations_cadence_1.id) expect(iteration_2.reload.iterations_cadence_id).to eq(iterations_cadence_1.id) - expect(iteration_3.reload.iterations_cadence_id).to eq(iterations_cadence_2.id) end end end diff --git a/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb new file mode 100644 index 00000000000..28a8dcf0d4c --- /dev/null +++ b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require Rails.root.join('db', 'post_migrate', '20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb') + +RSpec.describe MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings, :migration do + let(:namespaces) { table(:namespaces) } + let(:namespace_settings) { table(:namespace_settings) } + + let!(:namespace_wo_settings) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) } + let!(:namespace_wo_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) } + let!(:namespace_w_settings_delay_true) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) } + let!(:namespace_w_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) } + + let!(:namespace_settings_delay_true) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_true.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) } + let!(:namespace_settings_delay_false) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_false.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) } + + it 'migrates delayed_project_removal to namespace_settings' do + disable_migrations_output { migrate! } + + expect(namespace_settings.count).to eq(3) + + expect(namespace_settings.find_by(namespace_id: namespace_wo_settings.id).delayed_project_removal).to eq(true) + expect(namespace_settings.find_by(namespace_id: namespace_wo_settings_delay_false.id)).to be_nil + + expect(namespace_settings_delay_true.reload.delayed_project_removal).to eq(true) + expect(namespace_settings_delay_false.reload.delayed_project_removal).to eq(false) + end +end diff --git a/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb b/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb index 25c2b4efe8b..fb629c90d9f 100644 --- a/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb +++ b/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb @@ -16,13 +16,13 @@ RSpec.describe RescheduleSetDefaultIterationCadences do let(:group_7) { namespaces.create!(name: 'test_7', path: 'test_7') } let(:group_8) { namespaces.create!(name: 'test_8', path: 'test_8') } - let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id) } - let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id) } - let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id) } - let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id) } - let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id) } - let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id) } - let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id) } + let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } around do |example| freeze_time { Sidekiq::Testing.fake! { example.run } } diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb index 7241a07a215..7c57f08b2bd 100644 --- a/spec/models/iteration_spec.rb +++ b/spec/models/iteration_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' RSpec.describe Iteration do - let_it_be(:project) { create(:project) } - let_it_be(:group) { create(:group) } let(:set_cadence) { nil } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:group) } @@ -67,7 +68,7 @@ RSpec.describe Iteration do expect { iteration.save! }.to change { Iterations::Cadence.count }.by(1) end - it 'sets the newly created iterations_cadence to the reecord' do + it 'sets the newly created iterations_cadence to the record' do iteration.save! expect(iteration.iterations_cadence).to eq(Iterations::Cadence.last) @@ -148,7 +149,7 @@ RSpec.describe Iteration do context 'Validations' do subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) } - describe '#not_belonging_to_project' do + describe 'when iteration belongs to project' do subject { build(:iteration, project: project, start_date: Time.current, due_date: 1.day.from_now) } it 'is invalid' do @@ -180,13 +181,13 @@ RSpec.describe Iteration do let(:due_date) { 6.days.from_now } shared_examples_for 'overlapping dates' do |skip_constraint_test: false| - context 'when start_date is in range' do + context 'when start_date overlaps' do let(:start_date) { 5.days.from_now } let(:due_date) { 3.weeks.from_now } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') + expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group') end unless skip_constraint_test @@ -197,13 +198,13 @@ RSpec.describe Iteration do end end - context 'when end_date is in range' do + context 'when due_date overlaps' do let(:start_date) { Time.current } let(:due_date) { 6.days.from_now } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') + expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group') end unless skip_constraint_test @@ -217,7 +218,7 @@ RSpec.describe Iteration do context 'when both overlap' do it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') + expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group') end unless skip_constraint_test @@ -231,7 +232,7 @@ RSpec.describe Iteration do context 'group' do it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' } + let(:constraint_name) { 'iteration_start_and_due_date_iterations_cadence_id_constraint' } end context 'different group' do @@ -249,11 +250,12 @@ RSpec.describe Iteration do subject { build(:iteration, group: subgroup, start_date: start_date, due_date: due_date) } - it_behaves_like 'overlapping dates', skip_constraint_test: true + it { is_expected.to be_valid } end end - context 'project' do + # Skipped. Pending https://gitlab.com/gitlab-org/gitlab/-/issues/299864 + xcontext 'project' do let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } @@ -283,16 +285,16 @@ RSpec.describe Iteration do expect { subject.save! }.not_to raise_exception end end - end - context 'project in a group' do - let_it_be(:project) { create(:project, group: create(:group)) } - let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } + context 'project in a group' do + let_it_be(:project) { create(:project, group: create(:group)) } + let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } + subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } + it_behaves_like 'overlapping dates' do + let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } + end end end end @@ -310,19 +312,23 @@ RSpec.describe Iteration do let(:start_date) { 1.week.ago } let(:due_date) { 1.week.from_now } - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:start_date]).to include('cannot be in the past') - end + it { is_expected.to be_valid } end context 'when due_date is in the past' do + let(:start_date) { 2.weeks.ago } + let(:due_date) { 1.week.ago } + + it { is_expected.to be_valid } + end + + context 'when due_date is before start date' do let(:start_date) { Time.current } let(:due_date) { 1.week.ago } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:due_date]).to include('cannot be in the past') + expect(subject.errors[:due_date]).to include('must be greater than start date') end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index ed0b9063e32..b3c3c6aaa41 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Namespace do include ProjectForksHelper include GitHelpers - let!(:namespace) { create(:namespace) } + let!(:namespace) { create(:namespace, :with_namespace_settings) } let(:gitlab_shell) { Gitlab::Shell.new } let(:repository_storage) { 'default' } @@ -116,6 +116,28 @@ RSpec.describe Namespace do it { is_expected.to include_module(Namespaces::Traversal::Recursive) } end + describe 'callbacks' do + describe 'before_save :ensure_delayed_project_removal_assigned_to_namespace_settings' do + it 'sets the matching value in namespace_settings' do + expect { namespace.update!(delayed_project_removal: true) }.to change { + namespace.namespace_settings.delayed_project_removal + }.from(false).to(true) + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(migrate_delayed_project_removal: false) + end + + it 'does not set the matching value in namespace_settings' do + expect { namespace.update!(delayed_project_removal: true) }.not_to change { + namespace.namespace_settings.delayed_project_removal + } + end + end + end + end + describe '#visibility_level_field' do it { expect(namespace.visibility_level_field).to eq(:visibility_level) } end diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index 84ebd4852b9..51d52cbb901 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -48,6 +48,6 @@ RSpec.shared_examples 'a mutation that returns errors in the response' do |error it do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['errors']).to eq(errors) + expect(mutation_response['errors']).to match_array(errors) end end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index f91e4bd8cf7..68142e667a4 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a project' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) } + let(:instance) { build(timebox_type, *timebox_args, project: create(:project), group: nil) } let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { timebox_table_name } @@ -28,7 +28,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a group' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) } + let(:instance) { build(timebox_type, *timebox_args, project: nil, group: create(:group)) } let(:scope) { :group } let(:scope_attrs) { { namespace: instance.group } } let(:usage) { timebox_table_name }