diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-08 21:11:30 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-08 21:11:30 +0300 |
commit | 2308cd50203f5b377e4d6e03d017066507beacdf (patch) | |
tree | 651b3412094cb4b7af37c22f66974cac33fb2171 | |
parent | a34d7fd9a723d6cc9c7348be2afe522bdc2be67f (diff) |
Add latest changes from gitlab-org/gitlab@master
91 files changed, 1621 insertions, 698 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9f9ff6e5808..c36525a1fc3 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9bf28d2089501b82b40e2b9f6ad21cf80751f15f +c65b631d971809d9e0294356d7892860d4800cf3 diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index 4b9fe01e997..b5cb1862b45 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -1,7 +1,15 @@ <script> -import { GlDisclosureDropdown, GlButton, GlIcon, GlForm, GlFormCheckbox } from '@gitlab/ui'; +import { + GlDisclosureDropdown, + GlButton, + GlIcon, + GlForm, + GlFormCheckbox, + GlFormRadioGroup, +} from '@gitlab/ui'; // eslint-disable-next-line no-restricted-imports import { mapGetters, mapActions, mapState } from 'vuex'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '~/locale'; import { createAlert } from '~/alert'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; @@ -34,12 +42,14 @@ export default { GlButton, GlIcon, GlForm, + GlFormRadioGroup, GlFormCheckbox, MarkdownEditor, ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'), SummarizeMyReview: () => import('ee_component/batch_comments/components/summarize_my_review.vue'), }, + mixins: [glFeatureFlagsMixin()], inject: { canSummarize: { default: false }, }, @@ -53,6 +63,7 @@ export default { note: '', approve: false, approval_password: '', + reviewer_state: 'reviewed', }, formFieldProps: { id: 'review-note-body', @@ -74,6 +85,38 @@ export default { autosaveKey() { return `submit_review_dropdown/${this.getNoteableData.id}`; }, + radioGroupOptions() { + return [ + { + html: [ + __('Comment'), + `<p class="help-text"> + ${__('Submit general feedback without explicit approval.')} + </p>`, + ].join('<br />'), + value: 'reviewed', + }, + { + html: [ + __('Approve'), + `<p class="help-text"> + ${__('Submit feedback and approve these changes.')} + </p>`, + ].join('<br />'), + value: 'approved', + disabled: !this.userPermissions.canApprove, + }, + { + html: [ + __('Request changes'), + `<p class="help-text"> + ${__('Submit feedback that should be addressed before merging.')} + </p>`, + ].join('<br />'), + value: 'requested_changes', + }, + ]; + }, }, watch: { 'noteData.approve': function noteDataApproveWatch() { @@ -208,7 +251,14 @@ export default { @keydown.ctrl.enter="submitReview" /> </div> - <template v-if="userPermissions.canApprove"> + <gl-form-radio-group + v-if="glFeatures.mrRequestChanges" + v-model="noteData.reviewer_state" + :options="radioGroupOptions" + class="gl-mt-4" + data-testid="reviewer_states" + /> + <template v-else-if="userPermissions.canApprove"> <gl-form-checkbox v-model="noteData.approve" data-testid="approve_merge_request" @@ -216,14 +266,14 @@ export default { > {{ __('Approve merge request') }} </gl-form-checkbox> - <approval-password - v-if="getNoteableData.require_password_to_approve" - v-show="noteData.approve" - v-model="noteData.approval_password" - class="gl-mt-3" - data-testid="approve_password" - /> </template> + <approval-password + v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve" + v-show="noteData.approve || noteData.reviewer_state === 'approved'" + v-model="noteData.approval_password" + class="gl-mt-3" + data-testid="approve_password" + /> <div class="gl-display-flex gl-justify-content-start gl-mt-4"> <gl-button :loading="isSubmitting" diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue index 487215875c0..db84eaa82c2 100644 --- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue +++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue @@ -4,12 +4,22 @@ import { __, s__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants'; +const defaultTitle = __('CI/CD Catalog'); +const defaultDescription = s__( + 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.', +); + export default { components: { GlBanner, GlLink, }, - inject: ['pageTitle', 'pageDescription'], + inject: { + pageTitle: { default: defaultTitle }, + pageDescription: { + default: defaultDescription, + }, + }, data() { return { isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true', @@ -50,7 +60,7 @@ export default { </gl-banner> <h1 class="gl-font-size-h-display">{{ pageTitle }}</h1> <p> - <span>{{ pageDescription }}</span> + <span data-testid="description">{{ pageDescription }}</span> <gl-link :href="$options.learnMorePath" target="_blank">{{ $options.i18n.learnMore }}</gl-link> diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue new file mode 100644 index 00000000000..5e8727a3ed0 --- /dev/null +++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue @@ -0,0 +1,112 @@ +<script> +import { createAlert } from '~/alert'; +import { s__ } from '~/locale'; +import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue'; +import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue'; +import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue'; +import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; +import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings'; +import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql'; + +export default { + components: { + CatalogHeader, + CatalogListSkeletonLoader, + CiResourcesList, + EmptyState, + }, + data() { + return { + catalogResources: [], + currentPage: 1, + totalCount: 0, + pageInfo: {}, + }; + }, + apollo: { + catalogResources: { + query: getCatalogResources, + variables() { + return { + first: ciCatalogResourcesItemsCount, + }; + }, + update(data) { + return data?.ciCatalogResources?.nodes || []; + }, + result({ data }) { + const { pageInfo } = data?.ciCatalogResources || {}; + this.pageInfo = pageInfo; + this.totalCount = data?.ciCatalogResources?.count || 0; + }, + error(e) { + createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' }); + }, + }, + }, + computed: { + hasResources() { + return this.catalogResources.length > 0; + }, + isLoading() { + return this.$apollo.queries.catalogResources.loading; + }, + }, + methods: { + async handlePrevPage() { + try { + await this.$apollo.queries.catalogResources.fetchMore({ + variables: { + before: this.pageInfo.startCursor, + last: ciCatalogResourcesItemsCount, + first: null, + }, + }); + + this.currentPage -= 1; + } catch (e) { + // Ensure that the current query is properly stoped if an error occurs. + this.$apollo.queries.catalogResources.stop(); + createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' }); + } + }, + async handleNextPage() { + try { + await this.$apollo.queries.catalogResources.fetchMore({ + variables: { + after: this.pageInfo.endCursor, + }, + }); + + this.currentPage += 1; + } catch (e) { + // Ensure that the current query is properly stoped if an error occurs. + this.$apollo.queries.catalogResources.stop(); + + createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' }); + } + }, + }, + i18n: { + fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'), + }, +}; +</script> +<template> + <div> + <catalog-header /> + <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" /> + <empty-state v-else-if="!hasResources" /> + <ci-resources-list + v-else + :current-page="currentPage" + :page-info="pageInfo" + :prev-text="__('Prev')" + :next-text="__('Next')" + :resources="catalogResources" + :total-count="totalCount" + @onPrevPage="handlePrevPage" + @onNextPage="handleNextPage" + /> + </div> +</template> diff --git a/app/assets/javascripts/ci/catalog/global_catalog.vue b/app/assets/javascripts/ci/catalog/global_catalog.vue new file mode 100644 index 00000000000..76eac11a122 --- /dev/null +++ b/app/assets/javascripts/ci/catalog/global_catalog.vue @@ -0,0 +1,10 @@ +<script> +import CiCatalogHome from './components/ci_catalog_home.vue'; + +export default { + components: { CiCatalogHome }, +}; +</script> +<template> + <ci-catalog-home /> +</template> diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql new file mode 100644 index 00000000000..aae29edef5e --- /dev/null +++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql @@ -0,0 +1,16 @@ +#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql" + +query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) { + ciCatalogResources(after: $after, before: $before, first: $first, last: $last) { + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + count + nodes { + ...CatalogResourceFields + } + } +} diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js new file mode 100644 index 00000000000..5815245506c --- /dev/null +++ b/app/assets/javascripts/ci/catalog/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings'; + +import GlobalCatalog from './global_catalog.vue'; +import CiResourcesPage from './components/pages/ci_resources_page.vue'; +import { createRouter } from './router'; + +export const initCatalog = (selector = '#js-ci-cd-catalog') => { + const el = document.querySelector(selector); + if (!el) { + return null; + } + + const { dataset } = el; + const { ciCatalogPath } = dataset; + + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(resolvers, cacheConfig), + }); + + return new Vue({ + el, + name: 'GlobalCatalog', + router: createRouter(ciCatalogPath, CiResourcesPage), + apolloProvider, + provide: { + ciCatalogPath, + }, + render(h) { + return h(GlobalCatalog); + }, + }); +}; diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue index dc4a2d91c84..ed5ce02c32e 100644 --- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue +++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue @@ -64,7 +64,7 @@ export default { latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'), mergeTrainBadgeText: s__('Pipelines|merge train'), mergeTrainBadgeTooltip: s__( - 'Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.', + 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.', ), invalidBadgeText: s__('Pipelines|yaml invalid'), failedBadgeText: s__('Pipelines|error'), @@ -74,7 +74,11 @@ export default { ), detachedBadgeText: s__('Pipelines|merge request'), detachedBadgeTooltip: s__( - "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.", + "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch.", + ), + mergedResultsBadgeText: s__('Pipelines|merged results'), + mergedResultsBadgeTooltip: s__( + 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch.', ), stuckBadgeText: s__('Pipelines|stuck'), stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'), @@ -527,6 +531,15 @@ export default { {{ $options.i18n.detachedBadgeText }} </gl-badge> <gl-badge + v-if="badges.mergedResultsPipeline" + v-gl-tooltip + :title="$options.i18n.mergedResultsBadgeTooltip" + variant="info" + size="sm" + > + {{ $options.i18n.mergedResultsBadgeText }} + </gl-badge> + <gl-badge v-if="badges.stuck" v-gl-tooltip :title="$options.i18n.stuckBadgeTooltip" diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js index 067ec3f305e..0ab5d9bcda0 100644 --- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js +++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js @@ -26,6 +26,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph child, latest, mergeTrainPipeline, + mergedResultsPipeline, invalid, failed, autoDevops, @@ -62,6 +63,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph child: parseBoolean(child), latest: parseBoolean(latest), mergeTrainPipeline: parseBoolean(mergeTrainPipeline), + mergedResultsPipeline: parseBoolean(mergedResultsPipeline), invalid: parseBoolean(invalid), failed: parseBoolean(failed), autoDevops: parseBoolean(autoDevops), diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index a78b901af48..f98369c2fde 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -25,12 +25,22 @@ export const EMOJI_VERSION = '3'; const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage(); async function loadEmoji() { - if ( - isLocalStorageAvailable && - window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION && - window.localStorage.getItem(CACHE_KEY) - ) { - return JSON.parse(window.localStorage.getItem(CACHE_KEY)); + try { + window.localStorage.removeItem(CACHE_VERSION_KEY); + } catch { + // Cleanup after us and remove the old EMOJI_VERSION_KEY + } + + try { + if (isLocalStorageAvailable) { + const parsed = JSON.parse(window.localStorage.getItem(CACHE_KEY)); + if (parsed?.EMOJI_VERSION === EMOJI_VERSION && parsed.data) { + return parsed.data; + } + } + } catch { + // Maybe the stored data was corrupted or the version didn't match. + // Let's not error out. } // We load the JSON file direct from the server @@ -41,8 +51,7 @@ async function loadEmoji() { ); try { - window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); - window.localStorage.setItem(CACHE_KEY, JSON.stringify(data)); + window.localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION })); } catch { // Setting data in localstorage may fail when storage quota is exceeded. // We should continue even when this fails. diff --git a/app/assets/javascripts/pages/explore/catalog/index.js b/app/assets/javascripts/pages/explore/catalog/index.js new file mode 100644 index 00000000000..fec738a93a6 --- /dev/null +++ b/app/assets/javascripts/pages/explore/catalog/index.js @@ -0,0 +1,3 @@ +import { initCatalog } from '~/ci/catalog/'; + +initCatalog(); diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue index b50a82ff676..86a5f5107f8 100644 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -1,6 +1,7 @@ <script> // eslint-disable-next-line no-restricted-imports import { mapState, mapGetters } from 'vuex'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; @@ -15,6 +16,7 @@ import { SCOPE_NOTES, SCOPE_COMMITS, SCOPE_MILESTONES, + SCOPE_WIKI_BLOBS, SEARCH_TYPE_ADVANCED, } from '../constants'; import IssuesFilters from './issues_filters.vue'; @@ -24,6 +26,7 @@ import ProjectsFilters from './projects_filters.vue'; import NotesFilters from './notes_filters.vue'; import CommitsFilters from './commits_filters.vue'; import MilestonesFilters from './milestones_filters.vue'; +import WikiBlobsFilters from './wiki_blobs_filters.vue'; export default { name: 'GlobalSearchSidebar', @@ -33,6 +36,7 @@ export default { BlobsFilters, ProjectsFilters, NotesFilters, + WikiBlobsFilters, ScopeLegacyNavigation, ScopeSidebarNavigation, SidebarPortal, @@ -41,6 +45,7 @@ export default { CommitsFilters, MilestonesFilters, }, + mixins: [glFeatureFlagsMixin()], computed: { // useSidebarNavigation refers to whether the new left sidebar navigation is enabled ...mapState(['useSidebarNavigation', 'searchType']), @@ -66,6 +71,12 @@ export default { showMilestonesFilters() { return this.currentScope === SCOPE_MILESTONES; }, + showWikiBlobsFilters() { + return ( + this.currentScope === SCOPE_WIKI_BLOBS && + this.glFeatures?.searchProjectWikisHideArchivedProjects + ); + }, showScopeNavigation() { // showScopeNavigation refers to whether the scope navigation should be shown // while the legacy navigation is being used and there are no search results @@ -93,6 +104,7 @@ export default { <notes-filters v-if="showNotesFilters" /> <commits-filters v-if="showCommitsFilters" /> <milestones-filters v-if="showMilestonesFilters" /> + <wiki-blobs-filters v-if="showWikiBlobsFilters" /> </sidebar-portal> </section> @@ -109,6 +121,7 @@ export default { <notes-filters v-if="showNotesFilters" /> <commits-filters v-if="showCommitsFilters" /> <milestones-filters v-if="showMilestonesFilters" /> + <wiki-blobs-filters v-if="showWikiBlobsFilters" /> </div> <small-screen-drawer-navigation class="gl-lg-display-none"> <scope-legacy-navigation /> @@ -119,6 +132,7 @@ export default { <notes-filters v-if="showNotesFilters" /> <commits-filters v-if="showCommitsFilters" /> <milestones-filters v-if="showMilestonesFilters" /> + <wiki-blobs-filters v-if="showWikiBlobsFilters" /> </small-screen-drawer-navigation> </section> </template> diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js index ed90e2aaded..96a6f119da2 100644 --- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js +++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js @@ -5,7 +5,16 @@ const checkboxLabel = s__('GlobalSearch|Include archived'); export const TRACKING_NAMESPACE = 'search:archived:select'; export const TRACKING_LABEL_CHECKBOX = 'checkbox'; -const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones']; +const scopes = [ + 'projects', + 'issues', + 'merge_requests', + 'notes', + 'blobs', + 'commits', + 'milestones', + 'wiki_blobs', +]; const filterParam = 'include_archived'; diff --git a/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue new file mode 100644 index 00000000000..b1f386d9f4f --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue @@ -0,0 +1,18 @@ +<script> +import ArchivedFilter from './archived_filter/index.vue'; +import FiltersTemplate from './filters_template.vue'; + +export default { + name: 'WikiBlobsFilters', + components: { + ArchivedFilter, + FiltersTemplate, + }, +}; +</script> + +<template> + <filters-template> + <archived-filter class="gl-mb-5" /> + </filters-template> +</template> diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js index b5446ecbb42..1559155a941 100644 --- a/app/assets/javascripts/search/sidebar/constants/index.js +++ b/app/assets/javascripts/search/sidebar/constants/index.js @@ -5,6 +5,8 @@ export const SCOPE_PROJECTS = 'projects'; export const SCOPE_NOTES = 'notes'; export const SCOPE_COMMITS = 'commits'; export const SCOPE_MILESTONES = 'milestones'; +export const SCOPE_WIKI_BLOBS = 'wiki_blobs'; + export const LABEL_DEFAULT_CLASSES = [ 'gl-display-flex', 'gl-flex-direction-row', diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue index 74c41700f43..7962c8573df 100644 --- a/app/assets/javascripts/terraform/components/init_command_modal.vue +++ b/app/assets/javascripts/terraform/components/init_command_modal.vue @@ -40,15 +40,14 @@ export default { }, methods: { getModalInfoCopyStr() { - const stateNameEncoded = this.stateName - ? encodeURIComponent(this.stateName) - : '<YOUR-STATE-NAME>'; + const stateNameEncoded = this.stateName ? encodeURIComponent(this.stateName) : 'default'; return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN> +export TF_STATE_NAME=${stateNameEncoded} terraform init \\ - -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\ - -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\ - -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\ + -backend-config="address=${this.terraformApiUrl}/$TF_STATE_NAME" \\ + -backend-config="lock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\ + -backend-config="unlock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\ -backend-config="username=${this.username}" \\ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\ -backend-config="lock_method=POST" \\ diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue index 7903adea9bd..31cfe387b6e 100644 --- a/app/assets/javascripts/work_items/components/notes/system_note.vue +++ b/app/assets/javascripts/work_items/components/notes/system_note.vue @@ -26,6 +26,11 @@ import { __ } from '~/locale'; import NoteHeader from '~/notes/components/note_header.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +const ALLOWED_ICONS = ['issue-close']; +const ICON_COLORS = { + 'issue-close': 'gl-bg-blue-100! gl-text-blue-700', +}; + export default { i18n: { deleteButtonLabel: __('Remove description history'), @@ -66,6 +71,12 @@ export default { noteAnchorId() { return `note_${this.noteId}`; }, + getIconColor() { + return ICON_COLORS[this.note.systemNoteIconName] || ''; + }, + isAllowedIcon() { + return ALLOWED_ICONS.includes(this.note.systemNoteIconName); + }, isTargetNote() { return this.targetNoteHash === this.noteAnchorId; }, @@ -102,9 +113,16 @@ export default { class="note system-note note-wrapper" > <div - class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600" + :class="[ + getIconColor, + { + 'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon, + 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon, + }, + ]" + class="gl-float-left gl--flex-center gl-rounded-full gl-relative" > - <gl-icon :name="note.systemNoteIconName" /> + <gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" /> </div> <div class="timeline-content"> <div class="note-header"> diff --git a/app/assets/stylesheets/page_bundles/_system_note_styles.scss b/app/assets/stylesheets/page_bundles/_system_note_styles.scss new file mode 100644 index 00000000000..68e2b747c52 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/_system_note_styles.scss @@ -0,0 +1,59 @@ +/** +Shared styles for system note dot and icon styles used for MR, Issue, Work Item +*/ +.system-note-tiny-dot { + width: 8px; + height: 8px; + margin-top: 6px; + margin-left: 12px; + margin-right: 8px; + border: 2px solid var(--gray-50, $gray-50); + } + + .system-note-icon { + width: 20px; + height: 20px; + margin-left: 6px; + + &.gl-bg-green-100 { + --bg-color: var(--green-100, #{$green-100}); + } + + &.gl-bg-red-100 { + --bg-color: var(--red-100, #{$red-100}); + } + + &.gl-bg-blue-100 { + --bg-color: var(--blue-100, #{$blue-100}); + } + } + + .system-note-icon:not(.mr-system-note-empty)::before { + content: ''; + display: block; + position: absolute; + left: calc(50% - 1px); + bottom: 100%; + width: 2px; + height: 20px; + background: linear-gradient(to bottom, transparent, var(--bg-color)); + + .system-note:first-child & { + display: none; + } + } + + .system-note-icon:not(.mr-system-note-empty)::after { + content: ''; + display: block; + position: absolute; + left: calc(50% - 1px); + top: 100%; + width: 2px; + height: 20px; + background: linear-gradient(to bottom, var(--bg-color), transparent); + + .system-note:last-child & { + display: none; + } + }
\ No newline at end of file diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss index 43369efe851..05563f8e314 100644 --- a/app/assets/stylesheets/page_bundles/issuable.scss +++ b/app/assets/stylesheets/page_bundles/issuable.scss @@ -1,4 +1,5 @@ @import 'mixins_and_variables_and_functions'; +@import 'system_note_styles'; .issuable-details { section { @@ -104,61 +105,3 @@ @include gl-font-weight-normal; } } - -.system-note-tiny-dot { - width: 8px; - height: 8px; - margin-top: 6px; - margin-left: 12px; - margin-right: 8px; - border: 2px solid var(--gray-50, $gray-50); -} - - -.system-note-icon { - width: 20px; - height: 20px; - margin-left: 6px; - - &.gl-bg-green-100 { - --bg-color: var(--green-100, #{$green-100}); - } - - &.gl-bg-red-100 { - --bg-color: var(--red-100, #{$red-100}); - } - - &.gl-bg-blue-100 { - --bg-color: var(--blue-100, #{$blue-100}); - } -} - -.system-note-icon:not(.mr-system-note-empty)::before { - content: ''; - display: block; - position: absolute; - left: calc(50% - 1px); - bottom: 100%; - width: 2px; - height: 20px; - background: linear-gradient(to bottom, transparent, var(--bg-color)); - - .system-note:first-child & { - display: none; - } -} - -.system-note-icon:not(.mr-system-note-empty)::after { - content: ''; - display: block; - position: absolute; - left: calc(50% - 1px); - top: 100%; - width: 2px; - height: 20px; - background: linear-gradient(to bottom, var(--bg-color), transparent); - - .system-note:last-child & { - display: none; - } -} diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index 154803c7d88..ec73f27ed09 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -1,4 +1,5 @@ @import 'mixins_and_variables_and_functions'; +@import 'system_note_styles'; $work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important; $work-item-overview-right-sidebar-width: 23rem; diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb index 1ec25d44bfa..fb0073e0ad4 100644 --- a/app/controllers/projects/merge_requests/drafts_controller.rb +++ b/app/controllers/projects/merge_requests/drafts_controller.rb @@ -190,7 +190,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli def update_reviewer_state if reviewer_state_params[:reviewer_state] === 'approved' ::MergeRequests::ApprovalService - .new(project: @project, current_user: current_user) + .new(project: @project, current_user: current_user, params: approve_params) .execute(merge_request) else ::MergeRequests::UpdateReviewerStateService diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a9e15c0bd90..8a92db36311 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -46,6 +46,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:mr_pipelines_graphql, project) push_frontend_feature_flag(:notifications_todos_buttons, current_user) push_frontend_feature_flag(:widget_pipeline_pass_subscription_update, project) + push_frontend_feature_flag(:mr_request_changes, current_user) end before_action only: [:edit] do diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb index f78a28c89dd..48edda13904 100644 --- a/app/controllers/repositories/git_http_controller.rb +++ b/app/controllers/repositories/git_http_controller.rb @@ -125,6 +125,13 @@ module Repositories def log_user_activity Users::ActivityService.new(author: user, project: project, namespace: project&.namespace).execute end + + def append_info_to_payload(payload) + super + + payload[:metadata] ||= {} + payload[:metadata][:repository_storage] = project&.repository_storage + end end end diff --git a/app/finders/data_transfer/mocked_transfer_finder.rb b/app/finders/data_transfer/mocked_transfer_finder.rb deleted file mode 100644 index 9c5551005ea..00000000000 --- a/app/finders/data_transfer/mocked_transfer_finder.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -# Mocked data for data transfer -# Follow this epic for recent progress: https://gitlab.com/groups/gitlab-org/-/epics/9330 -module DataTransfer - class MockedTransferFinder - def execute - start_date = Date.new(2023, 0o1, 0o1) - date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') } - - 0.upto(11).map do |i| - { - date: date_for_index.call(i), - repository_egress: rand(70000..550000), - artifacts_egress: rand(70000..550000), - packages_egress: rand(70000..550000), - registry_egress: rand(70000..550000) - }.tap do |hash| - hash[:total_egress] = hash - .slice(:repository_egress, :artifacts_egress, :packages_egress, :registry_egress) - .values - .sum - end - end - end - end -end diff --git a/app/graphql/mutations/ci/catalog/resources/create.rb b/app/graphql/mutations/ci/catalog/resources/create.rb index 258f83a3e19..7f934e101c8 100644 --- a/app/graphql/mutations/ci/catalog/resources/create.rb +++ b/app/graphql/mutations/ci/catalog/resources/create.rb @@ -15,7 +15,7 @@ module Mutations def resolve(project_path:) project = authorized_find!(project_path: project_path) - response = ::Ci::Catalog::AddResourceService.new(project, current_user).execute + response = ::Ci::Catalog::Resources::CreateService.new(project, current_user).execute errors = response.success? ? [] : [response.message] diff --git a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb index 83bb144017c..133b86623f1 100644 --- a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb +++ b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb @@ -16,16 +16,12 @@ module Resolvers def resolve(**args) return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, group) - results = if Feature.enabled?(:data_transfer_monitoring_mock_data, group) - ::DataTransfer::MockedTransferFinder.new.execute - else - ::DataTransfer::GroupDataTransferFinder.new( - group: group, - from: args[:from], - to: args[:to], - user: current_user - ).execute.map(&:attributes) - end + results = ::DataTransfer::GroupDataTransferFinder.new( + group: group, + from: args[:from], + to: args[:to], + user: current_user + ).execute.map(&:attributes) { egress_nodes: results.to_a } end diff --git a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb index c3296f7d4c3..d711f837251 100644 --- a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb +++ b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb @@ -16,16 +16,12 @@ module Resolvers def resolve(**args) return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, project.group) - results = if Feature.enabled?(:data_transfer_monitoring_mock_data, project.group) - ::DataTransfer::MockedTransferFinder.new.execute - else - ::DataTransfer::ProjectDataTransferFinder.new( - project: project, - from: args[:from], - to: args[:to], - user: current_user - ).execute - end + results = ::DataTransfer::ProjectDataTransferFinder.new( + project: project, + from: args[:from], + to: args[:to], + user: current_user + ).execute { egress_nodes: results } end diff --git a/app/graphql/types/data_transfer/project_data_transfer_type.rb b/app/graphql/types/data_transfer/project_data_transfer_type.rb index 36afa20194e..363b675209d 100644 --- a/app/graphql/types/data_transfer/project_data_transfer_type.rb +++ b/app/graphql/types/data_transfer/project_data_transfer_type.rb @@ -13,7 +13,6 @@ module Types def total_egress(parent:) return unless Feature.enabled?(:data_transfer_monitoring, parent.group) - return 40_000_000 if Feature.enabled?(:data_transfer_monitoring_mock_data, parent.group) object[:egress_nodes].sum('repository_egress + artifacts_egress + packages_egress + registry_egress') end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index fc157df3891..e447940e2af 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -93,16 +93,11 @@ module AuthHelper end def saml_providers - auth_providers.select do |provider| - provider == :saml || auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML' + providers = Gitlab.config.omniauth.providers.select do |provider| + provider.name == 'saml' || provider.dig('args', 'strategy_class') == 'OmniAuth::Strategies::SAML' end - end - - def auth_strategy_class(provider) - config = Gitlab::Auth::OAuth::Provider.config_for(provider) - return if config.nil? || config['args'].blank? - config.args['strategy_class'] + providers.map(&:name).map(&:to_sym) end def any_form_based_providers_enabled? diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb index 0c3b7d26fe2..1558f013462 100644 --- a/app/helpers/projects/pipeline_helper.rb +++ b/app/helpers/projects/pipeline_helper.rb @@ -40,6 +40,7 @@ module Projects child: pipeline.child?.to_s, latest: pipeline.latest?.to_s, merge_train_pipeline: pipeline.merge_train_pipeline?.to_s, + merged_results_pipeline: (pipeline.merged_result_pipeline? && !pipeline.merge_train_pipeline?).to_s, invalid: pipeline.has_yaml_errors?.to_s, failed: pipeline.failure_reason?.to_s, auto_devops: pipeline.auto_devops_source?.to_s, diff --git a/app/services/ci/catalog/add_resource_service.rb b/app/services/ci/catalog/add_resource_service.rb deleted file mode 100644 index c22e7e84c3c..00000000000 --- a/app/services/ci/catalog/add_resource_service.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module Ci - module Catalog - class AddResourceService - include Gitlab::Allowable - - attr_reader :project, :current_user - - def initialize(project, user) - @current_user = user - @project = project - end - - def execute - raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project) - - validation_response = Ci::Catalog::Resources::ValidateService.new(project, project.default_branch).execute - - if validation_response.success? - create_catalog_resource - else - ServiceResponse.error(message: validation_response.message) - end - end - - private - - def create_catalog_resource - catalog_resource = Ci::Catalog::Resource.new(project: project) - - if catalog_resource.valid? - catalog_resource.save! - ServiceResponse.success(payload: catalog_resource) - else - ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', ')) - end - end - end - end -end diff --git a/app/services/ci/catalog/resources/create_service.rb b/app/services/ci/catalog/resources/create_service.rb new file mode 100644 index 00000000000..89367c70e82 --- /dev/null +++ b/app/services/ci/catalog/resources/create_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Ci + module Catalog + module Resources + class CreateService + include Gitlab::Allowable + + attr_reader :project, :current_user + + def initialize(project, user) + @current_user = user + @project = project + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project) + + catalog_resource = Ci::Catalog::Resource.new(project: project) + + if catalog_resource.valid? + catalog_resource.save! + ServiceResponse.success(payload: catalog_resource) + else + ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', ')) + end + end + end + end + end +end diff --git a/app/services/ci/enqueue_job_service.rb b/app/services/ci/enqueue_job_service.rb index 9e3bea3fd28..db616473336 100644 --- a/app/services/ci/enqueue_job_service.rb +++ b/app/services/ci/enqueue_job_service.rb @@ -11,11 +11,14 @@ module Ci end def execute(&transition) - job.user = current_user - job.job_variables_attributes = variables if variables - transition ||= ->(job) { job.enqueue! } - Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job', &transition) + + Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job| + job.user = current_user + job.job_variables_attributes = variables if variables + + transition.call(job) + end ResetSkippedJobsService.new(job.project, current_user).execute(job) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 684231d3a37..e4d894ede1c 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -83,7 +83,7 @@ %ul.content-list.todos-list = render @allowed_todos = paginate @todos, theme: "gitlab" - .js-nothing-here-container.gl-empty-state.gl-text-center.hidden + .col.js-nothing-here-container.gl-empty-state.gl-text-center.hidden .svg-content.svg-150 = image_tag 'illustrations/empty-todos-all-done-md.svg' .text-content.gl-text-center diff --git a/app/views/explore/catalog/show.html.haml b/app/views/explore/catalog/show.html.haml index 6c10ba7dfd7..7c8d788f8e3 100644 --- a/app/views/explore/catalog/show.html.haml +++ b/app/views/explore/catalog/show.html.haml @@ -1,3 +1,3 @@ - page_title _('CI/CD Catalog') -#js-ci-cd-catalog +#js-ci-cd-catalog{ data: { ci_catalog_path: explore_catalog_index_path } } diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml index b172e3bf94f..109bd559762 100644 --- a/app/views/shared/deploy_tokens/_form.html.haml +++ b/app/views/shared/deploy_tokens/_form.html.haml @@ -6,12 +6,12 @@ .form-group = f.label :name, class: 'label-bold' - = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true + = f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'deploy-token-name-field' }, required: true .text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.') .form-group = f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold' - = f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at + = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-token-expires-at-field' }, value: f.object.expires_at .text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.') .form-group @@ -22,15 +22,15 @@ .form-group = f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold' - = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_repository_checkbox' } } + = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { testid: 'deploy-token-read-repository-checkbox' } } - if container_registry_enabled?(group_or_project) - = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_registry_checkbox' } } - = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_registry_checkbox' } } + = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-read-registry-checkbox' } } + = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-write-registry-checkbox' } } - if packages_registry_enabled?(group_or_project) - = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } } - = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } } + = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-read-package-registry-checkbox' } } + = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-write-package-registry-checkbox' } } .gl-mt-3 - = f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true + = f.submit s_('DeployTokens|Create deploy token'), data: { testid: 'create-deploy-token-button' }, pajamas_button: true diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml index 30917ee6fff..2bc2e6c5b81 100644 --- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml +++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml @@ -1,11 +1,11 @@ -.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } } +.created-deploy-token-container.info-well{ data: { testid: 'created-deploy-token-container' } } .well-segment %h5.gl-mt-0 = s_('DeployTokens|Your new Deploy Token username') .form-group .input-group - = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' } + = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-user-field' } .input-group-append = deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left') %span.deploy-token-help-block.gl-mt-2.text-success @@ -15,7 +15,7 @@ .form-group .input-group - = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' } + = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-field' } .input-group-append = deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left') %span.deploy-token-help-block.gl-mt-2.text-danger diff --git a/config/feature_flags/development/data_transfer_monitoring_mock_data.yml b/config/feature_flags/development/data_transfer_monitoring_mock_data.yml deleted file mode 100644 index 77a43426e74..00000000000 --- a/config/feature_flags/development/data_transfer_monitoring_mock_data.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: data_transfer_monitoring_mock_data -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113392 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397693 -milestone: '15.11' -type: development -group: group::source code -default_enabled: false diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md index 761e9e6dd66..b5fc32e69dc 100644 --- a/doc/ci/jobs/index.md +++ b/doc/ci/jobs/index.md @@ -297,7 +297,8 @@ For example, if you start rolling out new code and: ## Expand and collapse job log sections -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0. +> - Support for output of multi-line command bash shell output [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3486) in GitLab 16.5 behind the [GitLab Runner feature flag](https://docs.gitlab.com/runner/configuration/feature-flags.html), `FF_SCRIPT_SECTIONS`. Job logs are divided into sections that can be collapsed or expanded. Each section displays the duration. diff --git a/doc/development/development_processes.md b/doc/development/development_processes.md index 5efcdd90df4..fa2beab52f6 100644 --- a/doc/development/development_processes.md +++ b/doc/development/development_processes.md @@ -1,7 +1,7 @@ --- stage: none group: unassigned -info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines" +info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review. --- # Development processes @@ -35,32 +35,12 @@ Complementary reads: ### Development guidelines review -When you submit a change to the GitLab development guidelines, who -you ask for reviews depends on the level of change. +For changes to development guidelines, request review and approval from an experienced GitLab Team Member. -#### Wording, style, or link changes - -Not all changes require extensive review. For example, MRs that don't change the -content's meaning or function can be reviewed, approved, and merged by any -maintainer or Technical Writer. These can include: - -- Typo fixes. -- Clarifying links, such as to external programming language documentation. -- Changes to comply with the [Documentation Style Guide](documentation/index.md) - that don't change the intent of the documentation page. - -#### Specific changes - -If the MR proposes changes that are limited to a particular stage, group, or team, -request a review and approval from an experienced GitLab Team Member in that -group. For example, if you're documenting a new internal API used exclusively by +For example, if you're documenting a new internal API used exclusively by a given group, request an engineering review from one of the group's members. -After the engineering review is complete, assign the MR to the -[Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments) -in the modified documentation page's metadata. -If the page is not assigned to a specific group, follow the -[Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines). +Small fixes, like typos, can be merged by any user with at least the Maintainer role. #### Broader changes @@ -85,7 +65,6 @@ In these cases, use the following workflow: - [Quality](https://about.gitlab.com/handbook/engineering/quality/) - [Engineering Productivity](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity/) - [Infrastructure](https://about.gitlab.com/handbook/engineering/infrastructure/) - - [Technical Writing](https://about.gitlab.com/handbook/product/ux/technical-writing/) You can skip this step for MRs authored by EMs or Staff Engineers responsible for their area. @@ -97,15 +76,12 @@ In these cases, use the following workflow: author / approver of the MR. If this is a significant change across multiple areas, request final review - and approval from the VP of Development, the DRI for Development Guidelines, - @clefelhocz1. + and approval from the VP of Development, who is the DRI for development guidelines. -1. After all approvals are complete, assign the MR to the - [Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments) - in the modified documentation page's metadata. - If the page is not assigned to a specific group, follow the - [Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines). - The Technical Writer may ask for additional approvals as previously suggested before merging the MR. +Any Maintainer can merge the MR. +If you would like a review by a technical writer, post a message in the #docs Slack channel. +Technical writers do not need to review the content, however, and any Maintainer +other than the MR author can merge. ### Reviewer values @@ -114,6 +90,8 @@ In these cases, use the following workflow: As a reviewer or as a reviewee, make sure to familiarize yourself with the [reviewer values](https://about.gitlab.com/handbook/engineering/workflow/reviewer-values/) we strive for at GitLab. +Also, any doc content should follow the [Documentation Style Guide](documentation/index.md). + ## Language-specific guides ### Go guides diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md index fc0f4013104..5c99f5c48df 100644 --- a/doc/development/documentation/workflow.md +++ b/doc/development/documentation/workflow.md @@ -36,6 +36,13 @@ A member of the Technical Writing team adds these labels: `docs::` prefix. For example, `~docs::improvement`. - The [`~Technical Writing` team label](../labels/index.md#team-labels). +NOTE: +With the exception of `/doc/development/documentation`, +technical writers do not review content in the `doc/development` directory. +Any Maintainer can merge content in the `doc/development` directory. +If you would like a technical writer review of content in the `doc/development` directory, +ask in the `#docs` Slack channel. + ## Post-merge reviews If not assigned to a Technical Writer for review prior to merging, a review must be scheduled diff --git a/doc/development/internal_analytics/index.md b/doc/development/internal_analytics/index.md index d02e366252a..b0e47233777 100644 --- a/doc/development/internal_analytics/index.md +++ b/doc/development/internal_analytics/index.md @@ -14,6 +14,13 @@ when developing new features or instrumenting existing ones. ## Fundamental concepts +<div class="video-fallback"> + See the video about <a href="https://www.youtube.com/watch?v=GtFNXbjygWo">the concepts of events and metrics.</a> +</div> +<figure class="video_container"> + <iframe src="https://www.youtube-nocookie.com/embed/GtFNXbjygWo" frameborder="0" allowfullscreen="true"> </iframe> +</figure> + Events and metrics are the foundation of the internal analytics system. Understanding the difference between the two concepts is vital to using the system. diff --git a/doc/development/repository_storage_moves/index.md b/doc/development/repository_storage_moves/index.md new file mode 100644 index 00000000000..578bc1eabee --- /dev/null +++ b/doc/development/repository_storage_moves/index.md @@ -0,0 +1,102 @@ +--- +stage: Create +group: Source Code +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Project Repository Storage Moves + +This document was created to help contributors understand the code design of +[project repository storage moves](../../api/project_repository_storage_moves.md). +Read this document before making changes to the code for this feature. + +This document is intentionally limited to an overview of how the code is +designed, as code can change often. To understand how a specific part of the +feature works, view the code and the specs. The details here explain how the +major components of the Code Owners feature work. + +NOTE: +This document should be updated when parts of the codebase referenced in this +document are updated, removed, or new parts are added. + +## Business logic + +- `Projects::RepositoryStorageMove`: Tracks the move, includes state machine. + - Defined in `app/models/projects/repository_storage_move.rb`. +- `RepositoryStorageMovable`: Contains the state machine logic, validators, and some helper methods. + - Defined in `app/models/concerns/repository_storage_movable.rb`. +- `Project`: The project model. + - Defined in `app/models/project.rb`. +- `CanMoveRepositoryStorage`: Contains helper methods that are into `Project`. + - Defined in `app/models/concerns/can_move_repository_storage.rb`. +- `API::ProjectRepositoryStorageMoves`: API class for project repository storage moves. + - Defined in `lib/api/project_repository_storage_moves.rb`. +- `Entities::Projects::RepositoryStorageMove`: API entity for serializing the `Projects::RepositoryStorageMove` model. + - Defined in `lib/api/entities/projects/repository_storage_moves.rb`. +- `Projects::ScheduleBulkRepositoryShardMovesService`: Service to schedule bulk moves. + - Defined in `app/services/projects/schedule_bulk_repository_shard_moves_service.rb`. +- `ScheduleBulkRepositoryShardMovesMethods`: Generic methods for bulk moves. + - Defined in `app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb`. +- `Projects::ScheduleBulkRepositoryShardMovesWorker`: Worker to handle bulk moves. + - Defined in `app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb`. +- `Projects::UpdateRepositoryStorageWorker`: Finds repository storage move and then calls the update storage service. + - Defined in `app/workers/projects/update_repository_storage_worker.rb`. +- `UpdateRepositoryStorageWorker`: Module containing generic logic for `Projects::UpdateRepositoryStorageWorker`. + - Defined in `app/workers/concerns/update_repository_storage_worker.rb`. +- `Projects::UpdateRepositoryStorageService`: Performs the move. + - Defined in `app/services/projects/update_repository_storage_service.rb`. +- `UpdateRepositoryStorageMethods`: Module with generic methods included in `Projects::UpdateRepositoryStorageService`. + - Defined in `app/services/concerns/update_repository_storage_methods.rb`. +- `Projects::UpdateService`: Schedules move if the passed parameters request a move. + - Defined in `app/services/projects/update_service.rb`. +- `PoolRepository`: Ruby object representing Gitaly `ObjectPool`. + - Defined in `app/models/pool_repository.rb`. +- `ObjectPool::CreateWorker`: Worker to create an `ObjectPool` via `Gitaly`. + - Defined in `app/workers/object_pool/create_worker.rb`. +- `ObjectPool::JoinWorker`: Worker to join an `ObjectPool` via `Gitaly`. + - Defined in `app/workers/object_pool/join_worker.rb`. +- `ObjectPool::ScheduleJoinWorker`: Worker to schedule an `ObjectPool::JoinWorker`. + - Defined in `app/workers/object_pool/schedule_join_worker.rb`. +- `ObjectPool::DestroyWorker`: Worker to destroy an `ObjectPool` via `Gitaly`. + - Defined in `app/workers/object_pool/destroy_worker.rb`. +- `ObjectPoolQueue`: Module to configure `ObjectPool` workers. + - Defined in `app/workers/concerns/object_pool_queue.rb`. +- `Repositories::ReplicateService`: Handles replication of data from one repository to another. + - Defined in `app/services/repositories/replicate_service.rb`. + +## Flow + +These flowcharts should help explain the flow from the endpoints down to the +models for different features. + +### Schedule a repository storage move via the API + +```mermaid +graph TD + A[<code>POST /api/:version/project_repository_storage_moves</code>] --> C + B[<code>POST /api/:version/projects/:id/repository_storage_moves</code>] --> D + C[Schedule move for each project in shard] --> D[Set state to scheduled] + D --> E[<code>after_transition callback</code>] + E --> F{<code>set_repository_read_only!</code>} + F -->|success| H[Schedule repository update worker] + F -->|error| G[Set state to failed] +``` + +### Moving the storage after being scheduled + +```mermaid +graph TD + A[Repository update worker scheduled] --> B{State is scheduled?} + B -->|Yes| C[Set state to started] + B -->|No| D[Return success] + C --> E{Same filesystem?} + E -.-> G[Set project repo to writable] + E -->|Yes| F["Mirror repositories (project, wiki, design, & pool)"] + G --> H[Update repo storage value] + H --> I[Set state to finished] + I --> J[Associate project with new pool repository] + J --> K[Unlink old pool repository] + K --> L[Update project repository storage values] + L --> N[Remove old paths if same filesystem] + N --> M[Set state to finished] +``` diff --git a/doc/user/gitlab_duo_chat.md b/doc/user/gitlab_duo_chat.md index c103d9c29ba..ba6cd9b8f21 100644 --- a/doc/user/gitlab_duo_chat.md +++ b/doc/user/gitlab_duo_chat.md @@ -13,8 +13,6 @@ You can get AI generated support from GitLab Duo Chat about the following topics - How to use GitLab. - Questions about an issue. -- How to use GitLab. -- Questions about an issue. - Question about an epic. - Questions about a code file. - Follow-up questions to answers from the chat. diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md index 2a33543fea5..5a08307cc11 100644 --- a/doc/user/organization/index.md +++ b/doc/user/organization/index.md @@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Organization +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/409913) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `ui_for_organizations`. Disabled by default. + +FLAG: +This feature is not ready for production use. +On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `ui_for_organizations`. +On GitLab.com, this feature is not available. + DISCLAIMER: This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. @@ -37,6 +44,37 @@ see [epic 9265](https://gitlab.com/groups/gitlab-org/-/epics/9265). For a video introduction to the new hierarchy concept for groups and projects for epics, see [Consolidating groups and projects update (August 2021)](https://www.youtube.com/watch?v=fE74lsG_8yM). +## View organizations + +To view the organizations you have access to: + +- On the left sidebar, select **Organizations** (**{organization}**). + +## Create an organization + +1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New organization**. +1. In the **Organization name** field, enter a name for the organization. +1. In the **Organization URL** field, enter a path for the organization. +1. Select **Create organization**. + +## Edit an organization's name + +1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to edit. +1. Select **Settings > General**. +1. Update the **Organization name** field. +1. Select **Save changes**. + +## Manage groups and projects + +1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage. +1. Select **Manage > Groups and projects**. +1. To switch between groups and projects, use the **Display** filter next to the search box. + +## Manage users + +1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage. +1. Select **Manage > Users**. + ## Related topics - [Organization developer documentation](../../development/organization/index.md) diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 58cfd90c13f..63e5cc93e7d 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -82,6 +82,7 @@ or: > - Filtering by `reviewer` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47605) in GitLab 13.7. > - Filtering by potential approvers was moved to GitLab Premium in 13.9. > - Filtering by `approved-by` moved to GitLab Premium in 13.9. +> - Filtering by `source-branch` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134555) in GitLab 16.6. To filter the list of merge requests: diff --git a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb index 8cd03978f27..f8a05d3132f 100644 --- a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb +++ b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true +# DEPRECATED. Consider using using Internal Events tracking framework +# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html + require 'rails/generators' module Gitlab module UsageMetricDefinition class RedisHllGenerator < Rails::Generators::Base - desc 'Generates a metric definition .yml file with defaults for Redis HLL.' + desc '[DEPRECATED] Generates a metric definition .yml file with defaults for Redis HLL.' argument :category, type: :string, desc: "Category name" argument :events, type: :array, desc: "Unique event names", banner: 'event_one event_two event_three' diff --git a/lib/generators/gitlab/usage_metric_definition_generator.rb b/lib/generators/gitlab/usage_metric_definition_generator.rb index d57a6b0b724..c231697e22e 100644 --- a/lib/generators/gitlab/usage_metric_definition_generator.rb +++ b/lib/generators/gitlab/usage_metric_definition_generator.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# DEPRECATED. Consider using using Internal Events tracking framework +# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html + require 'rails/generators' module Gitlab @@ -30,7 +33,7 @@ module Gitlab source_root File.expand_path('../../../generator_templates/usage_metric_definition', __dir__) - desc 'Generates metric definitions yml files' + desc '[DEPRECATED] Generates metric definitions yml files' class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if metric is for ee' class_option :dir, @@ -40,6 +43,13 @@ module Gitlab argument :key_paths, type: :array, desc: 'Unique JSON key paths for the metrics' def create_metric_file + say("This generator is DEPRECATED. Use Internal Events tracking framework instead.") + # rubocop: disable Gitlab/DocUrl -- link for developers, not users + say("https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html") + # rubocop: enable Gitlab/DocUrl + desc = ask("Would you like to continue anyway? y/N") || 'n' + return unless desc.casecmp('y') == 0 + validate! key_paths.each do |key_path| diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb index e39bbb36680..88991495a10 100644 --- a/lib/gitlab/instrumentation/redis_base.rb +++ b/lib/gitlab/instrumentation/redis_base.rb @@ -90,7 +90,7 @@ module Gitlab result = ::Gitlab::Instrumentation::RedisClusterValidator.validate(commands) return true if result.nil? - if !result[:valid] && !result[:allowed] && (Rails.env.development? || Rails.env.test?) + if !result[:valid] && !result[:allowed] && raise_cross_slot_validation_errors? raise RedisClusterValidator::CrossSlotError, "Redis command #{result[:command_name]} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands" end @@ -189,6 +189,10 @@ module Gitlab redirection_type, _, target_node_key = err_msg.split { redirection_type: redirection_type, target_node_key: target_node_key } end + + def raise_cross_slot_validation_errors? + Rails.env.development? || Rails.env.test? + end end end end diff --git a/lib/gitlab/seeders/ci/catalog/resource_seeder.rb b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb index f1d7e32613d..2971dabe044 100644 --- a/lib/gitlab/seeders/ci/catalog/resource_seeder.rb +++ b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb @@ -79,7 +79,7 @@ module Gitlab end def create_ci_catalog(project) - result = ::Ci::Catalog::AddResourceService.new(project, @current_user).execute + result = ::Ci::Catalog::Resources::CreateService.new(project, @current_user).execute if result.success? result.payload else diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 09570609ff4..c57f178a662 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10337,6 +10337,9 @@ msgstr "" msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier." msgstr "" +msgid "CiCatalog|Discover CI configuration resources for a seamless CI/CD experience." +msgstr "" + msgid "CiCatalog|Get started with the CI/CD Catalog" msgstr "" @@ -12601,9 +12604,6 @@ msgstr "" msgid "ComplianceReport|Remove framework from selected projects" msgstr "" -msgid "ComplianceReport|Retrieving the compliance framework report failed. Refresh the page and try again." -msgstr "" - msgid "ComplianceReport|Search target branch" msgstr "" @@ -35330,10 +35330,13 @@ msgstr "" msgid "Pipelines|This pipeline is stuck" msgstr "" -msgid "Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch." +msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch." msgstr "" -msgid "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch." +msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch." +msgstr "" + +msgid "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch." msgstr "" msgid "Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables." @@ -35417,6 +35420,9 @@ msgstr "" msgid "Pipelines|merge train" msgstr "" +msgid "Pipelines|merged results" +msgstr "" + msgid "Pipelines|stuck" msgstr "" @@ -40429,6 +40435,9 @@ msgstr "" msgid "Request a new one" msgstr "" +msgid "Request changes" +msgstr "" + msgid "Request data is too large" msgstr "" @@ -46503,6 +46512,15 @@ msgstr "" msgid "Submit feedback" msgstr "" +msgid "Submit feedback and approve these changes." +msgstr "" + +msgid "Submit feedback that should be addressed before merging." +msgstr "" + +msgid "Submit general feedback without explicit approval." +msgstr "" + msgid "Submit review" msgstr "" diff --git a/qa/qa/page/component/deploy_token.rb b/qa/qa/page/component/deploy_token.rb new file mode 100644 index 00000000000..71501391db1 --- /dev/null +++ b/qa/qa/page/component/deploy_token.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module DeployToken + extend QA::Page::PageConcern + + def self.included(base) + super + + base.view 'app/views/shared/deploy_tokens/_form.html.haml' do + element 'deploy-token-name-field' + element 'deploy-token-expires-at-field' + element 'deploy-token-read-repository-checkbox' + element 'deploy-token-read-package-registry-checkbox' + element 'deploy-token-write-package-registry-checkbox' + element 'deploy-token-read-registry-checkbox' + element 'deploy-token-write-registry-checkbox' + element 'create-deploy-token-button' + end + + base.view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do + element 'created-deploy-token-container' + element 'deploy-token-user-field' + element 'deploy-token-field' + end + end + + def fill_token_name(name) + fill_element('deploy-token-name-field', name) + end + + def fill_token_expires_at(expires_at) + fill_element('deploy-token-expires-at-field', "#{expires_at}\n") + end + + def fill_scopes(scopes) + check_element('deploy-token-read-repository-checkbox', true) if scopes.include? :read_repository + check_element('deploy-token-read-package-registry-checkbox', true) if scopes.include? :read_package_registry + check_element('deploy-token-write-package-registry-checkbox', true) if scopes.include? :write_package_registry + check_element('deploy-token-read-registry-checkbox', true) if scopes.include? :read_registry + check_element('deploy-token-write-registry-checkbox', true) if scopes.include? :write_registry + end + + def add_token + click_element('create-deploy-token-button') + end + + def token_username + within_new_project_deploy_token do + find_element('deploy-token-user-field').value + end + end + + def token_password + within_new_project_deploy_token do + find_element('deploy-token-field').value + end + end + + private + + def within_new_project_deploy_token(&block) + has_element?('created-deploy-token-container', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) + + within_element('created-deploy-token-container', &block) + end + end + end + end +end diff --git a/qa/qa/page/group/settings/group_deploy_tokens.rb b/qa/qa/page/group/settings/group_deploy_tokens.rb index c1c3303113b..4a44787d26d 100644 --- a/qa/qa/page/group/settings/group_deploy_tokens.rb +++ b/qa/qa/page/group/settings/group_deploy_tokens.rb @@ -5,60 +5,7 @@ module QA module Group module Settings class GroupDeployTokens < Page::Base - view 'app/views/shared/deploy_tokens/_form.html.haml' do - element :deploy_token_name_field - element :deploy_token_expires_at_field - element :deploy_token_read_repository_checkbox - element :deploy_token_read_package_registry_checkbox - element :deploy_token_read_registry_checkbox - element :deploy_token_write_package_registry_checkbox - element :create_deploy_token_button - end - - view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do - element :created_deploy_token_container - element :deploy_token_user_field - element :deploy_token_field - end - - def fill_token_name(name) - fill_element(:deploy_token_name_field, name) - end - - def fill_token_expires_at(expires_at) - fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n") - end - - def fill_scopes(read_repository: false, read_registry: false, read_package_registry: false, write_package_registry: false ) - check_element(:deploy_token_read_repository_checkbox, true) if read_repository - check_element(:deploy_token_read_package_registry_checkbox, true) if read_package_registry - check_element(:deploy_token_read_registry_checkbox, true) if read_registry - check_element(:deploy_token_write_package_registry_checkbox, true) if write_package_registry - end - - def add_token - click_element(:create_deploy_token_button) - end - - def token_username - within_new_project_deploy_token do - find_element(:deploy_token_user_field).value - end - end - - def token_password - within_new_project_deploy_token do - find_element(:deploy_token_field).value - end - end - - private - - def within_new_project_deploy_token(&block) - has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) - - within_element(:created_deploy_token_container, &block) - end + include Page::Component::DeployToken end end end diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb deleted file mode 100644 index cf25f4a0568..00000000000 --- a/qa/qa/page/project/settings/deploy_tokens.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - module Settings - class DeployTokens < Page::Base - view 'app/views/shared/deploy_tokens/_form.html.haml' do - element :deploy_token_name_field - element :deploy_token_expires_at_field - element :deploy_token_read_repository_checkbox - element :deploy_token_read_package_registry_checkbox - element :deploy_token_write_package_registry_checkbox - element :deploy_token_read_registry_checkbox - element :deploy_token_write_registry_checkbox - element :create_deploy_token_button - end - - view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do - element :created_deploy_token_container - element :deploy_token_user_field - element :deploy_token_field - end - - def fill_token_name(name) - fill_element(:deploy_token_name_field, name) - end - - def fill_token_expires_at(expires_at) - fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n") - end - - def fill_scopes(scopes) - if scopes.include? :read_repository - check_element(:deploy_token_read_repository_checkbox, true) - end - - if scopes.include? :read_package_registry - check_element(:deploy_token_read_package_registry_checkbox, true) - end - - if scopes.include? :write_package_registry - check_element(:deploy_token_write_package_registry_checkbox, true) - end - - if scopes.include? :read_registry - check_element(:deploy_token_read_registry_checkbox, true) - end - - if scopes.include? :write_registry - check_element(:deploy_token_write_registry_checkbox, true) - end - end - - def add_token - click_element(:create_deploy_token_button) - end - - def token_username - within_new_project_deploy_token do - find_element(:deploy_token_user_field).value - end - end - - def token_password - within_new_project_deploy_token do - find_element(:deploy_token_field).value - end - end - - private - - def within_new_project_deploy_token - has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) - - within_element(:created_deploy_token_container) do - yield - end - end - end - end - end - end -end diff --git a/qa/qa/page/project/settings/project_deploy_tokens.rb b/qa/qa/page/project/settings/project_deploy_tokens.rb new file mode 100644 index 00000000000..61b44a5e546 --- /dev/null +++ b/qa/qa/page/project/settings/project_deploy_tokens.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class ProjectDeployTokens < Page::Base + include Page::Component::DeployToken + end + end + end + end +end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index a5871f6cd50..d68784c09aa 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -33,7 +33,7 @@ module QA def expand_deploy_tokens(&block) expand_content('deploy-tokens-settings-content') do - Settings::DeployTokens.perform(&block) + Settings::ProjectDeployTokens.perform(&block) end end diff --git a/qa/qa/resource/group_deploy_token.rb b/qa/qa/resource/group_deploy_token.rb index 4c9b296ece1..3a110fcbdc8 100644 --- a/qa/qa/resource/group_deploy_token.rb +++ b/qa/qa/resource/group_deploy_token.rb @@ -51,7 +51,7 @@ module QA setting.expand_deploy_tokens do |page| page.fill_token_name(name) page.fill_token_expires_at(expires_at) - page.fill_scopes(read_repository: true, read_package_registry: true, write_package_registry: true) + page.fill_scopes(@scopes) page.add_token end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb index bde817eccd3..7004f608d9e 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true module QA - RSpec.describe 'Verify', :runner, product_group: :pipeline_security do + RSpec.describe 'Verify', :runner, product_group: :pipeline_security, + quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422863', + type: :flaky + } do describe 'Unlocking job artifacts across parent-child pipelines' do let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" } let(:project) { create(:project, name: 'unlock-job-artifacts-parent-child-project') } diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 602c9c0a2ce..0ae44d3654e 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -200,4 +200,24 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end end end + + describe '#append_info_to_payload' do + let(:log_payload) { {} } + let(:container) { project.design_management_repository } + let(:repository_path) { "#{container.full_path}.git" } + let(:params) { { repository_path: repository_path, service: 'git-upload-pack' } } + let(:repository_storage) { "default" } + + before do + allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *| + method.call(log_payload) + end + end + + it 'appends metadata for logging' do + post :git_upload_pack, params: params + expect(controller).to have_received(:append_info_to_payload) + expect(log_payload.dig(:metadata, :repository_storage)).to eq(repository_storage) + end + end end diff --git a/spec/features/explore/catalog_spec.rb b/spec/features/explore/catalog_spec.rb new file mode 100644 index 00000000000..52ce52e43fe --- /dev/null +++ b/spec/features/explore/catalog_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do + let_it_be(:namespace) { create(:group) } + let_it_be(:user) { create(:user) } + + before_all do + namespace.add_developer(user) + end + + before do + sign_in(user) + end + + describe 'GET explore/catalog' do + let_it_be(:project) { create(:project, :repository, namespace: namespace) } + let_it_be(:ci_resource_projects) do + create_list( + :project, + 3, + :repository, + description: 'A simple component', + namespace: namespace + ) + end + + before do + ci_resource_projects.each do |current_project| + create(:ci_catalog_resource, project: current_project) + end + + visit explore_catalog_index_path + wait_for_requests + end + + it 'shows CI Catalog title and description', :aggregate_failures do + expect(page).to have_content('CI/CD Catalog') + expect(page).to have_content('Discover CI configuration resources for a seamless CI/CD experience.') + end + + it 'renders CI Catalog resources list' do + expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3) + end + + context 'for a single CI/CD catalog resource' do + it 'renders resource details', :aggregate_failures do + within_testid('catalog-resource-item', match: :first) do + expect(page).to have_content(ci_resource_projects[2].name) + expect(page).to have_content(ci_resource_projects[2].description) + expect(page).to have_content(namespace.name) + end + end + + context 'when clicked' do + before do + find_by_testid('ci-resource-link', match: :first).click + end + + it 'navigate to the details page' do + expect(page).to have_content('Go to the project') + end + end + end + end + + describe 'GET explore/catalog/:id' do + let_it_be(:project) { create(:project, :repository, namespace: namespace) } + let_it_be(:new_ci_resource) { create(:ci_catalog_resource, project: project) } + + before do + visit explore_catalog_path(id: new_ci_resource["id"]) + end + + it 'navigates to the details page' do + expect(page).to have_content('Go to the project') + end + end +end diff --git a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb b/spec/finders/data_transfer/mocked_transfer_finder_spec.rb deleted file mode 100644 index f60bc98f587..00000000000 --- a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe DataTransfer::MockedTransferFinder, feature_category: :source_code_management do - describe '#execute' do - subject(:execute) { described_class.new.execute } - - it 'returns mock data' do - expect(execute.first).to include( - date: '2023-01-01', - repository_egress: be_a(Integer), - artifacts_egress: be_a(Integer), - packages_egress: be_a(Integer), - registry_egress: be_a(Integer), - total_egress: be_a(Integer) - ) - - expect(execute.size).to eq(12) - end - end -end diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js index ef86eba1d1a..1037bd48df6 100644 --- a/spec/frontend/__helpers__/emoji.js +++ b/spec/frontend/__helpers__/emoji.js @@ -1,5 +1,5 @@ import { initEmojiMap, EMOJI_VERSION } from '~/emoji'; -import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants'; +import { CACHE_KEY } from '~/emoji/constants'; export const validEmoji = { atom: { @@ -105,9 +105,8 @@ export function clearEmojiMock() { initEmojiMap.promise = null; } -export async function initEmojiMock(mockData = mockEmojiData) { +export async function initEmojiMock(data = mockEmojiData) { clearEmojiMock(); - localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); - localStorage.setItem(CACHE_KEY, JSON.stringify(mockData)); + localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION })); await initEmojiMap(); } diff --git a/spec/frontend/__helpers__/local_storage_helper.js b/spec/frontend/__helpers__/local_storage_helper.js index cf75b0b53fe..367e7ec24ba 100644 --- a/spec/frontend/__helpers__/local_storage_helper.js +++ b/spec/frontend/__helpers__/local_storage_helper.js @@ -30,6 +30,9 @@ export const createLocalStorageSpy = () => { let storage = {}; return { + get length() { + return Object.keys(storage).length; + }, clear: jest.fn(() => { storage = {}; }), diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js index 2f057af8a7d..9e0b13c7e6e 100644 --- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js @@ -19,7 +19,11 @@ let wrapper; let publishReview; let trackingSpy; -function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) { +function factory({ + canApprove = true, + shouldAnimateReviewButton = false, + mrRequestChanges = false, +} = {}) { publishReview = jest.fn(); trackingSpy = mockTracking(undefined, null, jest.spyOn); const requestHandlers = [ @@ -75,6 +79,9 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) wrapper = mountExtended(SubmitDropdown, { store, apolloProvider, + provide: { + glFeatures: { mrRequestChanges }, + }, }); } @@ -101,6 +108,7 @@ describe('Batch comments submit dropdown', () => { note: 'Hello world', approve: false, approval_password: '', + reviewer_state: 'reviewed', }); }); @@ -171,4 +179,52 @@ describe('Batch comments submit dropdown', () => { ); }, ); + + describe('when mrRequestChanges feature flag is enabled', () => { + it('renders a radio group with review state options', async () => { + factory({ mrRequestChanges: true }); + + await waitForPromises(); + + expect(wrapper.findAll('.gl-form-radio').length).toBe(3); + }); + + it('renders disabled approve radio button when user can not approve', async () => { + factory({ mrRequestChanges: true, canApprove: false }); + + wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown'); + + await waitForPromises(); + + expect(wrapper.find('.custom-control-input[value="approved"]').attributes('disabled')).toBe( + 'disabled', + ); + }); + + it.each` + value + ${'approved'} + ${'reviewed'} + ${'requested_changes'} + `('sends $value review state to api when submitting', async ({ value }) => { + factory({ mrRequestChanges: true }); + + wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown'); + + await waitForPromises(); + + await wrapper.find(`.custom-control-input[value="${value}"]`).trigger('change'); + + findForm().vm.$emit('submit', { preventDefault: jest.fn() }); + + expect(publishReview).toHaveBeenCalledWith(expect.anything(), { + noteable_type: 'merge_request', + noteable_id: 1, + note: 'Hello world', + approve: false, + approval_password: '', + reviewer_state: value, + }); + }); + }); }); diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js index 912fd9e1a93..2a5c24d0515 100644 --- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js +++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js @@ -10,36 +10,53 @@ describe('CatalogHeader', () => { let wrapper; const defaultProps = {}; - const defaultProvide = { + const customProvide = { pageTitle: 'Catalog page', pageDescription: 'This is a nice catalog page', }; const findBanner = () => wrapper.findComponent(GlBanner); const findFeedbackButton = () => findBanner().findComponent(GlButton); - const findTitle = () => wrapper.findByText(defaultProvide.pageTitle); - const findDescription = () => wrapper.findByText(defaultProvide.pageDescription); + const findTitle = () => wrapper.find('h1'); + const findDescription = () => wrapper.findByTestId('description'); - const createComponent = ({ props = {}, stubs = {} } = {}) => { + const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => { wrapper = shallowMountExtended(CatalogHeader, { propsData: { ...defaultProps, ...props, }, - provide: defaultProvide, + provide, stubs: { ...stubs, }, }); }; - it('renders the Catalog title and description', () => { - createComponent(); + describe('title and description', () => { + describe('when there are no values provided', () => { + beforeEach(() => { + createComponent(); + }); - expect(findTitle().exists()).toBe(true); - expect(findDescription().exists()).toBe(true); - }); + it('renders the default values', () => { + expect(findTitle().text()).toBe('CI/CD Catalog'); + expect(findDescription().text()).toBe( + 'Discover CI configuration resources for a seamless CI/CD experience.', + ); + }); + }); + describe('when custom values are provided', () => { + beforeEach(() => { + createComponent({ provide: customProvide }); + }); + it('renders the custom values', () => { + expect(findTitle().text()).toBe(customProvide.pageTitle); + expect(findDescription().text()).toBe(customProvide.pageDescription); + }); + }); + }); describe('Feedback banner', () => { describe('when user has never dismissed', () => { beforeEach(() => { diff --git a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js new file mode 100644 index 00000000000..e18b418b155 --- /dev/null +++ b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js @@ -0,0 +1,211 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createAlert } from '~/alert'; + +import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue'; +import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue'; +import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue'; +import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; +import { cacheConfig } from '~/ci/catalog/graphql/settings'; +import ciResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue'; + +import getCatalogResources from '~/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql'; + +import { emptyCatalogResponseBody, catalogResponseBody } from '../../mock'; + +Vue.use(VueApollo); +jest.mock('~/alert'); + +describe('CiResourcesPage', () => { + let wrapper; + let catalogResourcesResponse; + + const createComponent = () => { + const handlers = [[getCatalogResources, catalogResourcesResponse]]; + const mockApollo = createMockApollo(handlers, {}, cacheConfig); + + wrapper = shallowMountExtended(ciResourcesPage, { + apolloProvider: mockApollo, + }); + + return waitForPromises(); + }; + + const findCatalogHeader = () => wrapper.findComponent(CatalogHeader); + const findCiResourcesList = () => wrapper.findComponent(CiResourcesList); + const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader); + const findEmptyState = () => wrapper.findComponent(EmptyState); + + beforeEach(() => { + catalogResourcesResponse = jest.fn(); + }); + + describe('when initial queries are loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows a loading icon and no list', () => { + expect(findLoadingState().exists()).toBe(true); + expect(findEmptyState().exists()).toBe(false); + expect(findCiResourcesList().exists()).toBe(false); + }); + }); + + describe('when queries have loaded', () => { + it('renders the Catalog Header', async () => { + await createComponent(); + + expect(findCatalogHeader().exists()).toBe(true); + }); + + describe('and there are no resources', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody); + + await createComponent(); + }); + + it('renders the empty state', () => { + expect(findLoadingState().exists()).toBe(false); + expect(findEmptyState().exists()).toBe(true); + expect(findCiResourcesList().exists()).toBe(false); + }); + }); + + describe('and there are resources', () => { + const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources; + + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + + await createComponent(); + }); + it('renders the resources list', () => { + expect(findLoadingState().exists()).toBe(false); + expect(findEmptyState().exists()).toBe(false); + expect(findCiResourcesList().exists()).toBe(true); + }); + + it('passes down props to the resources list', () => { + expect(findCiResourcesList().props()).toMatchObject({ + currentPage: 1, + resources: nodes, + pageInfo, + totalCount: count, + }); + }); + }); + }); + + describe('pagination', () => { + it.each` + eventName + ${'onPrevPage'} + ${'onNextPage'} + `('refetch query with new params when receiving $eventName', async ({ eventName }) => { + const { pageInfo } = catalogResponseBody.data.ciCatalogResources; + + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + + expect(catalogResourcesResponse).toHaveBeenCalledTimes(1); + + await findCiResourcesList().vm.$emit(eventName); + + expect(catalogResourcesResponse).toHaveBeenCalledTimes(2); + + if (eventName === 'onNextPage') { + expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + after: pageInfo.endCursor, + first: 20, + }); + } else { + expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + before: pageInfo.startCursor, + last: 20, + first: null, + }); + } + }); + }); + + describe('pages count', () => { + describe('when the fetchMore call suceeds', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + + await createComponent(); + }); + + it('increments and drecrements the page count correctly', async () => { + expect(findCiResourcesList().props().currentPage).toBe(1); + + findCiResourcesList().vm.$emit('onNextPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(2); + + await findCiResourcesList().vm.$emit('onPrevPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(1); + }); + }); + + describe('when the fetchMore call fails', () => { + const errorMessage = 'there was an error'; + + describe('for next page', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody); + catalogResourcesResponse.mockRejectedValue({ message: errorMessage }); + + await createComponent(); + }); + + it('does not increment the page and calls createAlert', async () => { + expect(findCiResourcesList().props().currentPage).toBe(1); + + findCiResourcesList().vm.$emit('onNextPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(1); + expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' }); + }); + }); + + describe('for previous page', () => { + beforeEach(async () => { + // Initial query + catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody); + // When clicking on next + catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody); + // when clicking on previous + catalogResourcesResponse.mockRejectedValue({ message: errorMessage }); + + await createComponent(); + }); + + it('does not decrement the page and calls createAlert', async () => { + expect(findCiResourcesList().props().currentPage).toBe(1); + + findCiResourcesList().vm.$emit('onNextPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(2); + + findCiResourcesList().vm.$emit('onPrevPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(2); + expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/global_catalog_spec.js b/spec/frontend/ci/catalog/global_catalog_spec.js new file mode 100644 index 00000000000..fddabf46c0b --- /dev/null +++ b/spec/frontend/ci/catalog/global_catalog_spec.js @@ -0,0 +1,17 @@ +import { shallowMount } from '@vue/test-utils'; +import GlobalCatalog from '~/ci/catalog/global_catalog.vue'; +import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue'; + +describe('GlobalCatalog', () => { + let wrapper; + + const findHomeComponent = () => wrapper.findComponent(CiCatalogHome); + + beforeEach(() => { + wrapper = shallowMount(GlobalCatalog); + }); + + it('renders the catalog home component', () => { + expect(findHomeComponent().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/ci/catalog/index_spec.js b/spec/frontend/ci/catalog/index_spec.js new file mode 100644 index 00000000000..01332cfbb3d --- /dev/null +++ b/spec/frontend/ci/catalog/index_spec.js @@ -0,0 +1,48 @@ +import Vue from 'vue'; +import { initCatalog } from '~/ci/catalog/'; +import * as Router from '~/ci/catalog/router'; +import CiResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue'; + +describe('~/ci/catalog/index', () => { + describe('initCatalog', () => { + const SELECTOR = 'SELECTOR'; + + let el; + let component; + const baseRoute = '/explore/catalog'; + + const createElement = () => { + el = document.createElement('div'); + el.id = SELECTOR; + el.dataset.ciCatalogPath = baseRoute; + document.body.appendChild(el); + }; + + afterEach(() => { + el = null; + }); + + describe('when the element exists', () => { + beforeEach(() => { + createElement(); + jest.spyOn(Router, 'createRouter'); + component = initCatalog(`#${SELECTOR}`); + }); + + it('returns a Vue Instance', () => { + expect(component).toBeInstanceOf(Vue); + }); + + it('creates a router with the received base path and component', () => { + expect(Router.createRouter).toHaveBeenCalledTimes(1); + expect(Router.createRouter).toHaveBeenCalledWith(baseRoute, CiResourcesPage); + }); + }); + + describe('When the element does not exist', () => { + it('returns `null`', () => { + expect(initCatalog('foo')).toBe(null); + }); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js index 1f24218e494..125f003224c 100644 --- a/spec/frontend/ci/catalog/mock.js +++ b/spec/frontend/ci/catalog/mock.js @@ -1,5 +1,23 @@ import { componentsMockData } from '~/ci/catalog/constants'; +export const emptyCatalogResponseBody = { + data: { + ciCatalogResources: { + pageInfo: { + startCursor: + 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9', + endCursor: + 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9', + hasNextPage: false, + hasPreviousPage: false, + __typename: 'PageInfo', + }, + count: 0, + nodes: [], + }, + }, +}; + export const catalogResponseBody = { data: { ciCatalogResources: { diff --git a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js index 6b8a525a15a..dacee556030 100644 --- a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js +++ b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js @@ -97,6 +97,7 @@ describe('Pipeline details header', () => { child: false, latest: true, mergeTrainPipeline: false, + mergedResultsPipeline: false, invalid: false, failed: false, autoDevops: false, diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js index a4affdfb7ce..7d6a45fbf30 100644 --- a/spec/frontend/emoji/index_spec.js +++ b/spec/frontend/emoji/index_spec.js @@ -1,9 +1,11 @@ +import MockAdapter from 'axios-mock-adapter'; import { emojiFixtureMap, initEmojiMock, validEmoji, invalidEmoji, clearEmojiMock, + mockEmojiData, } from 'helpers/emoji'; import { trimText } from 'helpers/text_helper'; import { createMockClient } from 'helpers/mock_apollo_helper'; @@ -16,6 +18,7 @@ import { getEmojiMap, emojiFallbackImageSrc, loadCustomEmojiWithNames, + EMOJI_VERSION, } from '~/emoji'; import isEmojiUnicodeSupported, { @@ -26,8 +29,11 @@ import isEmojiUnicodeSupported, { isHorceRacingSkinToneComboEmoji, isPersonZwjEmoji, } from '~/emoji/support/is_emoji_unicode_supported'; -import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants'; +import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants'; import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql'; +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { useLocalStorageSpy } from 'jest/__helpers__/local_storage_helper'; let mockClient; jest.mock('~/lib/graphql', () => { @@ -74,6 +80,195 @@ function createMockEmojiClient() { document.body.dataset.groupFullPath = 'test-group'; } +describe('retrieval of emojis.json', () => { + useLocalStorageSpy(); + + let mock; + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(/emojis\.json$/).reply(HTTP_STATUS_OK, mockEmojiData); + initEmojiMap.promise = null; + }); + + afterEach(() => { + mock.restore(); + }); + + const assertCorrectLocalStorage = () => { + expect(localStorage.length).toBe(1); + expect(localStorage.getItem(CACHE_KEY)).toBe( + JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }), + ); + }; + + const assertEmojiBeingLoadedCorrectly = () => { + expect(Object.keys(getEmojiMap())).toEqual(Object.keys(validEmoji)); + }; + + it('should remove the old `CACHE_VERSION_KEY`', async () => { + localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); + + await initEmojiMap(); + + expect(localStorage.getItem(CACHE_VERSION_KEY)).toBe(null); + }); + + describe('when the localStorage is empty', () => { + it('should call the API and store results in localStorage', async () => { + await initEmojiMap(); + + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores the correct version', () => { + beforeEach(async () => { + localStorage.setItem(CACHE_KEY, JSON.stringify({ data: mockEmojiData, EMOJI_VERSION })); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should not call the API and not mutate the localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(0); + expect(localStorage.setItem).not.toHaveBeenCalled(); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores an incorrect version', () => { + beforeEach(async () => { + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ data: mockEmojiData, EMOJI_VERSION: `${EMOJI_VERSION}-different` }), + ); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should call the API and store results in localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores corrupted data', () => { + beforeEach(async () => { + localStorage.setItem(CACHE_KEY, "[invalid: 'INVALID_JSON"); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should call the API and store results in localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores data in a different format', () => { + beforeEach(async () => { + localStorage.setItem(CACHE_KEY, JSON.stringify([])); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should call the API and store results in localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage is full', () => { + beforeEach(async () => { + const oldSetItem = localStorage.setItem; + localStorage.setItem = jest.fn().mockImplementationOnce((key, value) => { + if (key === CACHE_KEY) { + throw new Error('Storage Full'); + } + oldSetItem(key, value); + }); + await initEmojiMap(); + }); + + it('should call API but not store the results', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + expect(localStorage.length).toBe(0); + expect(localStorage.setItem).toHaveBeenCalledTimes(1); + expect(localStorage.setItem).toHaveBeenCalledWith( + CACHE_KEY, + JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }), + ); + }); + }); + + describe('backwards compatibility', () => { + // As per: https://gitlab.com/gitlab-org/gitlab/-/blob/62b66abd3bb7801e7c85b4e42a1bbd51fbb37c1b/app/assets/javascripts/emoji/index.js#L27-52 + async function prevImplementation() { + if ( + window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION && + window.localStorage.getItem(CACHE_KEY) + ) { + return JSON.parse(window.localStorage.getItem(CACHE_KEY)); + } + + // We load the JSON file direct from the server + // because it can't be loaded from a CDN due to + // cross domain problems with JSON + const { data } = await axios.get( + `${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`, + ); + + try { + window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); + window.localStorage.setItem(CACHE_KEY, JSON.stringify(data)); + } catch { + // Setting data in localstorage may fail when storage quota is exceeded. + // We should continue even when this fails. + } + + return data; + } + + it('Old -> New -> Old should not break', async () => { + // The follow steps simulate a multi-version deployment. e.g. + // Hitting a page on "regular" .com, then canary, and then "regular" again + + // Load emoji the old way to pre-populate the cache + let res = await prevImplementation(); + expect(res).toEqual(mockEmojiData); + expect(mock.history.get.length).toBe(1); + localStorage.setItem.mockClear(); + + // Load emoji the new way + await initEmojiMap(); + expect(mock.history.get.length).toBe(2); + assertEmojiBeingLoadedCorrectly(); + assertCorrectLocalStorage(); + localStorage.setItem.mockClear(); + + // Load emoji the old way to pre-populate the cache + res = await prevImplementation(); + expect(res).toEqual(mockEmojiData); + expect(mock.history.get.length).toBe(3); + expect(localStorage.setItem.mock.calls).toEqual([ + [CACHE_VERSION_KEY, EMOJI_VERSION], + [CACHE_KEY, JSON.stringify(mockEmojiData)], + ]); + + // Load emoji the old way should work again (and be taken from the cache) + res = await prevImplementation(); + expect(res).toEqual(mockEmojiData); + expect(mock.history.get.length).toBe(3); + }); + }); +}); + describe('emoji', () => { beforeEach(async () => { await initEmojiMock(); diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js index af6621d5193..c2d88493d71 100644 --- a/spec/frontend/search/sidebar/components/app_spec.js +++ b/spec/frontend/search/sidebar/components/app_spec.js @@ -17,6 +17,7 @@ import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue'; import NotesFilters from '~/search/sidebar/components/notes_filters.vue'; import CommitsFilters from '~/search/sidebar/components/commits_filters.vue'; import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue'; +import WikiBlobsFilters from '~/search/sidebar/components/wiki_blobs_filters.vue'; import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; @@ -44,6 +45,11 @@ describe('GlobalSearchSidebar', () => { wrapper = shallowMount(GlobalSearchSidebar, { store, + provide: { + glFeatures: { + searchProjectWikisHideArchivedProjects: true, + }, + }, }); }; @@ -55,6 +61,7 @@ describe('GlobalSearchSidebar', () => { const findNotesFilters = () => wrapper.findComponent(NotesFilters); const findCommitsFilters = () => wrapper.findComponent(CommitsFilters); const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters); + const findWikiBlobsFilters = () => wrapper.findComponent(WikiBlobsFilters); const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation); const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation); const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation); @@ -85,6 +92,8 @@ describe('GlobalSearchSidebar', () => { ${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true} ${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_BASIC} | ${true} ${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true} + ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${true} + ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true} `('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => { beforeEach(() => { getterSpies.currentScope = jest.fn(() => scope); diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js index 4015482b81b..cdd25e90318 100644 --- a/spec/frontend/terraform/components/init_command_modal_spec.js +++ b/spec/frontend/terraform/components/init_command_modal_spec.js @@ -8,13 +8,13 @@ const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1'; const username = 'username'; const modalId = 'fake-modal-id'; const stateName = 'aws/eu-central-1'; -const stateNamePlaceholder = '<YOUR-STATE-NAME>'; const stateNameEncoded = encodeURIComponent(stateName); const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN> +export TF_STATE_NAME=${stateNameEncoded} terraform init \\ - -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\ - -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\ - -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\ + -backend-config="address=${terraformApiUrl}/$TF_STATE_NAME" \\ + -backend-config="lock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\ + -backend-config="unlock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\ -backend-config="username=${username}" \\ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\ -backend-config="lock_method=POST" \\ @@ -67,7 +67,7 @@ describe('InitCommandModal', () => { describe('init command', () => { it('includes correct address', () => { expect(findInitCommand().text()).toContain( - `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`, + `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`, ); }); it('includes correct username', () => { @@ -94,7 +94,7 @@ describe('InitCommandModal', () => { describe('on rendering', () => { it('includes correct address', () => { expect(findInitCommand().text()).toContain( - `-backend-config="address=${terraformApiUrl}/${stateNamePlaceholder}"`, + `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`, ); }); }); diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js index 03f1aa356ad..69bc0961240 100644 --- a/spec/frontend/work_items/components/notes/system_note_spec.js +++ b/spec/frontend/work_items/components/notes/system_note_spec.js @@ -40,8 +40,14 @@ describe('Work Items system note component', () => { ); }); - it('should render svg icon', () => { - expect(findTimelineIcon().exists()).toBe(true); + it('should render svg icon only for allowed icons', () => { + expect(findTimelineIcon().exists()).toBe(false); + + const ALLOWED_ICONS = ['issue-close']; + ALLOWED_ICONS.forEach((icon) => { + createComponent({ note: { ...workItemSystemNoteWithMetadata, systemNoteIconName: icon } }); + expect(findTimelineIcon().exists()).toBe(true); + }); }); it('should not show compare previous version for FOSS', () => { diff --git a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb index 4ea3d287454..b5bffbc8803 100644 --- a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb +++ b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ let(:from) { Date.new(2022, 1, 1) } let(:to) { Date.new(2023, 1, 1) } + let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) } let(:finder_results) do [ build(:project_data_transfer, date: to, repository_egress: 250000) @@ -41,21 +42,12 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ include_examples 'Data transfer resolver' - context 'when data_transfer_monitoring_mock_data is disabled' do - let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) } + it 'calls GroupDataTransferFinder with expected arguments' do + expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with( + group: group, from: from, to: to, user: current_user).once.and_return(finder) + allow(finder).to receive(:execute).once.and_return(finder_results) - before do - stub_feature_flags(data_transfer_monitoring_mock_data: false) - end - - it 'calls GroupDataTransferFinder with expected arguments' do - expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with( - group: group, from: from, to: to, user: current_user - ).once.and_return(finder) - allow(finder).to receive(:execute).once.and_return(finder_results) - - expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) }) - end + expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) }) end end diff --git a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb index 7307c1a54a9..25ff02218cf 100644 --- a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb +++ b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb @@ -10,6 +10,9 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat let(:from) { Date.new(2022, 1, 1) } let(:to) { Date.new(2023, 1, 1) } + + let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) } + let(:finder_results) do [ { @@ -44,21 +47,12 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat include_examples 'Data transfer resolver' - context 'when data_transfer_monitoring_mock_data is disabled' do - let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) } - - before do - stub_feature_flags(data_transfer_monitoring_mock_data: false) - end - - it 'calls ProjectDataTransferFinder with expected arguments' do - expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with( - project: project, from: from, to: to, user: current_user - ).once.and_return(finder) - allow(finder).to receive(:execute).once.and_return(finder_results) + it 'calls ProjectDataTransferFinder with expected arguments' do + expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with( + project: project, from: from, to: to, user: current_user).once.and_return(finder) + allow(finder).to receive(:execute).once.and_return(finder_results) - expect(resolve_egress).to eq({ egress_nodes: finder_results }) - end + expect(resolve_egress).to eq({ egress_nodes: finder_results }) end end diff --git a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb index a93da279b7f..80ead81650e 100644 --- a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb +++ b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb @@ -14,25 +14,15 @@ RSpec.describe GitlabSchema.types['ProjectDataTransfer'], feature_category: :sou let_it_be(:project) { create(:project) } let(:from) { Date.new(2022, 1, 1) } let(:to) { Date.new(2023, 1, 1) } - let(:finder_result) { 40_000_000 } + let(:relation) { instance_double(ActiveRecord::Relation) } - it 'returns mock data' do - expect(resolve_field(:total_egress, { from: from, to: to }, extras: { parent: project }, - arg_style: :internal)).to eq(finder_result) + before do + allow(relation).to receive(:sum).and_return(10) end - context 'when data_transfer_monitoring_mock_data is disabled' do - let(:relation) { instance_double(ActiveRecord::Relation) } - - before do - allow(relation).to receive(:sum).and_return(10) - stub_feature_flags(data_transfer_monitoring_mock_data: false) - end - - it 'calls sum on active record relation' do - expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project }, - arg_style: :internal)).to eq(10) - end + it 'calls sum on active record relation' do + expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project }, + arg_style: :internal)).to eq(10) end end end diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 40798b4c038..264137add8a 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -2,7 +2,9 @@ require "spec_helper" -RSpec.describe AuthHelper do +RSpec.describe AuthHelper, feature_category: :system_access do + include LoginHelpers + describe "button_based_providers" do it 'returns all enabled providers from devise' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } @@ -310,88 +312,16 @@ RSpec.describe AuthHelper do end end - describe '#auth_strategy_class' do - subject(:auth_strategy_class) { helper.auth_strategy_class(name) } - - context 'when configuration specifies no provider' do - let(:name) { 'does_not_exist' } - - before do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([]) - end - - it 'returns false' do - expect(auth_strategy_class).to be_falsey - end - end - - context 'when configuration specifies a provider with args but without strategy_class' do - let(:name) { 'google_oauth2' } - let(:provider) do - Struct.new(:name, :args).new( - name, - 'app_id' => 'YOUR_APP_ID' - ) - end - - before do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) - end - - it 'returns false' do - expect(auth_strategy_class).to be_falsey - end - end - - context 'when configuration specifies a provider with args and strategy_class' do - let(:name) { 'provider1' } - let(:strategy) { 'OmniAuth::Strategies::LDAP' } - let(:provider) do - Struct.new(:name, :args).new( - name, - 'strategy_class' => strategy - ) - end - - before do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) - end - - it 'returns the class' do - expect(auth_strategy_class).to eq(strategy) - end - end - - context 'when configuration specifies another provider with args and another strategy_class' do - let(:name) { 'provider1' } - let(:strategy) { 'OmniAuth::Strategies::LDAP' } - let(:provider) do - Struct.new(:name, :args).new( - 'another_name', - 'strategy_class' => strategy - ) - end - - before do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) - end - - it 'returns false' do - expect(auth_strategy_class).to be_falsey - end - end - end - describe '#saml_providers' do subject(:saml_providers) { helper.saml_providers } let(:saml_strategy) { 'OmniAuth::Strategies::SAML' } - let(:saml_provider_1_name) { 'saml_provider_1' } + let(:saml_provider_1_name) { 'saml' } let(:saml_provider_1) do Struct.new(:name, :args).new( saml_provider_1_name, - 'strategy_class' => saml_strategy + {} ) end @@ -422,7 +352,7 @@ RSpec.describe AuthHelper do context 'when SAML is enabled without specifying a strategy class' do before do - allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml]) + stub_omniauth_config(providers: [saml_provider_1]) end it 'returns the saml provider' do @@ -432,8 +362,7 @@ RSpec.describe AuthHelper do context 'when configuration specifies no provider' do before do - allow(Devise).to receive(:omniauth_providers).and_return([]) - allow(Gitlab.config.omniauth).to receive(:providers).and_return([]) + stub_omniauth_config(providers: []) end it 'returns an empty list' do @@ -443,30 +372,27 @@ RSpec.describe AuthHelper do context 'when configuration specifies a provider with a SAML strategy_class' do before do - allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name]) - allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1]) + stub_omniauth_config(providers: [saml_provider_1]) end it 'returns the provider' do - expect(saml_providers).to match_array([saml_provider_1_name]) + expect(saml_providers).to match_array([saml_provider_1_name.to_sym]) end end context 'when configuration specifies two providers with a SAML strategy_class' do before do - allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name]) - allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2]) + stub_omniauth_config(providers: [saml_provider_1, saml_provider_2]) end it 'returns the provider' do - expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name]) + expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym]) end end context 'when configuration specifies a provider with a non-SAML strategy_class' do before do - allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name]) - allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider]) + stub_omniauth_config(providers: [ldap_provider]) end it 'returns an empty list' do @@ -476,12 +402,11 @@ RSpec.describe AuthHelper do context 'when configuration specifies four providers but only two with SAML strategy_class' do before do - allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name]) - allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider]) + stub_omniauth_config(providers: [saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider]) end it 'returns the provider' do - expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name]) + expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym]) end end end diff --git a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb index b6e1d59f6c0..5265b608ab4 100644 --- a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb +++ b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout do +RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout, feature_category: :service_ping do include UsageDataHelpers let(:category) { 'test_category' } @@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout stub_const("#{Gitlab::UsageMetricDefinitionGenerator}::TOP_LEVEL_DIR", temp_dir) # Stub Prometheus requests from Gitlab::Utils::UsageData stub_prometheus_queries + + allow_next_instance_of(Gitlab::UsageMetricDefinitionGenerator) do |instance| + allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning + end end after do diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb index f7a4bac39d7..e0cb74d8559 100644 --- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb +++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do +RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_category: :service_ping do include UsageDataHelpers let(:key_path) { 'counts_weekly.test_metric' } @@ -14,6 +14,10 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir) # Stub Prometheus requests from Gitlab::Utils::UsageData stub_prometheus_queries + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning + end end after do @@ -100,4 +104,19 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do expect(files.count).to eq(2) end end + + ['n', 'N', 'random word', nil].each do |answer| + context "when user agreed with deprecation warning by typing: #{answer}" do + it 'does not create definition file' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:ask).and_return(answer) + end + + described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name }).invoke_all + files = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_metric.yml')) + + expect(files.count).to eq(0) + end + end + end end diff --git a/spec/lib/gitlab/auth/saml/config_spec.rb b/spec/lib/gitlab/auth/saml/config_spec.rb index d657622c9f2..2ecc26f9b96 100644 --- a/spec/lib/gitlab/auth/saml/config_spec.rb +++ b/spec/lib/gitlab/auth/saml/config_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::Saml::Config do + include LoginHelpers + describe '.enabled?' do subject { described_class.enabled? } @@ -10,7 +12,7 @@ RSpec.describe Gitlab::Auth::Saml::Config do context 'when SAML is enabled' do before do - allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml]) + stub_basic_saml_config end it { is_expected.to eq(true) } diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 07a06025c0f..f8a4d8023c1 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -42,15 +42,19 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac context 'when Redis calls are made' do let_it_be(:redis_store_class) { define_helper_redis_store_class } - before do # init redis connection with `test` env details + before do redis_store_class.with(&:ping) Gitlab::Redis::Queues.with(&:ping) RequestStore.clear! end - it 'adds Redis data and omits Gitaly data' do - stub_rails_env('staging') # to avoid raising CrossSlotError + it 'adds Redis data including cross slot calls' do + expect(Gitlab::Instrumentation::RedisBase) + .to receive(:raise_cross_slot_validation_errors?) + .once.and_return(false) + redis_store_class.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) } + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do redis_store_class.with { |redis| redis.mget('cache-test', 'cache-test-2') } end diff --git a/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb index e4cd0d9c006..4bd4455d1bd 100644 --- a/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb +++ b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb @@ -53,7 +53,7 @@ RSpec.describe ::Gitlab::Seeders::Ci::Catalog::ResourceSeeder, feature_category: context 'when ci resource creation fails' do before do - allow_next_instance_of(::Ci::Catalog::AddResourceService) do |service| + allow_next_instance_of(::Ci::Catalog::Resources::CreateService) do |service| allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error')) end end diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb index b7c038afa54..e17074a0247 100644 --- a/spec/requests/api/graphql/group/data_transfer_spec.rb +++ b/spec/requests/api/graphql/group/data_transfer_spec.rb @@ -71,45 +71,21 @@ RSpec.describe 'group data transfers', feature_category: :source_code_management context 'when user has enough permissions' do before do group.add_owner(current_user) + subject end - context 'when data_transfer_monitoring_mock_data is NOT enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: false) - subject - end - - it 'returns real results' do - expect(response).to have_gitlab_http_status(:ok) + it 'returns real results' do + expect(response).to have_gitlab_http_status(:ok) - expect(egress_data.count).to eq(2) + expect(egress_data.count).to eq(2) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) - expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6]) - end - - it_behaves_like 'a working graphql query' + expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6]) end - context 'when data_transfer_monitoring_mock_data is enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: true) - subject - end - - it 'returns mock results' do - expect(response).to have_gitlab_http_status(:ok) - - expect(egress_data.count).to eq(12) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) - end - - it_behaves_like 'a working graphql query' - end + it_behaves_like 'a working graphql query' end end diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb index e46b614f02e..f990cab55f4 100644 --- a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb @@ -36,19 +36,5 @@ RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition expect(response).to have_gitlab_http_status(:success) end end - - context 'with an invalid project' do - let_it_be(:project) { create(:project, :repository) } - - before_all do - project.add_owner(current_user) - end - - it 'returns an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(graphql_mutation_response(:catalog_resources_create)['errors']).not_to be_empty - end - end end end diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb index aafa8d65eb9..79b2b10419b 100644 --- a/spec/requests/api/graphql/project/data_transfer_spec.rb +++ b/spec/requests/api/graphql/project/data_transfer_spec.rb @@ -68,45 +68,21 @@ RSpec.describe 'project data transfers', feature_category: :source_code_manageme context 'when user has enough permissions' do before do project.add_owner(current_user) + subject end - context 'when data_transfer_monitoring_mock_data is NOT enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: false) - subject - end - - it 'returns real results' do - expect(response).to have_gitlab_http_status(:ok) + it 'returns real results' do + expect(response).to have_gitlab_http_status(:ok) - expect(egress_data.count).to eq(2) + expect(egress_data.count).to eq(2) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) - expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2]) - end - - it_behaves_like 'a working graphql query' + expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2]) end - context 'when data_transfer_monitoring_mock_data is enabled' do - before do - stub_feature_flags(data_transfer_monitoring_mock_data: true) - subject - end - - it 'returns mock results' do - expect(response).to have_gitlab_http_status(:ok) - - expect(egress_data.count).to eq(12) - expect(egress_data.first.keys).to match_array( - %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] - ) - end - - it_behaves_like 'a working graphql query' - end + it_behaves_like 'a working graphql query' end end diff --git a/spec/services/ci/catalog/add_resource_service_spec.rb b/spec/services/ci/catalog/resources/create_service_spec.rb index 145d04d457c..202c76acaec 100644 --- a/spec/services/ci/catalog/add_resource_service_spec.rb +++ b/spec/services/ci/catalog/resources/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_composition do +RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipeline_composition do let_it_be(:project) { create(:project, :catalog_resource_with_components) } let_it_be(:user) { create(:user) } @@ -32,20 +32,6 @@ RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_comp end end - context 'with an invalid project' do - let_it_be(:project) { create(:project, :repository) } - - before_all do - project.add_owner(user) - end - - it 'does not create a catalog resource' do - response = service.execute - - expect(response.message).to eq('Project must have a description, Project must contain components') - end - end - context 'with an invalid catalog resource' do it 'does not save the catalog resource' do catalog_resource = instance_double(::Ci::Catalog::Resource, diff --git a/spec/services/ci/enqueue_job_service_spec.rb b/spec/services/ci/enqueue_job_service_spec.rb index c2bb0bb2bb5..85983651148 100644 --- a/spec/services/ci/enqueue_job_service_spec.rb +++ b/spec/services/ci/enqueue_job_service_spec.rb @@ -78,4 +78,33 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_ execute end end + + context 'when the job is manually triggered another user' do + let(:job_variables) do + [{ key: 'third', secret_value: 'third' }, + { key: 'fourth', secret_value: 'fourth' }] + end + + let(:service) do + described_class.new(build, current_user: user, variables: job_variables) + end + + it 'assigns the user and variables to the job', :aggregate_failures do + called = false + service.execute do + unless called + called = true + raise ActiveRecord::StaleObjectError + end + + build.enqueue! + end + + build.reload + + expect(called).to be true # ensure we actually entered the failure path + expect(build.user).to eq(user) + expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth') + end + end end diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml index 0af4de11d51..e60cc4278af 100644 --- a/spec/support/finder_collection_allowlist.yml +++ b/spec/support/finder_collection_allowlist.yml @@ -70,4 +70,3 @@ - UploaderFinder - UserGroupNotificationSettingsFinder - UserGroupsCounter -- DataTransfer::MockedTransferFinder # Can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/397693 is closed diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 1a89cf4cc37..d35fa801638 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -262,19 +262,15 @@ module LoginHelpers end def stub_omniauth_config(messages) - allow(Gitlab.config.omniauth).to receive_messages(messages) + allow(Gitlab.config.omniauth).to receive_messages(GitlabSettings::Options.build(messages)) end def stub_basic_saml_config - allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config| - allow(config).to receive_messages({ options: { name: 'saml', args: {} } }) - end + stub_omniauth_config(providers: [{ name: 'saml', args: {} }]) end def stub_saml_group_config(groups) - allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config| - allow(config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) - end + stub_omniauth_config(providers: [{ name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} }]) end end diff --git a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb index 8551bd052ce..c50e0434eb1 100644 --- a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb @@ -1,16 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'Data transfer resolver' do - it 'returns mock data' do |_query_object| - mocked_data = ['mocked_data'] - - allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance| - allow(instance).to receive(:execute).and_return(mocked_data) - end - - expect(resolve_egress[:egress_nodes]).to eq(mocked_data) - end - context 'when data_transfer_monitoring is disabled' do before do stub_feature_flags(data_transfer_monitoring: false) |