diff options
| author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-31 18:08:39 +0000 |
|---|---|---|
| committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-31 18:08:39 +0000 |
| commit | e7fb61499317b3c044845817b991e13aea276955 (patch) | |
| tree | 127aef09952fa595b8e5b886db5dd89d974d91da | |
| parent | 85ea3dd4f4855e99d9a69c8e609095199597195a (diff) | |
| download | gitlab-ce-e7fb61499317b3c044845817b991e13aea276955.tar.gz | |
Add latest changes from gitlab-org/gitlab@master
83 files changed, 676 insertions, 709 deletions
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index aee61a5b2a5..02b99e86fd3 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -240,7 +240,7 @@ export default { class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end" > <div - class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container" + class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden" > <gl-loading-icon v-if="item.isLoading" size="md" class="mt-3" /> <span diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index aba1f42ead5..a874c9e070a 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -289,7 +289,7 @@ export default { <p v-if="isDeleteForm" data-testid="delete-confirmation-message"> {{ $options.i18n.deleteConfirmationMessage }} </p> - <form v-else class="js-board-config-modal" data-testid="board-form-wrapper" @submit.prevent> + <form v-else data-testid="board-form-wrapper" @submit.prevent> <div v-if="!readonly" class="gl-mb-5" data-testid="board-form"> <label class="gl-font-weight-bold gl-font-lg" for="board-new-name"> {{ $options.i18n.titleFieldLabel }} diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 1024be61359..858cd368b12 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -287,7 +287,7 @@ export default { :data-board-type="list.listType" :class="{ 'bg-danger-100': boardItemsSizeExceedsMax }" draggable=".board-card" - class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list" + class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2" data-testid="tree-root-wrapper" @start="handleDragOnStart" @end="handleDragOnEnd" diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 46b28d20da9..f02feab264c 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -262,7 +262,7 @@ export default { 'gl-py-2': list.collapsed && isSwimlanesHeader, 'gl-flex-direction-column': list.collapsed, }" - class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle" + class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3" > <gl-button v-gl-tooltip.hover @@ -443,7 +443,7 @@ export default { ref="settingsBtn" v-gl-tooltip.hover :aria-label="$options.i18n.listSettings" - class="no-drag js-board-settings-button" + class="no-drag" :title="$options.i18n.listSettings" icon="settings" @click="openSidebarSettings" diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 8333856e996..2951eda1112 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -239,11 +239,12 @@ export default { </script> <template> - <div class="boards-switcher js-boards-selector gl-mr-3"> - <span class="boards-selector-wrapper js-boards-selector-wrapper"> + <div class="boards-switcher gl-mr-3" data-testid="boards-selector"> + <span class="boards-selector-wrapper"> <gl-dropdown + data-testid="boards-dropdown" data-qa-selector="boards_dropdown" - toggle-class="dropdown-menu-toggle js-dropdown-toggle" + toggle-class="dropdown-menu-toggle" menu-class="flex-column dropdown-extended-height" :loading="isBoardLoading" :text="board.name" @@ -276,8 +277,8 @@ export default { <gl-dropdown-item v-for="recentBoard in recentBoards" :key="`recent-${recentBoard.id}`" - class="js-dropdown-item" :href="`${boardBaseUrl}/${recentBoard.id}`" + data-testid="dropdown-item" > {{ recentBoard.name }} </gl-dropdown-item> @@ -292,8 +293,8 @@ export default { <gl-dropdown-item v-for="otherBoard in filteredBoards" :key="otherBoard.id" - class="js-dropdown-item" :href="`${boardBaseUrl}/${otherBoard.id}`" + data-testid="dropdown-item" > {{ otherBoard.name }} </gl-dropdown-item> @@ -331,7 +332,7 @@ export default { <gl-dropdown-item v-if="showDelete" v-gl-modal-directive="'board-config-modal'" - class="text-danger js-delete-board" + class="text-danger" @click.prevent="showPage('delete')" > {{ s__('IssueBoards|Delete board') }} diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue index 1ab7deebfaf..9312db06efe 100644 --- a/app/assets/javascripts/boards/components/issue_time_estimate.vue +++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue @@ -43,7 +43,7 @@ export default { <gl-tooltip :target="() => $refs.issueTimeEstimate" placement="bottom" - class="js-issue-time-estimate" + data-testid="issue-time-estimate" > <span class="gl-font-weight-bold gl-display-block">{{ $options.i18n.timeEstimate }}</span> {{ title }} diff --git a/app/assets/javascripts/boards/components/item_count.vue b/app/assets/javascripts/boards/components/item_count.vue index 9b1ff254766..a11c23e5625 100644 --- a/app/assets/javascripts/boards/components/item_count.vue +++ b/app/assets/javascripts/boards/components/item_count.vue @@ -29,7 +29,7 @@ export default { <span :class="{ 'text-danger': issuesExceedMax }" data-testid="board-items-count"> {{ itemsSize }} </span> - <span v-if="isMaxLimitSet" class="js-max-issue-size"> + <span v-if="isMaxLimitSet" class="max-issue-size"> {{ maxIssueCount }} </span> </div> diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue index 0169c9c72ed..71612e0742f 100644 --- a/app/assets/javascripts/boards/components/toggle_focus.vue +++ b/app/assets/javascripts/boards/components/toggle_focus.vue @@ -38,7 +38,6 @@ export default { v-gl-tooltip category="tertiary" :icon="isFullscreen ? 'minimize' : 'maximize'" - class="js-focus-mode-btn" data-qa-selector="focus_mode_button" :title="$options.i18n.toggleFocusMode" :aria-label="$options.i18n.toggleFocusMode" diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js deleted file mode 100644 index 1e54c2511b8..00000000000 --- a/app/assets/javascripts/boards/config_toggle.js +++ /dev/null @@ -1,25 +0,0 @@ -import Vue from 'vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import ConfigToggle from './components/config_toggle.vue'; - -export default () => { - const el = document.querySelector('.js-board-config'); - - if (!el) { - return; - } - - // eslint-disable-next-line no-new - new Vue({ - el, - name: 'ConfigToggleRoot', - render(h) { - return h(ConfigToggle, { - props: { - canAdminList: parseBoolean(el.dataset.canAdminList), - hasScope: parseBoolean(el.dataset.hasScope), - }, - }); - }, - }); -}; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 2abc283c5b4..77c5994b5a1 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -13,7 +13,6 @@ import { } from '~/lib/utils/common_utils'; import { queryToObject } from '~/lib/utils/url_utility'; import { fullBoardId } from './boards_util'; -import initNewBoard from './new_board'; import { gqlClient } from './graphql'; Vue.use(VueApollo); @@ -112,6 +111,4 @@ export default () => { }); mountBoardApp($boardApp); - - initNewBoard(); }; diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js deleted file mode 100644 index bb659eb075a..00000000000 --- a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js +++ /dev/null @@ -1,42 +0,0 @@ -import Vue from 'vue'; -import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue'; -import store from '~/boards/stores'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { queryToObject } from '~/lib/utils/url_utility'; - -export default ( - apolloProvider, - isSignedIn, - releasesFetchPath, - epicFeatureAvailable, - iterationFeatureAvailable, -) => { - const el = document.getElementById('js-issue-board-filtered-search'); - const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true }); - - const initialFilterParams = { - ...convertObjectPropsToCamelCase(rawFilterParams, {}), - }; - - if (!el) { - return null; - } - - return new Vue({ - el, - name: 'BoardFilteredSearchRoot', - provide: { - initialFilterParams, - isSignedIn, - releasesFetchPath, - epicFeatureAvailable, - iterationFeatureAvailable, - }, - store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094 - apolloProvider, - render: (createElement) => - createElement(IssueBoardFilteredSearch, { - props: { fullPath: store.state?.fullPath || '', boardType: store.state?.boardType || '' }, - }), - }); -}; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js deleted file mode 100644 index 0bc9cfbd867..00000000000 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue'; -import store from '~/boards/stores'; -import createDefaultClient from '~/lib/graphql'; -import { parseBoolean } from '~/lib/utils/common_utils'; - -Vue.use(VueApollo); - -const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), -}); - -export default (params = {}) => { - const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); - const { dataset } = boardsSwitcherElement; - return new Vue({ - el: boardsSwitcherElement, - name: 'BoardsSelectorRoot', - components: { - BoardsSelector, - }, - apolloProvider, - store, - provide: { - fullPath: params.fullPath, - rootPath: params.rootPath, - allowScopedLabels: params.allowScopedLabels, - labelsManagePath: params.labelsManagePath, - allowLabelCreate: parseBoolean(dataset.canAdminBoard), - }, - data() { - const boardsSelectorProps = { - ...dataset, - hasMissingBoards: parseBoolean(dataset.hasMissingBoards), - canAdminBoard: parseBoolean(dataset.canAdminBoard), - multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), - scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), - weights: JSON.parse(dataset.weights), - }; - - return { boardsSelectorProps }; - }, - render(createElement) { - return createElement(BoardsSelector, { - props: this.boardsSelectorProps, - }); - }, - }); -}; diff --git a/app/assets/javascripts/boards/new_board.js b/app/assets/javascripts/boards/new_board.js deleted file mode 100644 index 34f2fea79a9..00000000000 --- a/app/assets/javascripts/boards/new_board.js +++ /dev/null @@ -1,29 +0,0 @@ -import Vue from 'vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import { getExperimentVariant } from '~/experimentation/utils'; -import { CANDIDATE_VARIANT } from '~/experimentation/constants'; -import NewBoardButton from './components/new_board_button.vue'; - -export default () => { - if (getExperimentVariant('prominent_create_board_btn') !== CANDIDATE_VARIANT) { - return; - } - - const el = document.querySelector('.js-new-board'); - - if (!el) { - return; - } - - // eslint-disable-next-line no-new - new Vue({ - el, - provide: { - multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleIssueBoardsAvailable), - canAdminBoard: parseBoolean(el.dataset.canAdminBoard), - }, - render(h) { - return h(NewBoardButton); - }, - }); -}; diff --git a/app/assets/javascripts/boards/toggle_epics_swimlanes.js b/app/assets/javascripts/boards/toggle_epics_swimlanes.js deleted file mode 100644 index 2d1ec238274..00000000000 --- a/app/assets/javascripts/boards/toggle_epics_swimlanes.js +++ /dev/null @@ -1 +0,0 @@ -export default () => {}; diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js deleted file mode 100644 index 8f057e192dd..00000000000 --- a/app/assets/javascripts/boards/toggle_focus.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue'; -import ToggleFocus from './components/toggle_focus.vue'; - -export default () => { - const issueBoardsContentSelector = '.content-wrapper > .js-focus-mode-board'; - - return new Vue({ - el: '#js-toggle-focus-btn', - name: 'ToggleFocusRoot', - render(h) { - return h(ToggleFocus, { - props: { - issueBoardsContentSelector, - }, - }); - }, - }); -}; diff --git a/app/assets/javascripts/boards/toggle_labels.js b/app/assets/javascripts/boards/toggle_labels.js deleted file mode 100644 index 2d1ec238274..00000000000 --- a/app/assets/javascripts/boards/toggle_labels.js +++ /dev/null @@ -1 +0,0 @@ -export default () => {}; diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue index 6d2ff9ea7b6..154e176dc6e 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue @@ -1,4 +1,5 @@ <script> +import { GlLink } from '@gitlab/ui'; import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility'; import { n__, sprintf } from '~/locale'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; @@ -9,6 +10,7 @@ import { LIST_INTRO_TEXT, EXPIRATION_POLICY_WILL_RUN_IN, EXPIRATION_POLICY_DISABLED_TEXT, + SET_UP_CLEANUP, } from '../../constants/index'; export default { @@ -16,6 +18,7 @@ export default { components: { TitleArea, MetadataItem, + GlLink, }, props: { expirationPolicy: { @@ -43,6 +46,16 @@ export default { required: false, default: false, }, + cleanupPoliciesSettingsPath: { + type: String, + default: '', + required: false, + }, + showCleanupPolicyLink: { + type: Boolean, + required: false, + default: false, + }, }, loader: { repeat: 10, @@ -51,6 +64,7 @@ export default { }, i18n: { CONTAINER_REGISTRY_TITLE, + SET_UP_CLEANUP, }, computed: { imagesCountText() { @@ -105,6 +119,9 @@ export default { :text="expirationPolicyText" size="xl" /> + <gl-link v-if="showCleanupPolicyLink" class="gl-ml-2" :href="cleanupPoliciesSettingsPath">{{ + $options.i18n.SET_UP_CLEANUP + }}</gl-link> </template> </title-area> </template> diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js index 40f9b09a982..e584da23edb 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js @@ -4,7 +4,7 @@ export const EXPIRATION_POLICY_WILL_RUN_IN = s__( 'ContainerRegistry|Expiration policy will run in %{time}', ); export const EXPIRATION_POLICY_DISABLED_TEXT = s__( - 'ContainerRegistry|Expiration policy is disabled', + 'ContainerRegistry|Expiration policy is disabled.', ); export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted'); export const DELETE_ALERT_LINK_TEXT = s__( @@ -13,3 +13,4 @@ export const DELETE_ALERT_LINK_TEXT = s__( export const CLEANUP_TIMED_OUT_ERROR_MESSAGE = s__( 'ContainerRegistry|Cleanup timed out before it could delete all tags', ); +export const SET_UP_CLEANUP = s__('ContainerRegistry|Set up cleanup'); diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js index ca5bd8d6964..a558550c91f 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js @@ -35,7 +35,7 @@ export default () => { expirationPolicy, isGroupPage, isAdmin, - showCleanupPolicyOnAlert, + showCleanupPolicyLink, showUnfinishedTagCleanupCallout, connectionError, invalidPathError, @@ -68,7 +68,7 @@ export default () => { expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined, isGroupPage: parseBoolean(isGroupPage), isAdmin: parseBoolean(isAdmin), - showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert), + showCleanupPolicyLink: parseBoolean(showCleanupPolicyLink), showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout), connectionError: parseBoolean(connectionError), invalidPathError: parseBoolean(invalidPathError), diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue index 5f9e614bebb..d1cab406984 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue @@ -11,7 +11,6 @@ import { import { get } from 'lodash'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; import createFlash from '~/flash'; -import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import Tracking from '~/tracking'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; @@ -60,7 +59,6 @@ export default { GlSkeletonLoader, RegistryHeader, DeleteImage, - CleanupPolicyEnabledAlert, PersistedSearch, }, directives: { @@ -273,12 +271,6 @@ export default { </gl-sprintf> </gl-alert> - <cleanup-policy-enabled-alert - v-if="config.showCleanupPolicyOnAlert" - :project-path="config.projectPath" - :cleanup-policies-settings-path="config.cleanupPoliciesSettingsPath" - /> - <gl-empty-state v-if="showConnectionError" :title="$options.i18n.CONNECTION_ERROR_TITLE" @@ -304,6 +296,8 @@ export default { :expiration-policy="config.expirationPolicy" :help-page-path="config.helpPagePath" :hide-expiration-policy-data="config.isGroupPage" + :cleanup-policies-settings-path="config.cleanupPoliciesSettingsPath" + :show-cleanup-policy-link="config.showCleanupPolicyLink" > <template #commands> <cli-commands diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index 7be3bba7cae..854c88b2ad3 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -9,7 +9,6 @@ import { UNAVAILABLE_ADMIN_FEATURE_TEXT, } from '~/packages_and_registries/settings/project/constants'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; -import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; import SettingsForm from './settings_form.vue'; @@ -18,19 +17,11 @@ export default { components: { SettingsBlock, SettingsForm, - CleanupPolicyEnabledAlert, GlAlert, GlSprintf, GlLink, }, - inject: [ - 'projectPath', - 'isAdmin', - 'adminSettingsPath', - 'enableHistoricEntries', - 'helpPagePath', - 'showCleanupPolicyOnAlert', - ], + inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'], i18n: { UNAVAILABLE_FEATURE_TITLE, UNAVAILABLE_FEATURE_INTRO_TEXT, @@ -87,7 +78,6 @@ export default { <template> <section data-testid="registry-settings-app"> - <cleanup-policy-enabled-alert v-if="showCleanupPolicyOnAlert" :project-path="projectPath" /> <settings-block :collapsible="false"> <template #title> {{ __('Clean up image tags') }}</template> <template #description> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js index 2a3e2c28fa6..17c33073668 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js @@ -20,7 +20,6 @@ export default () => { adminSettingsPath, tagsRegexHelpPagePath, helpPagePath, - showCleanupPolicyOnAlert, } = el.dataset; return new Vue({ el, @@ -35,7 +34,6 @@ export default () => { adminSettingsPath, tagsRegexHelpPagePath, helpPagePath, - showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert), }, render(createElement) { return createElement('registry-settings-app', {}); diff --git a/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue b/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue deleted file mode 100644 index d51c62e0623..00000000000 --- a/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue +++ /dev/null @@ -1,54 +0,0 @@ -<script> -import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; - -export default { - components: { - GlAlert, - GlLink, - GlSprintf, - LocalStorageSync, - }, - props: { - projectPath: { - type: String, - required: true, - }, - cleanupPoliciesSettingsPath: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - dismissed: false, - }; - }, - computed: { - storageKey() { - return `cleanup_policy_enabled_for_project_${this.projectPath}`; - }, - }, - i18n: { - message: s__( - 'ContainerRegistry|Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}', - ), - }, -}; -</script> - -<template> - <local-storage-sync v-model="dismissed" :storage-key="storageKey"> - <gl-alert v-if="!dismissed" class="gl-mt-2" dismissible @dismiss="dismissed = true"> - <gl-sprintf :message="$options.i18n.message"> - <template #link="{ content }"> - <gl-link v-if="cleanupPoliciesSettingsPath" :href="cleanupPoliciesSettingsPath">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </gl-alert> - </local-storage-sync> -</template> diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index 88ff05b20a0..5dae812bbcb 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -33,13 +33,7 @@ function initIntervalPatternInput() { } function getEnabledRefTypes() { - const refTypes = [REF_TYPE_BRANCHES]; - - if (gon.features.pipelineSchedulesWithTags) { - refTypes.push(REF_TYPE_TAGS); - } - - return refTypes; + return [REF_TYPE_BRANCHES, REF_TYPE_TAGS]; } function initTargetRefDropdown() { @@ -61,9 +55,7 @@ function initTargetRefDropdown() { value: $refField.value, useSymbolicRefNames: true, translations: { - dropdownHeader: gon.features.pipelineSchedulesWithTags - ? __('Select target branch or tag') - : __('Select target branch'), + dropdownHeader: __('Select target branch or tag'), }, }, class: 'gl-w-full', diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss index f91ca489bdf..eecd4954e39 100644 --- a/app/assets/stylesheets/page_bundles/boards.scss +++ b/app/assets/stylesheets/page_bundles/boards.scss @@ -198,7 +198,7 @@ border-bottom: 1px solid var(--gray-100, $gray-100); height: 3rem; - .js-max-issue-size::before { + .max-issue-size::before { content: '/'; } } diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 641b3adb12b..e223e2a914d 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -43,8 +43,6 @@ class Groups::BoardsController < Groups::ApplicationController def assign_endpoint_vars @boards_endpoint = group_boards_path(group) - @namespace_path = group.to_param - @labels_endpoint = group_labels_path(group) end def authorize_read_board! diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 7ad3a2ee358..936ecc171d7 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -70,7 +70,7 @@ class Import::BaseController < ApplicationController end def already_added_projects - @already_added_projects ||= filtered(find_already_added_projects(provider_name)) + @already_added_projects ||= find_already_added_projects(provider_name) end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index c44a0830e2e..b82e60e9ac7 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -44,8 +44,6 @@ class Projects::BoardsController < Projects::ApplicationController def assign_endpoint_vars @boards_endpoint = project_boards_path(project) @bulk_issues_path = bulk_update_project_issues_path(project) - @namespace_path = project.namespace.full_path - @labels_endpoint = project_labels_path(project) end def authorize_read_board! diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 271c31b6429..ac94cc001dd 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -10,10 +10,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] - before_action do - push_frontend_feature_flag(:pipeline_schedules_with_tags, @project, default_enabled: :yaml) - end - feature_category :continuous_integration # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 7b4d87a77e4..f849f36bf84 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -127,20 +127,6 @@ module BoardsHelper can?(current_user, :admin_issue, current_board_parent) end - def board_list_data - include_descendant_groups = @group&.present? - - { - toggle: "dropdown", - list_labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_ancestor_groups: true), - labels: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: include_descendant_groups), - labels_endpoint: @labels_endpoint, - namespace_path: @namespace_path, - project_path: @project&.path, - group_path: @group&.path - } - end - def serializer CurrentBoardSerializer.new end diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb index 01075862618..20d40626449 100644 --- a/app/helpers/packages_helper.rb +++ b/app/helpers/packages_helper.rb @@ -46,7 +46,7 @@ module PackagesHelper ::Gitlab::Tracking.event(category, event_name.to_s, **args) end - def show_cleanup_policy_on_alert(project) + def show_cleanup_policy_link(project) Gitlab.com? && Gitlab.config.registry.enabled && project.feature_available?(:container_registry, current_user) && diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb index b4b9574942e..d5cbbb96134 100644 --- a/app/models/ci/namespace_mirror.rb +++ b/app/models/ci/namespace_mirror.rb @@ -11,8 +11,6 @@ module Ci end scope :contains_any_of_namespaces, -> (ids) do - return none if ids.empty? - where('traversal_ids && ARRAY[?]::int[]', ids) end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 03ac5c8e5df..28b4b0dcca1 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -174,7 +174,7 @@ class ContainerRepository < ApplicationRecord end end - before_transition %i[importing import_aborted] => :import_done do |container_repository| + before_transition any => :import_done do |container_repository| container_repository.migration_import_done_at = Time.zone.now end diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb index 42914b0d668..fabbd5b49cb 100644 --- a/app/models/project_import_state.rb +++ b/app/models/project_import_state.rb @@ -6,6 +6,8 @@ class ProjectImportState < ApplicationRecord self.table_name = "project_mirror_data" + after_commit :expire_etag_cache + belongs_to :project, inverse_of: :import_state validates :project, presence: true @@ -76,6 +78,23 @@ class ProjectImportState < ApplicationRecord end end + def expire_etag_cache + if realtime_changes_path + Gitlab::EtagCaching::Store.new.tap do |store| + store.touch(realtime_changes_path) + rescue Gitlab::EtagCaching::Store::InvalidKeyError + # no-op: not every realtime changes endpoint is using etag caching + end + end + end + + def realtime_changes_path + Gitlab::Routing.url_helpers.polymorphic_path([:realtime_changes_import, project.import_type.to_sym], format: :json) + rescue NoMethodError + # polymorphic_path throws NoMethodError when no such path exists + nil + end + def relation_hard_failures(limit:) project.import_failures.hard_failures_by_correlation_id(correlation_id).limit(limit) end diff --git a/app/models/user.rb b/app/models/user.rb index c683f5dd252..281e52b90b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2220,27 +2220,43 @@ class User < ApplicationRecord end def ci_owned_project_runners_from_project_members - Ci::RunnerProject - .select('ci_runners.*') - .joins(:runner) - .where(project: project_members.where('access_level >= ?', Gitlab::Access::MAINTAINER).pluck(:source_id)) + project_ids = project_members.where('access_level >= ?', Gitlab::Access::MAINTAINER).pluck(:source_id) + + Ci::Runner + .joins(:runner_projects) + .where(runner_projects: { project: project_ids }) end def ci_owned_project_runners_from_group_members - Ci::RunnerProject - .select('ci_runners.*') - .joins(:runner) - .joins('JOIN ci_project_mirrors ON ci_project_mirrors.project_id = ci_runner_projects.project_id') - .joins('JOIN ci_namespace_mirrors ON ci_namespace_mirrors.namespace_id = ci_project_mirrors.namespace_id') - .merge(ci_namespace_mirrors_for_group_members(Gitlab::Access::MAINTAINER)) + cte_namespace_ids = Gitlab::SQL::CTE.new( + :cte_namespace_ids, + ci_namespace_mirrors_for_group_members(Gitlab::Access::MAINTAINER).select(:namespace_id) + ) + + cte_project_ids = Gitlab::SQL::CTE.new( + :cte_project_ids, + Ci::ProjectMirror + .select(:project_id) + .where('ci_project_mirrors.namespace_id IN (SELECT namespace_id FROM cte_namespace_ids)') + ) + + Ci::Runner + .with(cte_namespace_ids.to_arel) + .with(cte_project_ids.to_arel) + .joins(:runner_projects) + .where('ci_runner_projects.project_id IN (SELECT project_id FROM cte_project_ids)') end def ci_owned_group_runners - Ci::RunnerNamespace - .select('ci_runners.*') - .joins(:runner) - .joins('JOIN ci_namespace_mirrors ON ci_namespace_mirrors.namespace_id = ci_runner_namespaces.namespace_id') - .merge(ci_namespace_mirrors_for_group_members(Gitlab::Access::OWNER)) + cte_namespace_ids = Gitlab::SQL::CTE.new( + :cte_namespace_ids, + ci_namespace_mirrors_for_group_members(Gitlab::Access::OWNER).select(:namespace_id) + ) + + Ci::Runner + .with(cte_namespace_ids.to_arel) + .joins(:runner_namespaces) + .where('ci_runner_namespaces.namespace_id IN (SELECT namespace_id FROM cte_namespace_ids)') end def ci_namespace_mirrors_for_group_members(level) diff --git a/app/services/incident_management/issuable_escalation_statuses/build_service.rb b/app/services/incident_management/issuable_escalation_statuses/build_service.rb new file mode 100644 index 00000000000..9ebcf72a0c9 --- /dev/null +++ b/app/services/incident_management/issuable_escalation_statuses/build_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module IncidentManagement + module IssuableEscalationStatuses + class BuildService < ::BaseProjectService + def initialize(issue) + @issue = issue + @alert = issue.alert_management_alert + + super(project: issue.project) + end + + def execute + return issue.escalation_status if issue.escalation_status + + issue.build_incident_management_issuable_escalation_status(alert_params) + end + + private + + attr_reader :issue, :alert + + def alert_params + return {} unless alert + + { + status_event: alert.status_event_for(alert.status_name) + } + end + end + end +end + +IncidentManagement::IssuableEscalationStatuses::BuildService.prepend_mod diff --git a/app/services/incident_management/issuable_escalation_statuses/create_service.rb b/app/services/incident_management/issuable_escalation_statuses/create_service.rb index e28debf0fa3..9b22fb97e0d 100644 --- a/app/services/incident_management/issuable_escalation_statuses/create_service.rb +++ b/app/services/incident_management/issuable_escalation_statuses/create_service.rb @@ -2,14 +2,15 @@ module IncidentManagement module IssuableEscalationStatuses - class CreateService < BaseService + class CreateService < ::BaseProjectService def initialize(issue) @issue = issue - @alert = issue.alert_management_alert + + super(project: issue.project) end def execute - escalation_status = ::IncidentManagement::IssuableEscalationStatus.new(issue: issue, **alert_params) + escalation_status = BuildService.new(issue).execute if escalation_status.save ServiceResponse.success(payload: { escalation_status: escalation_status }) @@ -20,17 +21,7 @@ module IncidentManagement private - attr_reader :issue, :alert - - def alert_params - return {} unless alert - - { - status_event: alert.status_event_for(alert.status_name) - } - end + attr_reader :issue end end end - -IncidentManagement::IssuableEscalationStatuses::CreateService.prepend_mod diff --git a/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb b/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb index 8f591b375ee..1d0504a6e80 100644 --- a/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb +++ b/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb @@ -31,9 +31,7 @@ module IncidentManagement attr_reader :issuable, :param_errors def available? - issuable.supports_escalation? && - user_has_permissions? && - escalation_status.present? + issuable.supports_escalation? && user_has_permissions? end def user_has_permissions? @@ -42,7 +40,7 @@ module IncidentManagement def escalation_status strong_memoize(:escalation_status) do - issuable.escalation_status + issuable.escalation_status || BuildService.new(issuable).execute end end diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 0818c3d5cff..e00d8e2c050 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -15,7 +15,7 @@ = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group.row .col-md-9 - = f.label :ref, Feature.enabled?(:pipeline_schedules_with_tags, default_enabled: :yaml) ? _('Target branch or tag') : _('Target branch'), class: 'label-bold' + = f.label :ref, _('Target branch or tag'), class: 'label-bold' %div{ data: { testid: 'schedule-target-ref' } } .js-target-ref-dropdown{ data: { project_id: @project.id, default_branch: @project.default_branch } } = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index aab5e9fca98..51f0b6319a1 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -19,7 +19,7 @@ "project_path": @project.full_path, "gid_prefix": container_repository_gid_prefix, "is_admin": current_user&.admin.to_s, - "show_cleanup_policy_on_alert": show_cleanup_policy_on_alert(@project).to_s, + "show_cleanup_policy_link": show_cleanup_policy_link(@project).to_s, "cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project), connection_error: (!!@connection_error).to_s, invalid_path_error: (!!@invalid_path_error).to_s, diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml index 658b2f2e65c..378bb0f9306 100644 --- a/app/views/projects/settings/packages_and_registries/show.html.haml +++ b/app/views/projects/settings/packages_and_registries/show.html.haml @@ -11,5 +11,5 @@ admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'), enable_historic_entries: container_expiration_policies_historic_entry_enabled?.to_s, help_page_path: help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'), - show_cleanup_policy_on_alert: show_cleanup_policy_on_alert(@project).to_s, + show_cleanup_policy_link: show_cleanup_policy_link(@project).to_s, tags_regex_help_page_path: help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'regex-pattern-examples') } } diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml deleted file mode 100644 index c667b3a4626..00000000000 --- a/app/views/shared/boards/_switcher.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- parent = board.resource_parent -- milestone_filter_opts = { format: :json } -- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board? -- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : [] - -#js-multiple-boards-switcher.inline.boards-switcher{ data: { milestone_path: milestones_filter_path(milestone_filter_opts), - board_base_url: board_base_url, - has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s, - can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, - multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, - scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false', - weights: weights.to_json } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 37a79a50fb1..7fdf8ea7796 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,22 +1,12 @@ - type = local_assigns.fetch(:type) -- board = local_assigns.fetch(:board, nil) - show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true) - disable_target_branch = local_assigns.fetch(:disable_target_branch, false) - placeholder = local_assigns[:placeholder] || _('Search or filter results...') - block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : '' -- is_epic_board = board&.to_type == "EpicBoard" - -- if is_epic_board - - user_can_admin_list = can?(current_user, :admin_epic_board_list, board.resource_parent) -- elsif board - - user_can_admin_list = can?(current_user, :admin_issue_board_list, board.resource_parent) .issues-filters .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class } .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100 - - if type == :boards - = render "shared/boards/switcher", board: board - .js-new-board{ data: { multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, } } = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do - if params[:search].present? = hidden_field_tag :search, params[:search] @@ -25,201 +15,188 @@ - checkbox_id = 'check-all-issues' %label.gl-sr-only{ for: checkbox_id }= _('Select all') = check_box_tag checkbox_id, nil, false, class: "check-all-issues left" - - if is_epic_board - #js-board-filtered-search{ data: { full_path: @group&.full_path } } - - elsif board - #js-issue-board-filtered-search - - else - .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row - .filtered-search-box - - if type != :boards - - text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline") - = dropdown_tag(text, - options: { wrapper_class: "filtered-search-history-dropdown-wrapper", - toggle_class: "gl-button btn btn-default filtered-search-history-dropdown-toggle-button", - dropdown_class: "filtered-search-history-dropdown", - content_class: "filtered-search-history-dropdown-content" }) do - .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } } - .filtered-search-box-input-container.droplab-dropdown - .scroll-container - %ul.tokens-container.list-unstyled - %li.input-token - %input.form-control.filtered-search{ search_filter_input_options(type, placeholder) } - #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } } - %button.gl-button.btn.btn-link{ type: 'button' } - -# Encapsulate static class name `{{icon}}` inside #{} to bypass - -# haml lint's ClassAttributeWithStaticValue - %svg - %use{ 'xlink:href': "#{'{{icon}}'}" } - %span.js-filter-hint - {{formattedKey}} - #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } } - %li.filter-dropdown-item{ data: { value: "{{ title }}" } } - %button.gl-button.btn.btn-link{ type: 'button' } - {{ title }} - %span.btn-helptext - {{ help }} - #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu + .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row + .filtered-search-box + - if type != :boards + - text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline") + = dropdown_tag(text, + options: { wrapper_class: "filtered-search-history-dropdown-wrapper", + toggle_class: "gl-button btn btn-default filtered-search-history-dropdown-toggle-button", + dropdown_class: "filtered-search-history-dropdown", + content_class: "filtered-search-history-dropdown-content" }) do + .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } } + .filtered-search-box-input-container.droplab-dropdown + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ search_filter_input_options(type, placeholder) } + #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } } + %button.gl-button.btn.btn-link{ type: 'button' } + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %svg + %use{ 'xlink:href': "#{'{{icon}}'}" } + %span.js-filter-hint + {{formattedKey}} + #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } } + %li.filter-dropdown-item{ data: { value: "{{ title }}" } } + %button.gl-button.btn.btn-link{ type: 'button' } + {{ title }} + %span.btn-helptext + {{ help }} + #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu + - if current_user + %ul{ data: { dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: current_user + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: User.new(username: '{{username}}', name: '{{name}}'), + avatar: { lazy: true, url: '{{avatar_url}}' } + #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore - if current_user - %ul{ data: { dropdown: true } } - = render 'shared/issuable/user_dropdown_item', - user: current_user - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } = render 'shared/issuable/user_dropdown_item', - user: User.new(username: '{{username}}', name: '{{name}}'), - avatar: { lazy: true, url: '{{avatar_url}}' } - #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - - if current_user - = render 'shared/issuable/user_dropdown_item', - user: current_user - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + user: current_user + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: User.new(username: '{{username}}', name: '{{name}}'), + avatar: { lazy: true, url: '{{avatar_url}}' } + #js-dropdown-reviewer.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + - if current_user = render 'shared/issuable/user_dropdown_item', - user: User.new(username: '{{username}}', name: '{{name}}'), - avatar: { lazy: true, url: '{{avatar_url}}' } - #js-dropdown-reviewer.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - - if current_user + user: current_user + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: User.new(username: '{{username}}', name: '{{name}}'), + avatar: { lazy: true, url: '{{avatar_url}}' } + - if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml) + #js-dropdown-attention-requested.filtered-search-input-dropdown-menu.dropdown-menu + - if current_user + %ul{ data: { dropdown: true } } = render 'shared/issuable/user_dropdown_item', user: current_user %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } = render 'shared/issuable/user_dropdown_item', user: User.new(username: '{{username}}', name: '{{name}}'), avatar: { lazy: true, url: '{{avatar_url}}' } - - if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml) - #js-dropdown-attention-requested.filtered-search-input-dropdown-menu.dropdown-menu - - if current_user - %ul{ data: { dropdown: true } } - = render 'shared/issuable/user_dropdown_item', - user: current_user - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - = render 'shared/issuable/user_dropdown_item', - user: User.new(username: '{{username}}', name: '{{name}}'), - avatar: { lazy: true, url: '{{avatar_url}}' } - = render_if_exists 'shared/issuable/approver_dropdown' - = render_if_exists 'shared/issuable/approved_by_dropdown' - #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Any') - %li.filter-dropdown-item{ data: { value: 'Upcoming' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Upcoming') - %li.filter-dropdown-item{ data: { value: 'Started' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Started') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.gl-button.btn.btn-link.js-data-value{ type: 'button' } + = render_if_exists 'shared/issuable/approver_dropdown' + = render_if_exists 'shared/issuable/approved_by_dropdown' + #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Any') + %li.filter-dropdown-item{ data: { value: 'Upcoming' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Upcoming') + %li.filter-dropdown-item{ data: { value: 'Started' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Started') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.gl-button.btn.btn-link.js-data-value{ type: 'button' } + {{title}} + = render_if_exists 'shared/issuable/filter_iteration', type: type + #js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.gl-button.btn.btn-link.js-data-value{ type: 'button' } + {{title}} + #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.gl-button.btn.btn-link{ type: 'button' } + %span.dropdown-label-box{ style: 'background: {{color}}' } + %span.label-title.js-data-value {{title}} - = render_if_exists 'shared/issuable/filter_iteration', type: type - #js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.gl-button.btn.btn-link.js-data-value{ type: 'button' } - {{title}} - #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.gl-button.btn.btn-link{ type: 'button' } - %span.dropdown-label-box{ style: 'background: {{color}}' } - %span.label-title.js-data-value - {{title}} - #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.gl-button.btn.btn-link{ type: 'button' } - %gl-emoji - %span.js-data-value.gl-ml-3 - {{name}} - #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Yes') - %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('No') - #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Yes') - %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('No') - - unless disable_target_branch - #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.gl-button.btn.btn-link.js-data-value.monospace - {{title}} - #js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu + #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.gl-button.btn.btn-link{ type: 'button' } + %gl-emoji + %span.js-data-value.gl-ml-3 + {{name}} + #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('No') + #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('No') + - unless disable_target_branch + #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.gl-button.btn.btn-link.js-data-value{ type: 'button' } + %button.gl-button.btn.btn-link.js-data-value.monospace {{title}} + #js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.gl-button.btn.btn-link.js-data-value{ type: 'button' } + {{title}} - = render_if_exists 'shared/issuable/filter_weight', type: type + = render_if_exists 'shared/issuable/filter_weight', type: type - = render_if_exists 'shared/issuable/filter_epic', type: type + = render_if_exists 'shared/issuable/filter_epic', type: type - %button.clear-search.hidden{ type: 'button' } - = sprite_icon('close', size: 16, css_class: 'clear-search-icon') + %button.clear-search.hidden{ type: 'button' } + = sprite_icon('close', size: 16, css_class: 'clear-search-icon') .filter-dropdown-container.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-align-items-flex-start - - if type == :boards - #js-board-labels-toggle - - if current_user - #js-board-epics-swimlanes-toggle - .js-board-config{ data: { can_admin_list: user_can_admin_list.to_s, has_scope: board.scoped?.to_s } } - - if user_can_admin_list - .js-create-column-trigger{ data: board_list_data } - #js-toggle-focus-btn - - elsif type != :productivity_analytics && show_sorting_dropdown + - if type != :productivity_analytics && show_sorting_dropdown = render 'shared/issuable/sort_dropdown' diff --git a/config/feature_flags/development/pipeline_schedules_with_tags.yml b/config/feature_flags/development/deployment_approval_rules.yml index 9eb7b60d300..31b235053c3 100644 --- a/config/feature_flags/development/pipeline_schedules_with_tags.yml +++ b/config/feature_flags/development/deployment_approval_rules.yml @@ -1,8 +1,8 @@ --- -name: pipeline_schedules_with_tags -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81476 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354421 -milestone: '14.9' +name: deployment_approval_rules +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83495 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354726 +milestone: '14.10' type: development -group: group::pipeline execution -default_enabled: true +group: group::release +default_enabled: false diff --git a/db/post_migrate/20220323130000_add_temp_index_on_null_project_namespace_ids.rb b/db/post_migrate/20220323130000_add_temp_index_on_null_project_namespace_ids.rb new file mode 100644 index 00000000000..e73993065b0 --- /dev/null +++ b/db/post_migrate/20220323130000_add_temp_index_on_null_project_namespace_ids.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddTempIndexOnNullProjectNamespaceIds < Gitlab::Database::Migration[1.0] + TMP_INDEX_FOR_NULL_PROJECT_NAMESPACE_ID = 'tmp_index_for_null_project_namespace_id' + + disable_ddl_transaction! + + def up + add_concurrent_index :projects, :id, name: TMP_INDEX_FOR_NULL_PROJECT_NAMESPACE_ID, where: 'project_namespace_id IS NULL' + end + + def down + remove_concurrent_index_by_name :projects, name: TMP_INDEX_FOR_NULL_PROJECT_NAMESPACE_ID + end +end diff --git a/db/schema_migrations/20220323130000 b/db/schema_migrations/20220323130000 new file mode 100644 index 00000000000..a370ab82750 --- /dev/null +++ b/db/schema_migrations/20220323130000 @@ -0,0 +1 @@ +6152ff1a3647e1ac26e8f462215a0dec7d96c5552373f4ad99424c7c74e80577
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fbae8724a83..5689a7b4f60 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29634,6 +29634,8 @@ CREATE INDEX tmp_index_for_namespace_id_migration_on_group_members ON members US CREATE INDEX tmp_index_for_namespace_id_migration_on_routes ON routes USING btree (id) WHERE ((namespace_id IS NULL) AND ((source_type)::text = 'Namespace'::text)); +CREATE INDEX tmp_index_for_null_project_namespace_id ON projects USING btree (id) WHERE (project_namespace_id IS NULL); + CREATE INDEX tmp_index_issues_on_issue_type_and_id ON issues USING btree (issue_type, id); CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2); diff --git a/doc/.vale/gitlab/SubstitutionWarning.yml b/doc/.vale/gitlab/SubstitutionWarning.yml index 84d1bab3cc6..7b20887f53f 100644 --- a/doc/.vale/gitlab/SubstitutionWarning.yml +++ b/doc/.vale/gitlab/SubstitutionWarning.yml @@ -11,6 +11,7 @@ link: https://about.gitlab.com/handbook/communication/#top-misused-terms level: warning ignorecase: true swap: + air(?:-| )?gapped: offline environment click: select code base: codebase config: configuration diff --git a/doc/ci/runners/runners_scope.md b/doc/ci/runners/runners_scope.md index aa7e268e800..fb5395076b0 100644 --- a/doc/ci/runners/runners_scope.md +++ b/doc/ci/runners/runners_scope.md @@ -143,6 +143,8 @@ Group runners process jobs by using a first in, first out ([FIFO](https://en.wik ### Create a group runner +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/19819) in GitLab 14.10, path changed from **Settings > CI/CD > Runners**. + You can create a group runner for your self-managed GitLab instance or for GitLab.com. You must have the Owner role for the group. @@ -150,7 +152,7 @@ To create a group runner: 1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/). 1. Go to the group you want to make the runner work for. -1. Go to **Settings > CI/CD** and expand the **Runners** section. +1. On the left sidebar, select **CI/CD > Runners**. 1. Note the URL and token. 1. [Register the runner](https://docs.gitlab.com/runner/register/). @@ -163,7 +165,7 @@ You can do this for your self-managed GitLab instance or for GitLab.com. You must have the Owner role for the group. 1. Go to the group where you want to view the runners. -1. Go to **Settings > CI/CD** and expand the **Runners** section. +1. On the left sidebar, select **CI/CD > Runners**. 1. The following fields are displayed. | Attribute | Description | @@ -186,7 +188,7 @@ You can pause or remove a group runner for your self-managed GitLab instance or You must have the Owner role for the group. 1. Go to the group you want to remove or pause the runner for. -1. Go to **Settings > CI/CD** and expand the **Runners** section. +1. On the left sidebar, select **CI/CD > Runners**. 1. Click **Pause** or **Remove runner**. - If you pause a group runner that is used by multiple projects, the runner pauses for all projects. - From the group view, you cannot remove a runner that is assigned to more than one project. diff --git a/doc/development/diffs.md b/doc/development/diffs.md index d61de740f15..5f03ba93a4d 100644 --- a/doc/development/diffs.md +++ b/doc/development/diffs.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Code Review info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index dcb9a5f7362..65f6a0a328b 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -97,6 +97,12 @@ The token generated when you create an agent for Kubernetes. Use **agent access - secret token - authentication token +## air gap, air-gapped + +Use **offline environment** to describe installations that have physical barriers or security policies that prevent or limit internet access. Do not use **air gap**, **air gapped**, or **air-gapped**. For example: + +- The firewall policies in an offline environment prevent the computer from accessing the internet. + ## allow, enable Try to avoid **allow** and **enable**, unless you are talking about security-related features. @@ -1023,7 +1029,7 @@ Do not use **yet** when talking about the product or its features. The documenta Sometimes you might need to use **yet** when writing a task. If you use **yet**, ensure the surrounding phrases are written -in present tense, active voice. +in present tense, active voice. [View guidance about how to write about future features](index.md#promising-features-in-future-versions). ([Vale](../testing.md#vale) rule: [`CurrentStatus.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/CurrentStatus.yml)) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 76097e4f43a..2cddbaa9723 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -49,9 +49,16 @@ graph TD ## Create a subgroup -Users with the at least the Maintainer role on a group can create subgroups immediately below the group, unless -[configured otherwise](#change-who-can-create-subgroups). These users can create subgroups even if group creation is -[disabled by an Administrator](../../admin_area/index.md#prevent-a-user-from-creating-groups) in the user's settings. +Prerequisites: + +- You must either: + - Have at least the Maintainer role for a group to create subgroups for it. + - Have the [role determined by a setting](#change-who-can-create-subgroups). These users can create + subgroups even if group creation is + [disabled by an Administrator](../../admin_area/index.md#prevent-a-user-from-creating-groups) in the user's settings. + +NOTE: +You cannot host a GitLab Pages subgroup website with a top-level domain name. For example, `subgroupname.example.io`. To create a subgroup: diff --git a/lib/gitlab/ci/runner_upgrade_check.rb b/lib/gitlab/ci/runner_upgrade_check.rb new file mode 100644 index 00000000000..baf041fc358 --- /dev/null +++ b/lib/gitlab/ci/runner_upgrade_check.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class RunnerUpgradeCheck + include Singleton + + def initialize + reset! + end + + def check_runner_upgrade_status(runner_version) + return :unknown unless runner_version + + releases = RunnerReleases.instance.releases + parsed_runner_version = runner_version.is_a?(::Gitlab::VersionInfo) ? runner_version : ::Gitlab::VersionInfo.parse(runner_version) + + raise ArgumentError, "'#{runner_version}' is not a valid version" unless parsed_runner_version.valid? + + available_releases = releases.reject { |release| release > @gitlab_version } + + return :recommended if available_releases.any? { |available_release| patch_update?(available_release, parsed_runner_version) } + return :recommended if outside_backport_window?(parsed_runner_version, releases) + return :available if available_releases.any? { |available_release| available_release > parsed_runner_version } + + :not_available + end + + def reset! + @gitlab_version = ::Gitlab::VersionInfo.parse(::Gitlab::VERSION) + end + + public_class_method :instance + + private + + def patch_update?(available_release, runner_version) + # https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases + available_release.major == runner_version.major && + available_release.minor == runner_version.minor && + available_release.patch > runner_version.patch + end + + def outside_backport_window?(runner_version, releases) + return false if runner_version >= releases.last # return early if runner version is too new + + latest_minor_releases = releases.map { |r| version_without_patch(r) }.uniq { |v| v.to_s } + latest_version_position = latest_minor_releases.count - 1 + runner_version_position = latest_minor_releases.index(version_without_patch(runner_version)) + + return true if runner_version_position.nil? # consider outside if version is too old + + # https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases + latest_version_position - runner_version_position > 2 + end + + def version_without_patch(version) + ::Gitlab::VersionInfo.new(version.major, version.minor, 0) + end + end + end +end diff --git a/lib/gitlab/github_import/object_counter.rb b/lib/gitlab/github_import/object_counter.rb index 7ce88280209..8873db24118 100644 --- a/lib/gitlab/github_import/object_counter.rb +++ b/lib/gitlab/github_import/object_counter.rb @@ -24,6 +24,8 @@ module Gitlab increment_project_counter(project, object_type, operation, integer) increment_global_counter(object_type, operation, integer) + + project.import_state&.expire_etag_cache end def summary(project) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dd6dab41ca8..0d9c0c70d1e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9678,9 +9678,6 @@ msgstr "" msgid "ContainerRegistry|Cleanup pending" msgstr "" -msgid "ContainerRegistry|Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}" -msgstr "" - msgid "ContainerRegistry|Cleanup policy for tags is disabled" msgstr "" @@ -9738,7 +9735,7 @@ msgstr "" msgid "ContainerRegistry|Enable expiration policy" msgstr "" -msgid "ContainerRegistry|Expiration policy is disabled" +msgid "ContainerRegistry|Expiration policy is disabled." msgstr "" msgid "ContainerRegistry|Expiration policy will run in %{time}" @@ -9833,6 +9830,9 @@ msgstr "" msgid "ContainerRegistry|Run cleanup:" msgstr "" +msgid "ContainerRegistry|Set up cleanup" +msgstr "" + msgid "ContainerRegistry|Some tags were not deleted" msgstr "" diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index c3a4f49daac..fe03039ddcc 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -258,7 +258,9 @@ RSpec.describe Import::GithubController do context 'when user input contains colons and spaces' do before do - allow(controller).to receive(:client_repos).and_return([]) + allow_next_instance_of(Gitlab::GithubImport::Client) do |client| + allow(client).to receive(:search_repos_by_name).and_return(items: []) + end end it 'sanitizes user input' do diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 26804b38db8..e897a5e022a 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -65,11 +65,12 @@ FactoryBot.define do transient do merged_by { author } + merged_at { nil } end after(:build) do |merge_request, evaluator| metrics = merge_request.build_metrics - metrics.merged_at = 1.week.from_now + metrics.merged_at = evaluator.merged_at || 1.week.from_now metrics.merged_by = evaluator.merged_by metrics.pipeline = create(:ci_empty_pipeline) end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 5dd627f3b76..bf976168bbe 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -282,7 +282,7 @@ RSpec.describe 'Project issue boards', :js do it 'shows issue count on the list' do page.within(find(".board:nth-child(2)")) do expect(page.find('[data-testid="board-items-count"]')).to have_text(total_planning_issues) - expect(page).not_to have_selector('.js-max-issue-size') + expect(page).not_to have_selector('.max-issue-size') end end end diff --git a/spec/features/boards/focus_mode_spec.rb b/spec/features/boards/focus_mode_spec.rb index 2bd1e625236..453a8d8870b 100644 --- a/spec/features/boards/focus_mode_spec.rb +++ b/spec/features/boards/focus_mode_spec.rb @@ -12,6 +12,6 @@ RSpec.describe 'Issue Boards focus mode', :js do end it 'shows focus mode button to anonymous users' do - expect(page).to have_selector('.js-focus-mode-btn') + expect(page).to have_button _('Toggle focus mode') end end diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb index 9148fb23214..cad303a14e5 100644 --- a/spec/features/boards/multi_select_spec.rb +++ b/spec/features/boards/multi_select_spec.rb @@ -72,7 +72,7 @@ RSpec.describe 'Multi Select Issue', :js do wait_for_requests - page.within(all('.js-board-list')[2]) do + page.within(all('.board-list')[2]) do expect(find('.board-card:nth-child(1)')).to have_content(issue1.title) expect(find('.board-card:nth-child(2)')).to have_content(issue2.title) end @@ -87,7 +87,7 @@ RSpec.describe 'Multi Select Issue', :js do wait_for_requests - page.within(all('.js-board-list')[2]) do + page.within(all('.board-list')[2]) do expect(find('.board-card:nth-child(1)')).to have_content(issue1.title) expect(find('.board-card:nth-child(2)')).to have_content(issue2.title) expect(find('.board-card:nth-child(3)')).to have_content(issue3.title) @@ -102,7 +102,7 @@ RSpec.describe 'Multi Select Issue', :js do wait_for_requests - page.within(all('.js-board-list')[1]) do + page.within(all('.board-list')[1]) do expect(find('.board-card:nth-child(1)')).to have_content(issue1.title) expect(find('.board-card:nth-child(2)')).to have_content(issue2.title) expect(find('.board-card:nth-child(3)')).to have_content(issue5.title) diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 717216a9882..f60d04af4fc 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -1,5 +1,4 @@ import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; @@ -14,6 +13,7 @@ import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.g import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql'; import defaultStore from '~/boards/stores'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mockGroupBoardResponse, mockProjectBoardResponse, @@ -60,7 +60,7 @@ describe('BoardsSelector', () => { searchBoxInput.trigger('input'); }; - const getDropdownItems = () => wrapper.findAll('.js-dropdown-item'); + const getDropdownItems = () => wrapper.findAllByTestId('dropdown-item'); const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader); const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findDropdown = () => wrapper.findComponent(GlDropdown); @@ -100,7 +100,7 @@ describe('BoardsSelector', () => { [groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess], ]); - wrapper = mount(BoardsSelector, { + wrapper = mountExtended(BoardsSelector, { store, apolloProvider: fakeApollo, propsData: { diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js index 635964b6b4a..948a7a20f7f 100644 --- a/spec/frontend/boards/components/issue_time_estimate_spec.js +++ b/spec/frontend/boards/components/issue_time_estimate_spec.js @@ -5,6 +5,8 @@ import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; describe('Issue Time Estimate component', () => { let wrapper; + const findIssueTimeEstimate = () => wrapper.find('[data-testid="issue-time-estimate"]'); + afterEach(() => { wrapper.destroy(); }); @@ -26,7 +28,7 @@ describe('Issue Time Estimate component', () => { }); it('renders expanded time estimate in tooltip', () => { - expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute'); + expect(findIssueTimeEstimate().text()).toContain('2 weeks 3 days 1 minute'); }); it('prevents tooltip xss', async () => { @@ -42,7 +44,7 @@ describe('Issue Time Estimate component', () => { expect(alertSpy).not.toHaveBeenCalled(); expect(wrapper.find('time').text().trim()).toEqual('0m'); - expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m'); + expect(findIssueTimeEstimate().text()).toContain('0m'); }); }); @@ -63,7 +65,7 @@ describe('Issue Time Estimate component', () => { }); it('renders expanded time estimate in tooltip', () => { - expect(wrapper.find('.js-issue-time-estimate').text()).toContain('104 hours 1 minute'); + expect(findIssueTimeEstimate().text()).toContain('104 hours 1 minute'); }); }); }); diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js index 45980c36f1c..06cd3910fc0 100644 --- a/spec/frontend/boards/components/item_count_spec.js +++ b/spec/frontend/boards/components/item_count_spec.js @@ -29,7 +29,7 @@ describe('IssueCount', () => { }); it('does not contains maxIssueCount in the template', () => { - expect(vm.find('.js-max-issue-size').exists()).toBe(false); + expect(vm.find('.max-issue-size').exists()).toBe(false); }); }); @@ -50,7 +50,7 @@ describe('IssueCount', () => { }); it('contains maxIssueCount in the template', () => { - expect(vm.find('.js-max-issue-size').text()).toEqual(String(maxIssueCount)); + expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount)); }); it('does not have text-danger class when issueSize is less than maxIssueCount', () => { @@ -75,7 +75,7 @@ describe('IssueCount', () => { }); it('contains maxIssueCount in the template', () => { - expect(vm.find('.js-max-issue-size').text()).toEqual(String(maxIssueCount)); + expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount)); }); it('has text-danger class', () => { diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js index c91a9c0f0fb..7d09c09d03b 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js @@ -1,4 +1,4 @@ -import { GlSprintf } from '@gitlab/ui'; +import { GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue'; @@ -6,6 +6,7 @@ import { CONTAINER_REGISTRY_TITLE, LIST_INTRO_TEXT, EXPIRATION_POLICY_DISABLED_TEXT, + SET_UP_CLEANUP, } from '~/packages_and_registries/container_registry/explorer/constants'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; @@ -21,6 +22,7 @@ describe('registry_header', () => { const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]'); const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]'); const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]'); + const findSetupCleanUpLink = () => wrapper.findComponent(GlLink); const mountComponent = async (propsData, slots) => { wrapper = shallowMount(Component, { @@ -88,6 +90,7 @@ describe('registry_header', () => { }); const text = findExpirationPolicySubHeader(); + expect(text.exists()).toBe(true); expect(text.props()).toMatchObject({ text: EXPIRATION_POLICY_DISABLED_TEXT, @@ -100,12 +103,17 @@ describe('registry_header', () => { await mountComponent({ expirationPolicy: { enabled: true }, expirationPolicyHelpPagePath: 'foo', + showCleanupPolicyLink: true, imagesCount: 1, }); const text = findExpirationPolicySubHeader(); + const cleanupLink = findSetupCleanUpLink(); + expect(text.exists()).toBe(true); expect(text.props('text')).toBe('Expiration policy will run in '); + expect(cleanupLink.exists()).toBe(true); + expect(cleanupLink.text()).toBe(SET_UP_CLEANUP); }); it('when the expiration policy is completely disabled', async () => { await mountComponent({ diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js index da4bfcde217..79403d29d18 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js @@ -6,7 +6,6 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; -import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue'; import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue'; @@ -58,7 +57,6 @@ describe('List Page', () => { const findPersistedSearch = () => wrapper.findComponent(PersistedSearch); const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); const findDeleteImage = () => wrapper.findComponent(DeleteImage); - const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert); const fireFirstSortUpdate = () => { findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] }); @@ -511,33 +509,4 @@ describe('List Page', () => { testTrackingCall('confirm_delete'); }); }); - - describe('cleanup is on alert', () => { - it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => { - mountComponent({ - config: { - showCleanupPolicyOnAlert: true, - projectPath: 'foo', - isGroupPage: false, - cleanupPoliciesSettingsPath: 'bar', - }, - }); - - await waitForApolloRequestRender(); - - expect(findCleanupAlert().exists()).toBe(true); - expect(findCleanupAlert().props()).toMatchObject({ - projectPath: 'foo', - cleanupPoliciesSettingsPath: 'bar', - }); - }); - - it('is hidden when showCleanupPolicyOnAlert is false', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - expect(findCleanupAlert().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js index a6c929844b1..0a72f0269ee 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js @@ -12,7 +12,6 @@ import { UNAVAILABLE_USER_FEATURE_TEXT, } from '~/packages_and_registries/settings/project/constants'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; -import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; import { @@ -31,12 +30,11 @@ describe('Registry Settings App', () => { adminSettingsPath: 'settingsPath', enableHistoricEntries: false, helpPagePath: 'helpPagePath', - showCleanupPolicyOnAlert: false, + showCleanupPolicyLink: false, }; const findSettingsComponent = () => wrapper.find(SettingsForm); const findAlert = () => wrapper.find(GlAlert); - const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert); const mountComponent = (provide = defaultProvidedValues, config) => { wrapper = shallowMount(component, { @@ -69,26 +67,6 @@ describe('Registry Settings App', () => { wrapper.destroy(); }); - describe('cleanup is on alert', () => { - it('exist when showCleanupPolicyOnAlert is true and has the correct props', () => { - mountComponent({ - ...defaultProvidedValues, - showCleanupPolicyOnAlert: true, - }); - - expect(findCleanupAlert().exists()).toBe(true); - expect(findCleanupAlert().props()).toMatchObject({ - projectPath: 'path', - }); - }); - - it('is hidden when showCleanupPolicyOnAlert is false', async () => { - mountComponent(); - - expect(findCleanupAlert().exists()).toBe(false); - }); - }); - describe('isEdited status', () => { it.each` description | apiResponse | workingCopy | result diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap deleted file mode 100644 index 2cded2ead2e..00000000000 --- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CleanupPolicyEnabledAlert renders 1`] = ` -<gl-alert-stub - class="gl-mt-2" - dismissible="true" - dismisslabel="Dismiss" - primarybuttonlink="" - primarybuttontext="" - secondarybuttonlink="" - secondarybuttontext="" - title="" - variant="info" -> - <gl-sprintf-stub - message="Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}" - /> -</gl-alert-stub> -`; diff --git a/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js b/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js deleted file mode 100644 index 269e087f5ac..00000000000 --- a/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import { GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import component from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; - -describe('CleanupPolicyEnabledAlert', () => { - let wrapper; - - const defaultProps = { - projectPath: 'foo', - cleanupPoliciesSettingsPath: 'label-bar', - }; - - const findAlert = () => wrapper.findComponent(GlAlert); - - const mountComponent = (props) => { - wrapper = shallowMount(component, { - stubs: { - LocalStorageSync, - }, - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders', () => { - mountComponent(); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('when dismissed is not visible', async () => { - mountComponent(); - - expect(findAlert().exists()).toBe(true); - findAlert().vm.$emit('dismiss'); - - await nextTick(); - - expect(findAlert().exists()).toBe(false); - }); -}); diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb index d7be4194e67..fc69aee4e04 100644 --- a/spec/helpers/packages_helper_spec.rb +++ b/spec/helpers/packages_helper_spec.rb @@ -65,11 +65,11 @@ RSpec.describe PackagesHelper do end end - describe '#show_cleanup_policy_on_alert' do + describe '#show_cleanup_policy_link' do let_it_be(:user) { create(:user) } let_it_be_with_reload(:container_repository) { create(:container_repository) } - subject { helper.show_cleanup_policy_on_alert(project.reload) } + subject { helper.show_cleanup_policy_link(project.reload) } where(:com, :config_registry, :project_registry, :nil_policy, :container_repositories_exist, :expected_result) do false | false | false | false | false | false diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb new file mode 100644 index 00000000000..d5151dd6beb --- /dev/null +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do + include StubVersion + using RSpec::Parameterized::TableSyntax + + describe '#check_runner_upgrade_status' do + subject(:result) { described_class.instance.check_runner_upgrade_status(runner_version) } + + before do + runner_releases_double = instance_double(Gitlab::Ci::RunnerReleases) + + allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double) + allow(runner_releases_double).to receive(:releases).and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) }) + end + + context 'with available_runner_releases configured up to 14.1.1' do + let(:available_runner_releases) { %w[13.9.0 13.9.1 13.9.2 13.10.0 13.10.1 14.0.0 14.0.1 14.0.2 14.1.0 14.1.1] } + + context 'with nil runner_version' do + let(:runner_version) { nil } + + it 'raises :unknown' do + is_expected.to eq(:unknown) + end + end + + context 'with invalid runner_version' do + let(:runner_version) { 'junk' } + + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'with Gitlab::VERSION set to 14.1.123' do + before do + stub_version('14.1.123', 'deadbeef') + + described_class.instance.reset! + end + + context 'with a runner_version that is too recent' do + let(:runner_version) { 'v14.2.0' } + + it 'returns :not_available' do + is_expected.to eq(:not_available) + end + end + end + + context 'with Gitlab::VERSION set to 14.0.123' do + before do + stub_version('14.0.123', 'deadbeef') + + described_class.instance.reset! + end + + context 'with valid params' do + where(:runner_version, :expected_result) do + 'v14.1.0' | :not_available # not available since the GitLab instance is still on 14.0.x + 'v14.0.1' | :recommended # recommended upgrade since 14.0.2 is available + 'v14.0.2' | :not_available # not available since 14.0.2 is the latest 14.0.x release available + 'v13.10.1' | :available # available upgrade: 14.1.1 + 'v13.10.0' | :recommended # recommended upgrade since 13.10.1 is available + 'v13.9.2' | :recommended # recommended upgrade since backports are no longer released for this version + 'v13.9.0' | :recommended # recommended upgrade since backports are no longer released for this version + 'v13.8.1' | :recommended # recommended upgrade since build is too old (missing in records) + 'v11.4.1' | :recommended # recommended upgrade since build is too old (missing in records) + end + + with_them do + it 'returns symbol representing expected upgrade status' do + is_expected.to be_a(Symbol) + is_expected.to eq(expected_result) + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/github_import/object_counter_spec.rb b/spec/lib/gitlab/github_import/object_counter_spec.rb index c9e4ac67061..e522f74416c 100644 --- a/spec/lib/gitlab/github_import/object_counter_spec.rb +++ b/spec/lib/gitlab/github_import/object_counter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :import_started, import_type: 'github') } it 'validates the operation being incremented' do expect { described_class.increment(project, :issue, :unknown) } @@ -49,4 +49,12 @@ RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache do 'imported' => {} }) end + + it 'expires etag cache of relevant realtime change endpoints on increment' do + expect_next_instance_of(Gitlab::EtagCaching::Store) do |instance| + expect(instance).to receive(:touch).with(Gitlab::Routing.url_helpers.realtime_changes_import_github_path(format: :json)) + end + + described_class.increment(project, :issue, :fetched) + end end diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 0ca918c898a..e937851392b 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -139,7 +139,8 @@ RSpec.describe ContainerRepository, :aggregate_failures do expect { subject } .to change { repository.reload.migration_state }.to('import_done') - .and change { repository.reload.migration_skipped_reason }.to('not_found') + .and change { repository.migration_skipped_reason }.to('not_found') + .and change { repository.migration_import_done_at }.from(nil) end end end diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb index 6d84b2c6931..40cf2bf21a3 100644 --- a/spec/models/project_import_state_spec.rb +++ b/spec/models/project_import_state_spec.rb @@ -101,6 +101,34 @@ RSpec.describe ProjectImportState, type: :model do end end + describe '#expire_etag_cache' do + context 'when project import type has realtime changes endpoint' do + before do + import_state.project.import_type = 'github' + end + + it 'expires revelant etag cache' do + expect_next_instance_of(Gitlab::EtagCaching::Store) do |instance| + expect(instance).to receive(:touch).with(Gitlab::Routing.url_helpers.realtime_changes_import_github_path(format: :json)) + end + + subject.expire_etag_cache + end + end + + context 'when project import type does not have realtime changes endpoint' do + before do + import_state.project.import_type = 'jira' + end + + it 'does not touch etag caches' do + expect(Gitlab::EtagCaching::Store).not_to receive(:new) + + subject.expire_etag_cache + end + end + end + describe 'import state transitions' do context 'state transition: [:started] => [:finished]' do let(:after_import_service) { spy(:after_import_service) } @@ -178,4 +206,20 @@ RSpec.describe ProjectImportState, type: :model do end end end + + describe 'callbacks' do + context 'after_commit :expire_etag_cache' do + before do + import_state.project.import_type = 'github' + end + + it 'expires etag cache' do + expect_next_instance_of(Gitlab::EtagCaching::Store) do |instance| + expect(instance).to receive(:touch).with(Gitlab::Routing.url_helpers.realtime_changes_import_github_path(format: :json)) + end + + subject.save! + end + end + end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb new file mode 100644 index 00000000000..c20a0688ac2 --- /dev/null +++ b/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IncidentManagement::IssuableEscalationStatuses::BuildService do + let_it_be(:project) { create(:project) } + let_it_be(:incident, reload: true) { create(:incident, project: project) } + + let(:service) { described_class.new(incident) } + + subject(:execute) { service.execute } + + it_behaves_like 'initializes new escalation status with expected attributes' + + context 'with associated alert' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, issue: incident) } + + it_behaves_like 'initializes new escalation status with expected attributes', { status_event: :acknowledge } + end +end diff --git a/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb index 8fbab361ec4..2c7d330766c 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb @@ -8,12 +8,12 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::CreateService do let(:incident) { create(:incident, project: project) } let(:service) { described_class.new(incident) } - subject(:execute) { service.execute} + subject(:execute) { service.execute } it 'creates an escalation status for the incident with no policy set' do - expect { execute }.to change { incident.reload.incident_management_issuable_escalation_status }.from(nil) + expect { execute }.to change { incident.reload.escalation_status }.from(nil) - status = incident.incident_management_issuable_escalation_status + status = incident.escalation_status expect(status.policy_id).to eq(nil) expect(status.escalations_started_at).to eq(nil) @@ -24,7 +24,22 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::CreateService do let!(:existing_status) { create(:incident_management_issuable_escalation_status, issue: incident) } it 'exits without changing anything' do - expect { execute }.not_to change { incident.reload.incident_management_issuable_escalation_status } + expect { execute }.not_to change { incident.reload.escalation_status } + end + end + + context 'with associated alert' do + before do + create(:alert_management_alert, :acknowledged, project: project, issue: incident) + end + + it 'creates an escalation status matching the alert attributes' do + expect { execute }.to change { incident.reload.escalation_status }.from(nil) + expect(incident.escalation_status).to have_attributes( + status_name: :acknowledged, + policy_id: nil, + escalations_started_at: nil + ) end end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb index b30b3a69ae6..25164df40ca 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb @@ -71,7 +71,12 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateServ context 'when an IssuableEscalationStatus record for the issue does not exist' do let(:issue) { create(:incident) } - it_behaves_like 'availability error response' + it_behaves_like 'successful response', { status_event: :acknowledge } + + it 'initializes an issuable escalation status record' do + expect { result }.not_to change(::IncidentManagement::IssuableEscalationStatus, :count) + expect(issue.escalation_status).to be_present + end end context 'when called without params' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 6d3c3dd4e39..d8e388b1deb 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1224,6 +1224,18 @@ RSpec.describe Issues::UpdateService, :mailer do end context 'without an escalation status record' do + it 'creates a new record' do + expect { update_issue(opts) }.to change(::IncidentManagement::IssuableEscalationStatus, :count).by(1) + end + + it_behaves_like 'updates the escalation status record', :acknowledged + end + + context 'with :incident_escalations feature flag disabled' do + before do + stub_feature_flags(incident_escalations: false) + end + it_behaves_like 'does not change the status record' end end diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index 1e303197990..15590fd10dc 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -138,7 +138,7 @@ RSpec.shared_examples 'multiple issue boards' do wait_for_requests - dropdown_selector = '.js-boards-selector .dropdown-menu' + dropdown_selector = '[data-testid="boards-selector"] .dropdown-menu' page.within(dropdown_selector) do yield end diff --git a/spec/support/shared_examples/incident_management/issuable_escalation_statuses/build_examples.rb b/spec/support/shared_examples/incident_management/issuable_escalation_statuses/build_examples.rb new file mode 100644 index 00000000000..050fdc3fff7 --- /dev/null +++ b/spec/support/shared_examples/incident_management/issuable_escalation_statuses/build_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'initializes new escalation status with expected attributes' do |attributes = {}| + let(:expected_attributes) { attributes } + + specify do + expect { execute }.to change { incident.escalation_status } + .from(nil) + .to(instance_of(::IncidentManagement::IssuableEscalationStatus)) + + expect(incident.escalation_status).to have_attributes( + id: nil, + issue_id: incident.id, + policy_id: nil, + escalations_started_at: nil, + status_event: nil, + **expected_attributes + ) + end +end diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb index 34073d0ea39..af15f465107 100644 --- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::GithubImport::ImportDiffNoteWorker do describe '#import' do it 'imports a diff note' do - project = double(:project, full_path: 'foo/bar', id: 1) + project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil) client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb index dc0338eccad..29f21c1d184 100644 --- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::GithubImport::ImportIssueWorker do describe '#import' do it 'imports an issue' do - project = double(:project, full_path: 'foo/bar', id: 1) + project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil) client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb index bc254e6246d..f4598340938 100644 --- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::GithubImport::ImportNoteWorker do describe '#import' do it 'imports a note' do - project = double(:project, full_path: 'foo/bar', id: 1) + project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil) client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb index 6fe9741075f..faed2f8f340 100644 --- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::GithubImport::ImportPullRequestWorker do describe '#import' do it 'imports a pull request' do - project = double(:project, full_path: 'foo/bar', id: 1) + project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil) client = double(:client) importer = double(:importer) hash = { |
