diff options
Diffstat (limited to 'app')
36 files changed, 490 insertions, 204 deletions
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 |