diff options
70 files changed, 1065 insertions, 588 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index e442a2a8856..146522c5e76 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -2371,7 +2371,6 @@ Layout/ArgumentAlignment: - 'spec/helpers/avatars_helper_spec.rb' - 'spec/helpers/emoji_helper_spec.rb' - 'spec/helpers/feature_flags_helper_spec.rb' - - 'spec/helpers/ide_helper_spec.rb' - 'spec/helpers/namespaces_helper_spec.rb' - 'spec/helpers/notify_helper_spec.rb' - 'spec/helpers/page_layout_helper_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 5508542a599..0206346fa0a 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -4927,7 +4927,6 @@ Layout/LineLength: - 'spec/requests/groups/milestones_controller_spec.rb' - 'spec/requests/groups/settings/access_tokens_controller_spec.rb' - 'spec/requests/groups_controller_spec.rb' - - 'spec/requests/ide_controller_spec.rb' - 'spec/requests/jwt_controller_spec.rb' - 'spec/requests/lfs_http_spec.rb' - 'spec/requests/oauth/tokens_controller_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 1ad85791e69..d57f93a1719 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -1494,7 +1494,6 @@ RSpec/ContextWording: - 'spec/helpers/gitlab_routing_helper_spec.rb' - 'spec/helpers/groups/group_members_helper_spec.rb' - 'spec/helpers/groups_helper_spec.rb' - - 'spec/helpers/ide_helper_spec.rb' - 'spec/helpers/integrations_helper_spec.rb' - 'spec/helpers/jira_connect_helper_spec.rb' - 'spec/helpers/labels_helper_spec.rb' @@ -2575,7 +2574,6 @@ RSpec/ContextWording: - 'spec/requests/groups/settings/access_tokens_controller_spec.rb' - 'spec/requests/groups_controller_spec.rb' - 'spec/requests/health_controller_spec.rb' - - 'spec/requests/ide_controller_spec.rb' - 'spec/requests/jira_connect/installations_controller_spec.rb' - 'spec/requests/jira_connect/oauth_application_ids_controller_spec.rb' - 'spec/requests/jira_routing_spec.rb' diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml index 66de6a35092..bc3c41ae992 100644 --- a/.rubocop_todo/style/percent_literal_delimiters.yml +++ b/.rubocop_todo/style/percent_literal_delimiters.yml @@ -993,7 +993,6 @@ Style/PercentLiteralDelimiters: - 'spec/requests/api/unleash_spec.rb' - 'spec/requests/api/users_spec.rb' - 'spec/requests/api/wikis_spec.rb' - - 'spec/requests/ide_controller_spec.rb' - 'spec/requests/jwt_controller_spec.rb' - 'spec/requests/lfs_locks_api_spec.rb' - 'spec/requests/users_controller_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0fd64bd648e..f60c9678b67 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1739a8ca9a5786b4730620b742153f45e00cb094 +65769c7a58d3339fe94a809bf6fd34f2f300a700 diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue index a7331cc1fd3..1b7782c6860 100644 --- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue @@ -68,9 +68,8 @@ export default { variables() { return this.queryVariables; }, - update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) { + update({ project: { jobs: { nodes = [], pageInfo = {} } = {} } }) { this.pageInfo = pageInfo; - this.count = count; return nodes .map(mapArchivesToJobNodes) .map(mapBooleansToJobNodes) @@ -93,7 +92,6 @@ export default { data() { return { jobArtifacts: [], - count: 0, pageInfo: {}, expandedJobs: [], pagination: INITIAL_PAGINATION_STATE, @@ -110,7 +108,9 @@ export default { }; }, showPagination() { - return this.count > JOBS_PER_PAGE; + const { hasNextPage, hasPreviousPage } = this.pageInfo; + + return hasNextPage || hasPreviousPage; }, prevPage() { return Number(this.pageInfo.hasPreviousPage); diff --git a/app/assets/javascripts/work_items/components/notes/activity_filter.vue b/app/assets/javascripts/work_items/components/notes/activity_filter.vue index 71784d3a807..6d5535797ef 100644 --- a/app/assets/javascripts/work_items/components/notes/activity_filter.vue +++ b/app/assets/javascripts/work_items/components/notes/activity_filter.vue @@ -1,18 +1,35 @@ <script> import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { s__ } from '~/locale'; import Tracking from '~/tracking'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { ASC, DESC } from '~/notes/constants'; -import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, + TRACKING_CATEGORY_SHOW, + WORK_ITEM_NOTES_FILTER_KEY, +} from '~/work_items/constants'; -const SORT_OPTIONS = [ - { key: DESC, text: __('Newest first'), dataid: 'js-newest-first' }, - { key: ASC, text: __('Oldest first'), dataid: 'js-oldest-first' }, +const filterOptions = [ + { + key: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + text: s__('WorkItem|All activity'), + }, + { + key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + text: s__('WorkItem|Comments only'), + testid: 'comments-activity', + }, + { + key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, + text: s__('WorkItem|History only'), + testid: 'history-activity', + }, ]; export default { - SORT_OPTIONS, + filterOptions, components: { GlDropdown, GlDropdownItem, @@ -20,11 +37,6 @@ export default { }, mixins: [Tracking.mixin()], props: { - sortOrder: { - type: String, - default: ASC, - required: false, - }, loading: { type: Boolean, default: false, @@ -34,80 +46,74 @@ export default { type: String, required: true, }, - }, - data() { - return { - persistSortOrder: true, - }; + discussionFilter: { + type: String, + default: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + required: false, + }, }, computed: { tracking() { return { category: TRACKING_CATEGORY_SHOW, - label: 'item_track_notes_sorting', + label: 'item_track_notes_filtering', property: `type_${this.workItemType}`, }; }, - selectedSortOption() { - const isSortOptionValid = this.sortOrder === ASC || this.sortOrder === DESC; - return isSortOptionValid ? SORT_OPTIONS.find(({ key }) => this.sortOrder === key) : ASC; - }, getDropdownSelectedText() { return this.selectedSortOption.text; }, + selectedSortOption() { + return ( + filterOptions.find(({ key }) => this.discussionFilter === key) || + WORK_ITEM_NOTES_FILTER_ALL_NOTES + ); + }, }, methods: { - setDiscussionSortDirection(direction) { - this.$emit('updateSavedSortOrder', direction); + setDiscussionFilterOption(filterValue) { + this.$emit('changeFilter', filterValue); }, - fetchSortedDiscussions(direction) { - if (this.isSortDropdownItemActive(direction)) { + fetchFilteredDiscussions(filterValue) { + if (this.isSortDropdownItemActive(filterValue)) { return; } - this.track('notes_sort_order_changed'); - this.$emit('changeSortOrder', direction); + this.track('work_item_notes_filter_changed'); + this.$emit('changeFilter', filterValue); }, - isSortDropdownItemActive(sortDir) { - return sortDir === this.sortOrder; + isSortDropdownItemActive(discussionFilter) { + return discussionFilter === this.discussionFilter; }, }, - WORK_ITEM_NOTES_SORT_ORDER_KEY, + WORK_ITEM_NOTES_FILTER_KEY, }; </script> <template> - <div - id="discussion-preferences" - data-testid="discussion-preferences" - class="gl-display-inline-block gl-vertical-align-bottom gl-w-full gl-sm-w-auto" - > + <div class="gl-display-inline-block gl-vertical-align-bottom"> <local-storage-sync - :value="sortOrder" - :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY" - :persist="persistSortOrder" + :value="discussionFilter" + :storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY" as-string - @input="setDiscussionSortDirection" + @input="setDiscussionFilterOption" /> <gl-dropdown - :id="`discussion-preferences-dropdown-${workItemType}`" class="gl-xs-w-full" size="small" :text="getDropdownSelectedText" :disabled="loading" right > - <div id="discussion-sort"> - <gl-dropdown-item - v-for="{ text, key, dataid } in $options.SORT_OPTIONS" - :key="text" - :data-testid="dataid" - is-check-item - :is-checked="isSortDropdownItemActive(key)" - @click="fetchSortedDiscussions(key)" - > - {{ text }} - </gl-dropdown-item> - </div> + <gl-dropdown-item + v-for="{ text, key, testid } in $options.filterOptions" + :key="text" + :data-testid="testid" + is-check-item + :is-checked="isSortDropdownItemActive(key)" + @click="fetchFilteredDiscussions(key)" + > + {{ text }} + </gl-dropdown-item> </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/work_items/components/notes/activity_sort.vue b/app/assets/javascripts/work_items/components/notes/activity_sort.vue new file mode 100644 index 00000000000..bfbb2b65346 --- /dev/null +++ b/app/assets/javascripts/work_items/components/notes/activity_sort.vue @@ -0,0 +1,99 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Tracking from '~/tracking'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { ASC, DESC } from '~/notes/constants'; +import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants'; + +const sortOptions = [ + { key: DESC, text: __('Newest first'), testid: 'newest-first' }, + { key: ASC, text: __('Oldest first') }, +]; + +export default { + sortOptions, + components: { + GlDropdown, + GlDropdownItem, + LocalStorageSync, + }, + mixins: [Tracking.mixin()], + props: { + sortOrder: { + type: String, + default: ASC, + required: false, + }, + loading: { + type: Boolean, + default: false, + required: false, + }, + workItemType: { + type: String, + required: true, + }, + }, + computed: { + tracking() { + return { + category: TRACKING_CATEGORY_SHOW, + label: 'item_track_notes_sorting', + property: `type_${this.workItemType}`, + }; + }, + selectedSortOption() { + return sortOptions.find(({ key }) => this.sortOrder === key) || ASC; + }, + getDropdownSelectedText() { + return this.selectedSortOption.text; + }, + }, + methods: { + setDiscussionSortDirection(direction) { + this.$emit('changeSort', direction); + }, + fetchSortedDiscussions(direction) { + if (this.isSortDropdownItemActive(direction)) { + return; + } + this.track('work_item_notes_sort_order_changed'); + this.$emit('changeSort', direction); + }, + isSortDropdownItemActive(sortDir) { + return sortDir === this.sortOrder; + }, + }, + WORK_ITEM_NOTES_SORT_ORDER_KEY, +}; +</script> + +<template> + <div class="gl-display-inline-block gl-vertical-align-bottom"> + <local-storage-sync + :value="sortOrder" + :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY" + as-string + @input="setDiscussionSortDirection" + /> + <gl-dropdown + class="gl-xs-w-full" + size="small" + :text="getDropdownSelectedText" + :disabled="loading" + right + > + <gl-dropdown-item + v-for="{ text, key, testid } in $options.sortOptions" + :key="text" + :data-testid="testid" + is-check-item + :is-checked="isSortDropdownItemActive(key)" + @click="fetchSortedDiscussions(key)" + > + {{ text }} + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue new file mode 100644 index 00000000000..07e25312f87 --- /dev/null +++ b/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue @@ -0,0 +1,61 @@ +<script> +import { GlButton, GlIcon, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, +} from '~/work_items/constants'; + +export default { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + i18n: { + information: s__( + "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options.", + ), + }, + components: { + GlButton, + GlIcon, + GlSprintf, + }, + methods: { + selectFilter(value) { + this.$emit('changeFilter', value); + }, + }, +}; +</script> + +<template> + <li class="timeline-entry note note-wrapper discussion-filter-note"> + <div class="timeline-icon gl-display-none gl-lg-display-flex"> + <gl-icon name="comment" /> + </div> + <div class="timeline-content gl-pl-8"> + <gl-sprintf :message="$options.i18n.information"> + <template #bold="{ content }"> + <b>{{ content }}</b> + </template> + </gl-sprintf> + + <div class="discussion-filter-actions"> + <gl-button + class="gl-mr-2 gl-mt-3" + data-testid="show-all-activity" + @click="selectFilter($options.WORK_ITEM_NOTES_FILTER_ALL_NOTES)" + > + {{ __('Show all activity') }} + </gl-button> + <gl-button + class="gl-mt-3" + data-testid="show-comments-only" + @click="selectFilter($options.WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS)" + > + {{ __('Show comments only') }} + </gl-button> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue new file mode 100644 index 00000000000..e700d5353e2 --- /dev/null +++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue @@ -0,0 +1,67 @@ +<script> +import ActivitySort from '~/work_items/components/notes/activity_sort.vue'; +import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; +import { s__ } from '~/locale'; +import { ASC } from '~/notes/constants'; +import { WORK_ITEM_NOTES_FILTER_ALL_NOTES } from '~/work_items/constants'; + +export default { + i18n: { + activityLabel: s__('WorkItem|Activity'), + }, + components: { + ActivitySort, + ActivityFilter, + }, + props: { + disableActivityFilterSort: { + type: Boolean, + required: true, + }, + sortOrder: { + type: String, + default: ASC, + required: false, + }, + workItemType: { + type: String, + required: true, + }, + discussionFilter: { + type: String, + default: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + required: false, + }, + }, + methods: { + changeNotesSortOrder(direction) { + this.$emit('changeSort', direction); + }, + filterDiscussions(filterValue) { + this.$emit('changeFilter', filterValue); + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-pb-3 gl-align-items-center" + > + <h3 class="gl-font-base gl-m-0">{{ $options.i18n.activityLabel }}</h3> + <div class="gl-display-flex gl-gap-3"> + <activity-filter + :loading="disableActivityFilterSort" + :work-item-type="workItemType" + :discussion-filter="discussionFilter" + @changeFilter="filterDiscussions" + /> + <activity-sort + :loading="disableActivityFilterSort" + :sort-order="sortOrder" + :work-item-type="workItemType" + @changeSort="changeNotesSortOrder" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue index aa6dd9b5184..331d0b5e8d0 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -1,11 +1,17 @@ <script> import { GlSkeletonLoader, GlModal } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; -import { s__, __ } from '~/locale'; +import { __ } from '~/locale'; import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants'; import SystemNote from '~/work_items/components/notes/system_note.vue'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; -import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants'; +import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; +import { + i18n, + DEFAULT_PAGE_SIZE_NOTES, + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, +} from '~/work_items/constants'; import { ASC, DESC } from '~/notes/constants'; import { getWorkItemNotesQuery } from '~/work_items/utils'; import { @@ -13,6 +19,7 @@ import { updateCacheAfterDeletingNote, } from '~/work_items/graphql/cache_utils'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; +import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue'; import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql'; import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql'; import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql'; @@ -20,9 +27,6 @@ import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation import WorkItemAddNote from './notes/work_item_add_note.vue'; export default { - i18n: { - ACTIVITY_LABEL: s__('WorkItem|Activity'), - }, loader: { repeat: 10, width: 1000, @@ -31,10 +35,11 @@ export default { components: { GlSkeletonLoader, GlModal, - ActivityFilter, SystemNote, WorkItemAddNote, WorkItemDiscussion, + WorkItemNotesActivityHeader, + WorkItemHistoryOnlyFilterNote, }, props: { workItemId: { @@ -65,6 +70,7 @@ export default { perPage: DEFAULT_PAGE_SIZE_NOTES, sortOrder: ASC, noteToDelete: null, + discussionFilter: WORK_ITEM_NOTES_FILTER_ALL_NOTES, }; }, computed: { @@ -83,7 +89,7 @@ export default { showLoadingMoreSkeleton() { return this.isLoadingMore && !this.changeNotesSortOrderAfterLoading; }, - disableActivityFilter() { + disableActivityFilterSort() { return this.initialLoading || this.isLoadingMore; }, formAtTop() { @@ -102,10 +108,27 @@ export default { notesArray() { const notes = this.workItemNotes?.nodes || []; + const visibleNotes = notes.filter((note) => { + const isSystemNote = this.isSystemNote(note); + + if (this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS && isSystemNote) { + return false; + } + + if (this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_HISTORY && !isSystemNote) { + return false; + } + + return true; + }); + if (this.sortOrder === DESC) { - return [...notes].reverse(); + return [...visibleNotes].reverse(); } - return notes; + return visibleNotes; + }, + commentsDisabled() { + return this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_HISTORY; }, }, apollo: { @@ -210,6 +233,9 @@ export default { changeNotesSortOrder(direction) { this.sortOrder = direction; }, + filterDiscussions(filterValue) { + this.discussionFilter = filterValue; + }, async fetchMoreNotes() { this.isLoadingMore = true; // copied from discussions batch logic - every fetchMore call has a higher @@ -271,17 +297,14 @@ export default { <template> <div class="gl-border-t gl-mt-5 work-item-notes"> - <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"> - <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label> - <activity-filter - class="gl-min-h-5 gl-pb-3" - :loading="disableActivityFilter" - :sort-order="sortOrder" - :work-item-type="workItemType" - @changeSortOrder="changeNotesSortOrder" - @updateSavedSortOrder="changeNotesSortOrder" - /> - </div> + <work-item-notes-activity-header + :sort-order="sortOrder" + :disable-activity-filter-sort="disableActivityFilterSort" + :work-item-type="workItemType" + :discussion-filter="discussionFilter" + @changeSort="changeNotesSortOrder" + @changeFilter="filterDiscussions" + /> <div v-if="initialLoading" class="gl-mt-5"> <gl-skeleton-loader v-for="index in $options.loader.repeat" @@ -298,7 +321,7 @@ export default { <template v-if="!initialLoading"> <ul class="notes main-notes-list timeline gl-clearfix!"> <work-item-add-note - v-if="formAtTop" + v-if="formAtTop && !commentsDisabled" v-bind="workItemCommentFormProps" @error="$emit('error', $event)" /> @@ -325,10 +348,14 @@ export default { </template> <work-item-add-note - v-if="!formAtTop" + v-if="!formAtTop && !commentsDisabled" v-bind="workItemCommentFormProps" @error="$emit('error', $event)" /> + <work-item-history-only-filter-note + v-if="commentsDisabled" + @changeFilter="filterDiscussions" + /> </ul> </template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 81f9bf04bc8..b372f2d6f7b 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -176,3 +176,9 @@ export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10; export const DEFAULT_PAGE_SIZE_NOTES = 30; export const WORK_ITEM_NOTES_SORT_ORDER_KEY = 'sort_direction_work_item'; + +export const WORK_ITEM_NOTES_FILTER_ALL_NOTES = 'ALL_NOTES'; +export const WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS = 'ONLY_COMMENTS'; +export const WORK_ITEM_NOTES_FILTER_ONLY_HISTORY = 'ONLY_HISTORY'; + +export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item'; diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 1b3d9223502..18c6f0bb9d3 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -10,7 +10,6 @@ class IdeController < ApplicationController before_action do push_frontend_feature_flag(:build_service_proxy) push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab) - define_index_vars end feature_category :web_ide @@ -22,6 +21,7 @@ class IdeController < ApplicationController if project Gitlab::Tracking.event(self.class.to_s, 'web_ide_views', namespace: project.namespace, user: current_user) + @fork_info = fork_info(project, params[:branch]) end render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? } @@ -33,16 +33,6 @@ class IdeController < ApplicationController render_404 unless can?(current_user, :read_project, project) end - def define_index_vars - return unless project - - @branch = params[:branch] - @path = params[:path] - @merge_request = params[:merge_request_id] - @learn_gitlab_source = params[:learn_gitlab_source] - @fork_info = fork_info(project, @branch) - end - def fork_info(project, branch) return if can?(current_user, :push_code, project) diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 296fe6856ac..063eef41f77 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -1,21 +1,26 @@ # frozen_string_literal: true module IdeHelper - def ide_data(project:, branch:, path:, merge_request:, fork_info:, learn_gitlab_source:) - { + # Overridden in EE + def ide_data(project:, fork_info:, params:) + base_data = { 'can-use-new-web-ide' => can_use_new_web_ide?.to_s, 'use-new-web-ide' => use_new_web_ide?.to_s, 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'), 'user-preferences-path' => profile_preferences_path, - 'branch-name' => branch, - 'file-path' => path, - 'fork-info' => fork_info&.to_json, 'editor-font-src-url' => font_url('jetbrains-mono/JetBrainsMono.woff2'), 'editor-font-family' => 'JetBrains Mono', - 'editor-font-format' => 'woff2', - 'merge-request' => merge_request, - 'learn-gitlab-source' => (!!learn_gitlab_source).to_s + 'editor-font-format' => 'woff2' }.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project)) + + return base_data unless project + + base_data.merge( + 'fork-info' => fork_info&.to_json, + 'branch-name' => params[:branch], + 'file-path' => params[:path], + 'merge-request' => params[:merge_request_id] + ) end def can_use_new_web_ide? @@ -77,3 +82,5 @@ module IdeHelper current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance') end end + +IdeHelper.prepend_mod_with('IdeHelper') diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 81d083bd082..0d93aff2bae 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -156,7 +156,7 @@ module IssuablesHelper end output << content_tag(:strong) do - author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline") + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block") author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none") author_output << issuable_meta_author_slot(issuable.author, css_class: 'ml-1') diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb index 89211ed6a3e..201007863b2 100644 --- a/app/helpers/nav/new_dropdown_helper.rb +++ b/app/helpers/nav/new_dropdown_helper.rb @@ -2,7 +2,7 @@ module Nav module NewDropdownHelper - def new_dropdown_view_model(group:, project:, with_context: false) + def new_dropdown_view_model(group:, project:) return unless current_user menu_sections = [] @@ -10,10 +10,8 @@ module Nav if project&.persisted? menu_sections.push(project_menu_section(project)) - data[:context] = project if with_context elsif group&.persisted? menu_sections.push(group_menu_section(group)) - data[:context] = group if with_context end menu_sections.push(general_menu_section) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 0d98c5a176a..17b225c5e9b 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -60,6 +60,14 @@ class NotifyPreview < ActionMailer::Preview end end + def access_token_created_email + Notify.access_token_created_email(user, 'token_name').message + end + + def access_token_revoked_email + Notify.access_token_revoked_email(user, 'token_name').message + end + def new_mention_in_merge_request_email Notify.new_mention_in_merge_request_email(user.id, merge_request.id, user.id).message end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 89a3d269a43..92ab2e1af25 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -177,6 +177,8 @@ module Ci where(file_type: self.erasable_file_types) end + scope :non_trace, -> { where.not(file_type: [:trace]) } + scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) } scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) } scope :order_expired_asc, -> { order(expire_at: :asc) } diff --git a/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb b/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb index 7862774473c..738fa19e29b 100644 --- a/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb +++ b/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb @@ -30,10 +30,7 @@ module Ci return ServiceResponse.error(message: 'Not all artifacts belong to requested project') end - result = Ci::JobArtifacts::DestroyBatchService.new( - job_artifact_scope, - skip_trace_artifacts: false - ).execute + result = Ci::JobArtifacts::DestroyBatchService.new(job_artifact_scope).execute destroyed_artifacts_count = result.fetch(:destroyed_artifacts_count) destroyed_ids = result.fetch(:destroyed_ids) diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb index 30683475ad2..57b95e59d7d 100644 --- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb +++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb @@ -35,7 +35,7 @@ module Ci def destroy_unlocked_job_artifacts loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do - artifacts = Ci::JobArtifact.expired_before(@start_at).artifact_unlocked.limit(BATCH_SIZE) + artifacts = Ci::JobArtifact.expired_before(@start_at).non_trace.artifact_unlocked.limit(BATCH_SIZE) service_response = destroy_batch(artifacts) @removed_artifacts_count += service_response[:destroyed_artifacts_count] end diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb index 94ecfa96be0..81cbeb31711 100644 --- a/app/services/ci/job_artifacts/destroy_batch_service.rb +++ b/app/services/ci/job_artifacts/destroy_batch_service.rb @@ -17,11 +17,10 @@ module Ci # +pick_up_at+:: When to pick up for deletion of files # Returns: # +Hash+:: A hash with status and destroyed_artifacts_count keys - def initialize(job_artifacts, pick_up_at: nil, skip_projects_on_refresh: false, skip_trace_artifacts: true) + def initialize(job_artifacts, pick_up_at: nil, skip_projects_on_refresh: false) @job_artifacts = job_artifacts.with_destroy_preloads.to_a @pick_up_at = pick_up_at @skip_projects_on_refresh = skip_projects_on_refresh - @skip_trace_artifacts = skip_trace_artifacts @destroyed_ids = [] end @@ -33,8 +32,6 @@ module Ci track_artifacts_undergoing_stats_refresh end - exclude_trace_artifacts if @skip_trace_artifacts - if @job_artifacts.empty? return success(destroyed_ids: @destroyed_ids, destroyed_artifacts_count: 0, statistics_updates: {}) end @@ -119,11 +116,6 @@ module Ci end end - # Traces should never be destroyed. - def exclude_trace_artifacts - _trace_artifacts, @job_artifacts = @job_artifacts.partition(&:trace?) - end - def track_artifacts_undergoing_stats_refresh project_ids = @job_artifacts.find_all do |artifact| artifact.project.refreshing_build_artifacts_size? diff --git a/app/services/releases/links/base_service.rb b/app/services/releases/links/base_service.rb index 939de982db4..8bab258f80a 100644 --- a/app/services/releases/links/base_service.rb +++ b/app/services/releases/links/base_service.rb @@ -2,6 +2,10 @@ module Releases module Links + REASON_BAD_REQUEST = :bad_request + REASON_NOT_FOUND = :not_found + REASON_FORBIDDEN = :forbidden + class BaseService attr_accessor :release, :current_user, :params diff --git a/app/services/releases/links/create_service.rb b/app/services/releases/links/create_service.rb index c73c9f40254..94823c54596 100644 --- a/app/services/releases/links/create_service.rb +++ b/app/services/releases/links/create_service.rb @@ -4,14 +4,14 @@ module Releases module Links class CreateService < BaseService def execute - return ServiceResponse.error(message: _('Access Denied')) unless allowed? + return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed? link = release.links.create(allowed_params) if link.persisted? ServiceResponse.success(payload: { link: link }) else - ServiceResponse.error(message: link.errors.full_messages) + ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages) end end diff --git a/app/services/releases/links/destroy_service.rb b/app/services/releases/links/destroy_service.rb index 9edde2f357b..1c1158017bb 100644 --- a/app/services/releases/links/destroy_service.rb +++ b/app/services/releases/links/destroy_service.rb @@ -4,13 +4,13 @@ module Releases module Links class DestroyService < BaseService def execute(link) - return ServiceResponse.error(message: _('Access Denied')) unless allowed? - return ServiceResponse.error(message: _('Link does not exist')) unless link + return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed? + return ServiceResponse.error(reason: REASON_NOT_FOUND, message: _('Link does not exist')) unless link if link.destroy ServiceResponse.success(payload: { link: link }) else - ServiceResponse.error(message: link.errors.full_messages) + ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages) end end diff --git a/app/services/releases/links/update_service.rb b/app/services/releases/links/update_service.rb index f50cde5c5a9..c29de86f31b 100644 --- a/app/services/releases/links/update_service.rb +++ b/app/services/releases/links/update_service.rb @@ -4,13 +4,13 @@ module Releases module Links class UpdateService < BaseService def execute(link) - return ServiceResponse.error(message: _('Access Denied')) unless allowed? - return ServiceResponse.error(message: _('Link does not exist')) unless link + return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed? + return ServiceResponse.error(reason: REASON_NOT_FOUND, message: _('Link does not exist')) unless link if link.update(allowed_params) ServiceResponse.success(payload: { link: link }) else - ServiceResponse.error(message: link.errors.full_messages) + ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages) end end diff --git a/app/views/groups/_invite_members_top_nav_link.html.haml b/app/views/groups/_invite_members_top_nav_link.html.haml index e419c479bca..35a8d4d9944 100644 --- a/app/views/groups/_invite_members_top_nav_link.html.haml +++ b/app/views/groups/_invite_members_top_nav_link.html.haml @@ -3,5 +3,3 @@ - data[:icon] = local_assigns.fetch(:icon) .js-invite-members-trigger{ data: data } - -= render 'groups/invite_members_modal', group: local_assigns.fetch(:context) diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml index 091d7e7a4f1..eb6d5668807 100644 --- a/app/views/ide/_show.html.haml +++ b/app/views/ide/_show.html.haml @@ -7,11 +7,6 @@ - content_for :prefetch_asset_tags do - webpack_preload_asset_tag('monaco') -- data = ide_data(project: @project, - branch: @branch, - path: @path, - merge_request: @merge_request, - fork_info: @fork_info, - learn_gitlab_source: @learn_gitlab_source) +- data = ide_data(project: @project, fork_info: @fork_info, params: params) = render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the GitLab IDE...') } diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 28579c7f7ea..40ec1ff199b 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -16,6 +16,9 @@ :plain window.uploads_path = "#{group_uploads_path(@group)}"; +- content_for :before_content do + = render 'groups/invite_members_modal', group: @group + = dispensable_render_if_exists "shared/web_hooks/group_web_hook_disabled_alert" = dispensable_render_if_exists "shared/free_user_cap_alert", source: @group diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml index 42c2fd645da..50a2b45aa7e 100644 --- a/app/views/layouts/header/_new_dropdown.html.haml +++ b/app/views/layouts/header/_new_dropdown.html.haml @@ -1,4 +1,4 @@ -- view_model = new_dropdown_view_model(project: @project, group: @group, with_context: true) +- view_model = new_dropdown_view_model(project: @project, group: @group) - menu_sections = view_model.fetch(:menu_sections) - title = view_model.fetch(:title) - show_headers = menu_sections.length > 1 @@ -28,8 +28,7 @@ %li< - if menu_item.fetch(:partial).present? = render partial: menu_item.fetch(:partial), - locals: { context: view_model[:context], - display_text: menu_item.fetch(:title), + locals: { display_text: menu_item.fetch(:title), icon: menu_item.fetch(:icon), data: menu_item.fetch(:data) } - else diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 6ad6696b313..09fa8575106 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -18,6 +18,9 @@ :plain window.uploads_path = "#{project_uploads_path(project)}"; +- content_for :before_content do + = render 'projects/invite_members_modal', project: @project + = dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert" = dispensable_render_if_exists "projects/free_user_cap_alert", project: @project diff --git a/app/views/projects/_invite_members_top_nav_link.html.haml b/app/views/projects/_invite_members_top_nav_link.html.haml index d2e68325a09..35a8d4d9944 100644 --- a/app/views/projects/_invite_members_top_nav_link.html.haml +++ b/app/views/projects/_invite_members_top_nav_link.html.haml @@ -3,5 +3,3 @@ - data[:icon] = local_assigns.fetch(:icon) .js-invite-members-trigger{ data: data } - -= render 'projects/invite_members_modal', project: local_assigns.fetch(:context) diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml index 32b093bb95c..c0362f3e85d 100644 --- a/app/views/projects/branch_rules/_show.html.haml +++ b/app/views/projects/branch_rules/_show.html.haml @@ -10,6 +10,7 @@ = expanded ? _('Collapse') : _('Expand') %p = _('Define rules for who can push, merge, and the required approvals for each branch.') + = link_to(_('Leave feadback.'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/388149', target: '_blank', rel: 'noopener noreferrer') .settings-content.gl-pr-0 #js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project), show_code_owners: show_code_owners.to_s, show_status_checks: show_status_checks.to_s, show_approvers: show_approvers.to_s } } diff --git a/data/deprecations/15-1-deprecate-maintainer_note.yml b/data/deprecations/15-1-deprecate-maintainer_note.yml deleted file mode 100644 index 175f85e997f..00000000000 --- a/data/deprecations/15-1-deprecate-maintainer_note.yml +++ /dev/null @@ -1,15 +0,0 @@ -- title: "REST API Runner maintainer_note" # (required) The name of the feature to be deprecated - announcement_milestone: "15.1" # (required) The milestone when this feature was first announced as deprecated. - removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed - breaking_change: true # (required) If this deprecation is a breaking change, set this value to true - reporter: pedropombeiro # (required) GitLab username of the person reporting the deprecation - stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth - issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363192 # (required) Link to the deprecation issue in GitLab - body: | # (required) Do not modify this line, instead modify the lines below. - The `maintainer_note` argument in the `POST /runners` REST endpoint was deprecated in GitLab 14.8 and replaced with the `maintenance_note` argument. - The `maintainer_note` argument will be removed in GitLab 16.0. -# The following items are not published on the docs page, but may be used in the future. - tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] - documentation_url: https://docs.gitlab.com/ee/api/runners.html#register-a-new-runner # (optional) This is a link to the current documentation page - image_url: # (optional) This is a link to a thumbnail image depicting the feature - video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg diff --git a/db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb b/db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb new file mode 100644 index 00000000000..feda6971a85 --- /dev/null +++ b/db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOnExpiredUnlockedNonTraceJobArtifacts < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'index_ci_job_artifacts_expire_at_unlocked_non_trace' + + def up + add_concurrent_index :ci_job_artifacts, :expire_at, + name: INDEX_NAME, + where: 'locked = 0 AND file_type != 3 AND expire_at IS NOT NULL' + end + + def down + remove_concurrent_index_by_name :ci_job_artifacts, INDEX_NAME + end +end diff --git a/db/schema_migrations/20230301065107 b/db/schema_migrations/20230301065107 new file mode 100644 index 00000000000..495cefad9d2 --- /dev/null +++ b/db/schema_migrations/20230301065107 @@ -0,0 +1 @@ +7e464616bdef6e225fdd31db84c4c32e223dffb81e13f1d6a5c85c2cd0a16144
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c1e5eca5283..1198a673376 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29501,6 +29501,8 @@ CREATE UNIQUE INDEX index_ci_instance_variables_on_key ON ci_instance_variables CREATE INDEX index_ci_job_artifact_states_on_job_artifact_id ON ci_job_artifact_states USING btree (job_artifact_id); +CREATE INDEX index_ci_job_artifacts_expire_at_unlocked_non_trace ON ci_job_artifacts USING btree (expire_at) WHERE ((locked = 0) AND (file_type <> 3) AND (expire_at IS NOT NULL)); + CREATE INDEX index_ci_job_artifacts_for_terraform_reports ON ci_job_artifacts USING btree (project_id, id) WHERE (file_type = 18); CREATE INDEX index_ci_job_artifacts_id_for_terraform_reports ON ci_job_artifacts USING btree (id) WHERE (file_type = 18); diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md index 926500090dc..1991d0b64cc 100644 --- a/doc/administration/dedicated/index.md +++ b/doc/administration/dedicated/index.md @@ -27,6 +27,7 @@ To request the creation of a new GitLab Dedicated environment for your organizat - Desired instance subdomain: The main domain for GitLab Dedicated instances is `gitlab-dedicated.com`. You get to choose the subdomain name where your instance is accessible from (for example, `customer_name.gitlab-dedicated.com`). - Initial storage: Initial storage size for your repositories in GB. - Availability Zone IDs for PrivateLink: If you plan to later add a PrivateLink connection (either [inbound](#inbound-private-link) or [outbound](#outbound-private-link)) to your environment, and you require the connections to be available in specific Availability Zones, you must provide up to two [Availability Zone IDs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#az-ids) during onboarding. If not specified, GitLab will select two random Availability Zone IDs in which the connections will be available. +- [KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html) for encrypted AWS services (if you are using that functionality). ### Maintenance window @@ -45,6 +46,111 @@ To change or update the configuration for your GitLab Dedicated instance, open a The turnaround time for processing configuration change requests is [documented in the GitLab handbook](https://about.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/#handling-configuration-changes-for-tenant-environments). +### Encrypted Data At Rest (BYOK) + +If you want your GitLab data to be encrypted at rest, the KMS keys used must be accessible by GitLab services. KMS keys can be used in two modes for this purpose: + +1. Per-service KMS keys (Backup, EBS, RDS, S3), or +1. One KMS key for all services. + +If you use a key per service, all services must be encrypted at rest. Selective enablement of this feature is not supported. + +The keys provided have to reside in the same primary and secondary region specified during [onboarding](#onboarding). + +For instructions on how to create and manage KMS keys, visit [Managing keys](https://docs.aws.amazon.com/kms/latest/developerguide/getting-started.html) in the AWS KMS documentation. + +To create a KMS key using the AWS Console: + +1. In `Configure key`, select: + 1. Key type: **Symmetrical** + 1. Key usage: **Encrypt and decrypt** + 1. `Advanced options`: + 1. Key material origin: **KMS** + 1. Regionality: **Multi-Region key** +1. Enter your values for key alias, description, and tags. +1. Select Key administrators (optionally allow or deny key administrators to delete the key). +1. For Key usage permissions, add the GitLab AWS account using the **Other AWS accounts** dialog. + +The last page asks you to confirm the KMS key policy. It should look similar to the following example, populated with your account IDs and usernames: + +```json +{ + "Version": "2012-10-17", + "Id": "byok-key-policy", + "Statement": [ + { + "Sid": "Enable IAM User Permissions", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::<CUSTOMER-ACCOUNT-ID>:root" + }, + "Action": "kms:*", + "Resource": "*" + }, + { + "Sid": "Allow access for Key Administrators", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam::<CUSTOMER-ACCOUNT-ID>:user/<CUSTOMER-USER>" + ] + }, + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:TagResource", + "kms:UntagResource", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:ReplicateKey", + "kms:UpdatePrimaryRegion" + ], + "Resource": "*" + }, + { + "Sid": "Allow use of the key", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam::<GITLAB-ACCOUNT-ID>:root" + ] + }, + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ], + "Resource": "*" + }, + { + "Sid": "Allow attachment of persistent resources", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam::<GITLAB-ACCOUNT-ID>:root" + ] + }, + "Action": [ + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ], + "Resource": "*" + } + ] +} +``` + ### Inbound Private Link [AWS Private Link](https://docs.aws.amazon.com/vpc/latest/privatelink/what-is-privatelink.html) allows users and applications in your VPC on AWS to securely connect to the GitLab Dedicated endpoint without network traffic going over the public internet. diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md index 7a5c846bdbc..b11524083b1 100644 --- a/doc/administration/get_started.md +++ b/doc/administration/get_started.md @@ -1,7 +1,7 @@ --- -info: For assistance with this CSM Onboarding page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. stage: none -group: unassigned +group: Tutorials --- # Get started administering GitLab **(FREE)** diff --git a/doc/administration/package_information/supported_os.md b/doc/administration/package_information/supported_os.md index da16a6c2012..f92d57c0035 100644 --- a/doc/administration/package_information/supported_os.md +++ b/doc/administration/package_information/supported_os.md @@ -56,6 +56,14 @@ although [new versions have been released](https://about.gitlab.com/releases/cat of the [Linux package install guide](https://about.gitlab.com/install/#content). Future GitLab upgrades are fetched according to your upgraded OS. +## Update both GitLab and the operating system + +To upgrade both the operating system (OS) and GitLab: + +1. Upgrade the OS. +1. Check if it's necessary to [update the GitLab package sources](#update-gitlab-package-sources-after-upgrading-the-os). +1. [Upgrade GitLab](../../update/index.md). + ## Packages for ARM64 > [Introduced](https://gitlab.com/gitlab-org/gitlab-omnibus-builder/-/issues/27) in GitLab 13.4. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 1788d503a54..5a2a1678a08 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -1908,21 +1908,6 @@ The [`project_fingerprint`](https://gitlab.com/groups/gitlab-org/-/epics/2791) a </div> -<div class="deprecation removal-160 breaking-change"> - -### REST API Runner maintainer_note - -Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span> - -WARNING: -This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). -Review the details carefully before upgrading. - -The `maintainer_note` argument in the `POST /runners` REST endpoint was deprecated in GitLab 14.8 and replaced with the `maintenance_note` argument. -The `maintainer_note` argument will be removed in GitLab 16.0. - -</div> - <div class="deprecation removal-153"> ### Vulnerability Report sort by Tool diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index 66115baf120..311fcf9aba1 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -65,14 +65,14 @@ module API end route_setting :authentication, job_token_allowed: true post 'links' do - authorize! :create_release, release - result = ::Releases::Links::CreateService .new(release, current_user, declared_params(include_missing: false)) .execute if result.success? present result.payload[:link], with: Entities::Releases::Link + elsif result.reason == ::Releases::Links::REASON_FORBIDDEN + forbidden! else render_api_error!(result.message, 400) end @@ -121,14 +121,14 @@ module API end route_setting :authentication, job_token_allowed: true put do - authorize! :update_release, release - result = ::Releases::Links::UpdateService .new(release, current_user, declared_params(include_missing: false)) .execute(link) if result.success? present result.payload[:link], with: Entities::Releases::Link + elsif result.reason == ::Releases::Links::REASON_FORBIDDEN + forbidden! else render_api_error!(result.message, 400) end @@ -145,14 +145,14 @@ module API end route_setting :authentication, job_token_allowed: true delete do - authorize! :destroy_release, release - result = ::Releases::Links::DestroyService .new(release, current_user) .execute(link) if result.success? present result.payload[:link], with: Entities::Releases::Link + elsif result.reason == ::Releases::Links::REASON_FORBIDDEN + forbidden! else render_api_error!(result.message, 400) end diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb index 33e7724bf9b..7ab7dc3d64e 100644 --- a/lib/gitlab/ci/config/external/file/component.rb +++ b/lib/gitlab/ci/config/external/file/component.rb @@ -15,7 +15,7 @@ module Gitlab end def matching? - super && ::Feature.enabled?(:ci_include_components, context.project) + super && ::Feature.enabled?(:ci_include_components, context.project&.root_namespace) end def content diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 271da2edbc9..c3a7b384172 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -367,12 +367,12 @@ module Gitlab def foreign_key_exists?(source, target = nil, **options) # This if block is necessary because foreign_key_exists? is called in down migrations that may execute before - # the postgres_foreign_keys view had necessary columns added, or even before the view existed. + # the postgres_foreign_keys view had necessary columns added. # In that case, we revert to the previous behavior of this method. # The behavior in the if block has a bug: it always returns false if the fk being checked has multiple columns. # This can be removed after init_schema.rb passes 20221122210711_add_columns_to_postgres_foreign_keys.rb # Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/386796 - if ActiveRecord::Migrator.current_version < 20221122210711 + unless connection.column_exists?('postgres_foreign_keys', 'constrained_table_name') return foreign_keys(source).any? do |foreign_key| tables_match?(target.to_s, foreign_key.to_table.to_s) && options_match?(foreign_key.options, options) diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 84f7338a9ce..e8e8bb6275d 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -466,7 +466,7 @@ module Gitlab # HTML comment line: # <!-- some commented text --> - ^<!--\ .*\ -->\ *$ + ^<!--\ .*?\ -->\ *$ ) }mx.freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f77f1a61811..6a8ed0085a3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25294,6 +25294,9 @@ msgstr "" msgid "Leave edit mode? All unsaved changes will be lost." msgstr "" +msgid "Leave feadback." +msgstr "" + msgid "Leave group" msgstr "" @@ -48872,6 +48875,9 @@ msgstr "" msgid "WorkItem|Add to milestone" msgstr "" +msgid "WorkItem|All activity" +msgstr "" + msgid "WorkItem|Are you sure you want to cancel editing?" msgstr "" @@ -48895,6 +48901,9 @@ msgstr "" msgid "WorkItem|Closed" msgstr "" +msgid "WorkItem|Comments only" +msgstr "" + msgid "WorkItem|Convert to task" msgstr "" @@ -48928,6 +48937,9 @@ msgstr "" msgid "WorkItem|Health status" msgstr "" +msgid "WorkItem|History only" +msgstr "" + msgid "WorkItem|Incident" msgstr "" @@ -49087,6 +49099,9 @@ msgstr "" msgid "WorkItem|Work item not found" msgstr "" +msgid "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options." +msgstr "" + msgid "Would you like to create a new branch?" msgstr "" diff --git a/package.json b/package.json index b259228d18f..2bf5ac780d9 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", - "@gitlab/svgs": "3.22.0", + "@gitlab/svgs": "3.23.0", "@gitlab/ui": "56.2.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230223005157", diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 451f3d56af7..3247acedaa5 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -15,6 +15,7 @@ ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true' RSpec.configure(&:disable_monkey_patching!) require 'active_support/all' +require 'pry' require_relative 'rails_autoload' require_relative '../config/settings' diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js index 790b082c10b..44c242fa2cb 100644 --- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js @@ -74,7 +74,14 @@ describe('JobArtifactsTable component', () => { ]; } const getJobArtifactsResponseThatPaginates = { - data: { project: { jobs: { nodes: enoughJobsToPaginate } } }, + data: { + project: { + jobs: { + nodes: enoughJobsToPaginate, + pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo, hasNextPage: true }, + }, + }, + }, }; const job = getJobArtifactsResponse.data.project.jobs.nodes[0]; @@ -316,7 +323,7 @@ describe('JobArtifactsTable component', () => { }); describe('pagination', () => { - const { pageInfo } = getJobArtifactsResponse.data.project.jobs; + const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs; const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates); beforeEach(async () => { @@ -324,10 +331,7 @@ describe('JobArtifactsTable component', () => { { getJobArtifactsQuery: query, }, - { - count: enoughJobsToPaginate.length, - pageInfo, - }, + { pageInfo }, ); await waitForPromises(); diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js index eb4bcbf942b..86c4ad9b361 100644 --- a/spec/frontend/work_items/components/notes/activity_filter_spec.js +++ b/spec/frontend/work_items/components/notes/activity_filter_spec.js @@ -1,25 +1,33 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { ASC, DESC } from '~/notes/constants'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + TRACKING_CATEGORY_SHOW, +} from '~/work_items/constants'; import { mockTracking } from 'helpers/tracking_helper'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -describe('Activity Filter', () => { +describe('Work Item Activity/Discussions Filtering', () => { let wrapper; const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const findDropdown = () => wrapper.findComponent(GlDropdown); const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first'); + const findOnlyCommentsItem = () => wrapper.findByTestId('comments-activity'); + const findOnlyHistoryItem = () => wrapper.findByTestId('history-activity'); - const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => { + const createComponent = ({ + discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES, + loading = false, + workItemType = 'Task', + } = {}) => { wrapper = shallowMountExtended(ActivityFilter, { propsData: { - sortOrder, + discussionFilter, loading, workItemType, }, @@ -30,45 +38,46 @@ describe('Activity Filter', () => { createComponent(); }); - describe('default', () => { - it('has a dropdown with 2 options', () => { + describe('Default', () => { + it('has a dropdown with 3 options', () => { expect(findDropdown().exists()).toBe(true); - expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length); + expect(findAllDropdownItems()).toHaveLength(ActivityFilter.filterOptions.length); }); it('has local storage sync with the correct props', () => { expect(findLocalStorageSync().props('asString')).toBe(true); }); - it('emits `updateSavedSortOrder` event when update is emitted', async () => { - findLocalStorageSync().vm.$emit('input', ASC); + it('emits `changeFilter` event when local storage input is emitted', () => { + findLocalStorageSync().vm.$emit('input', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY); - await nextTick(); - expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1); - expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]); + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]); }); }); - describe('when asc', () => { - describe('when the dropdown is clicked', () => { - it('calls the right actions', async () => { + describe('Changing filter value', () => { + it.each` + dropdownLabel | filterValue | dropdownItem + ${'Comments only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${findOnlyCommentsItem} + ${'History only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_HISTORY} | ${findOnlyHistoryItem} + `( + 'when `$dropdownLabel` is clicked it emits `$filterValue` with tracking info', + ({ dropdownItem, filterValue }) => { const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - findNewestFirstItem().vm.$emit('click'); - await nextTick(); + dropdownItem().vm.$emit('click'); - expect(wrapper.emitted('changeSortOrder')).toHaveLength(1); - expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]); + expect(wrapper.emitted('changeFilter')).toEqual([[filterValue]]); expect(trackingSpy).toHaveBeenCalledWith( TRACKING_CATEGORY_SHOW, - 'notes_sort_order_changed', + 'work_item_notes_filter_changed', { category: TRACKING_CATEGORY_SHOW, - label: 'item_track_notes_sorting', + label: 'item_track_notes_filtering', property: 'type_Task', }, ); - }); - }); + }, + ); }); }); diff --git a/spec/frontend/work_items/components/notes/activity_sort_spec.js b/spec/frontend/work_items/components/notes/activity_sort_spec.js new file mode 100644 index 00000000000..289823dc59e --- /dev/null +++ b/spec/frontend/work_items/components/notes/activity_sort_spec.js @@ -0,0 +1,69 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ActivitySort from '~/work_items/components/notes/activity_sort.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { ASC, DESC } from '~/notes/constants'; + +import { mockTracking } from 'helpers/tracking_helper'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; + +describe('Work Item Activity Sorting', () => { + let wrapper; + + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findNewestFirstItem = () => wrapper.findByTestId('newest-first'); + + const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => { + wrapper = shallowMountExtended(ActivitySort, { + propsData: { + sortOrder, + loading, + workItemType, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + describe('default', () => { + it('has a dropdown with 2 options', () => { + expect(findDropdown().exists()).toBe(true); + expect(findAllDropdownItems()).toHaveLength(ActivitySort.sortOptions.length); + }); + + it('has local storage sync with the correct props', () => { + expect(findLocalStorageSync().props('asString')).toBe(true); + }); + + it('emits `changeSort` event when update is emitted', () => { + findLocalStorageSync().vm.$emit('input', ASC); + + expect(wrapper.emitted('changeSort')).toEqual([[ASC]]); + }); + }); + + describe('when asc', () => { + describe('when the dropdown is clicked', () => { + it('calls the right actions', () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + findNewestFirstItem().vm.$emit('click'); + + expect(wrapper.emitted('changeSort')).toEqual([[DESC]]); + + expect(trackingSpy).toHaveBeenCalledWith( + TRACKING_CATEGORY_SHOW, + 'work_item_notes_sort_order_changed', + { + category: TRACKING_CATEGORY_SHOW, + label: 'item_track_notes_sorting', + property: 'type_Task', + }, + ); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js new file mode 100644 index 00000000000..339efad0608 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js @@ -0,0 +1,44 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, +} from '~/work_items/constants'; + +describe('Work Item History Filter note', () => { + let wrapper; + + const findShowAllActivityButton = () => wrapper.findByTestId('show-all-activity'); + const findShowCommentsButton = () => wrapper.findByTestId('show-comments-only'); + + const createComponent = () => { + wrapper = shallowMountExtended(WorkItemHistoryOnlyFilterNote, { + stubs: { + GlSprintf, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('timelineContent renders a string containing instruction for switching feed type', () => { + expect(wrapper.text()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('emits `changeFilter` event with 0 parameter on clicking Show all activity button', () => { + findShowAllActivityButton().vm.$emit('click'); + + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ALL_NOTES]]); + }); + + it('emits `changeFilter` event with 1 parameter on clicking Show comments only button', () => { + findShowCommentsButton().vm.$emit('click'); + + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS]]); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js new file mode 100644 index 00000000000..3b87a5e3e88 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js @@ -0,0 +1,65 @@ +import { shallowMount } from '@vue/test-utils'; +import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; +import ActivitySort from '~/work_items/components/notes/activity_sort.vue'; +import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; +import { ASC } from '~/notes/constants'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, +} from '~/work_items/constants'; + +describe('Work Item Note Activity Header', () => { + let wrapper; + + const findActivityLabelHeading = () => wrapper.find('h3'); + const findActivityFilterDropdown = () => wrapper.findComponent(ActivityFilter); + const findActivitySortDropdown = () => wrapper.findComponent(ActivitySort); + + const createComponent = ({ + disableActivityFilterSort = false, + sortOrder = ASC, + workItemType = 'Task', + discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES, + } = {}) => { + wrapper = shallowMount(WorkItemNotesActivityHeader, { + propsData: { + disableActivityFilterSort, + sortOrder, + workItemType, + discussionFilter, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('Should have the Activity label', () => { + expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel); + }); + + it('Should have Activity filtering dropdown', () => { + expect(findActivityFilterDropdown().exists()).toBe(true); + }); + + it('Should have Activity sorting dropdown', () => { + expect(findActivitySortDropdown().exists()).toBe(true); + }); + + describe('Activity Filter', () => { + it('emits `changeFilter` when filtering discussions', () => { + findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY); + + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]); + }); + }); + + describe('Activity Sorting', () => { + it('emits `changeSort` when sorting discussions/activity', () => { + findActivitySortDropdown().vm.$emit('changeSort', ASC); + + expect(wrapper.emitted('changeSort')).toEqual([[ASC]]); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index 157e00d3eac..09d0022fa1e 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -9,7 +9,7 @@ import SystemNote from '~/work_items/components/notes/system_note.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; +import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql'; @@ -59,10 +59,9 @@ describe('WorkItemNotes component', () => { const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote); const findAllListItems = () => wrapper.findAll('ul.timeline > *'); - const findActivityLabel = () => wrapper.find('label'); const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - const findSortingFilter = () => wrapper.findComponent(ActivityFilter); + const findActivityHeader = () => wrapper.findComponent(WorkItemNotesActivityHeader); const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index); const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion); const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index); @@ -129,8 +128,8 @@ describe('WorkItemNotes component', () => { createComponent(); }); - it('renders activity label', () => { - expect(findActivityLabel().exists()).toBe(true); + it('has the work item note activity header', () => { + expect(findActivityHeader().exists()).toBe(true); }); it('passes correct props to comment form component', async () => { @@ -221,26 +220,22 @@ describe('WorkItemNotes component', () => { await waitForPromises(); }); - it('filter exists', () => { - expect(findSortingFilter().exists()).toBe(true); - }); - - it('sorts the list when the `changeSortOrder` event is emitted', async () => { + it('sorts the list when the `changeSort` event is emitted', async () => { expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId); - await findSortingFilter().vm.$emit('changeSortOrder', DESC); + await findActivityHeader().vm.$emit('changeSort', DESC); expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId); }); it('puts form at start of list in when sorting by newest first', async () => { - await findSortingFilter().vm.$emit('changeSortOrder', DESC); + await findActivityHeader().vm.$emit('changeSort', DESC); expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true); }); it('puts form at end of list in when sorting by oldest first', async () => { - await findSortingFilter().vm.$emit('changeSortOrder', ASC); + await findActivityHeader().vm.$emit('changeSort', ASC); expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true); }); diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index 811b7a3490c..e5a39f6a24e 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -6,188 +6,135 @@ RSpec.describe IdeHelper, feature_category: :web_ide do describe '#ide_data' do let_it_be(:project) { create(:project) } let_it_be(:user) { project.creator } + let_it_be(:fork_info) { { ide_path: '/test/ide/path' } } + + let_it_be(:params) do + { + branch: 'master', + path: 'foo/bar', + merge_request_id: '1' + } + end + + let(:base_data) do + { + 'can-use-new-web-ide' => 'false', + 'use-new-web-ide' => 'false', + 'user-preferences-path' => profile_preferences_path, + 'project' => nil, + 'preview-markdown-path' => nil + } + end before do allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:content_security_policy_nonce).and_return('test-csp-nonce') end - context 'with vscode_web_ide=true and instance vars set' do + it 'returns hash' do + expect(helper.ide_data(project: nil, fork_info: fork_info, params: params)) + .to include(base_data) + end + + context 'with project' do + it 'returns hash with parameters' do + serialized_project = API::Entities::Project.represent(project, current_user: user).to_json + + expect( + helper.ide_data(project: project, fork_info: nil, params: params) + ).to include(base_data.merge( + 'fork-info' => nil, + 'branch-name' => params[:branch], + 'file-path' => params[:path], + 'merge-request' => params[:merge_request_id], + 'project' => serialized_project, + 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project) + )) + end + + context 'with fork info' do + it 'returns hash with fork info' do + expect(helper.ide_data(project: project, fork_info: fork_info, params: params)) + .to include('fork-info' => fork_info.to_json) + end + end + end + + context 'with environments guidance experiment', :experiment do before do - stub_feature_flags(vscode_web_ide: true) + stub_experiments(in_product_guidance_environments_webide: :candidate) end - it 'returns hash' do - expect( - helper.ide_data( - project: project, - branch: 'master', - path: 'foo/README.md', - merge_request: '7', - fork_info: nil, - learn_gitlab_source: nil - ) - ).to match( + context 'when project has no enviornments' do + it 'enables environment guidance' do + expect(helper.ide_data(project: project, fork_info: fork_info, params: params)) + .to include('enable-environments-guidance' => 'true') + end + + context 'and the callout has been dismissed' do + it 'disables environment guidance' do + callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: user) + callout.update!(dismissed_at: Time.now - 1.week) + allow(helper).to receive(:current_user).and_return(User.find(user.id)) + + expect(helper.ide_data(project: project, fork_info: fork_info, params: params)) + .to include('enable-environments-guidance' => 'false') + end + end + end + + context 'when the project has environments' do + it 'disables environment guidance' do + create(:environment, project: project) + + expect(helper.ide_data(project: project, fork_info: fork_info, params: params)) + .to include('enable-environments-guidance' => 'false') + end + end + end + + context 'with vscode_web_ide=true' do + let(:base_data) do + { 'can-use-new-web-ide' => 'true', 'use-new-web-ide' => 'true', 'user-preferences-path' => profile_preferences_path, 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'), - 'branch-name' => 'master', - 'project-path' => project.path_with_namespace, 'csp-nonce' => 'test-csp-nonce', 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'), - 'file-path' => 'foo/README.md', 'editor-font-family' => 'JetBrains Mono', 'editor-font-format' => 'woff2', - 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono}), - 'merge-request' => '7', - 'fork-info' => nil, - 'learn-gitlab-source' => 'false' - ) + 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono}) + } end - it 'does not use new web ide if user.use_legacy_web_ide' do - allow(user).to receive(:use_legacy_web_ide).and_return(true) - - expect( - helper.ide_data( - project: project, - branch: nil, - path: nil, - merge_request: nil, - fork_info: nil, - learn_gitlab_source: nil - ) - ).to include('use-new-web-ide' => 'false') - end - - it 'returns source data in the hash if learn gitlab source' do - allow(user).to receive(:use_legacy_web_ide).and_return(true) - - expect( - helper.ide_data( - project: project, - branch: nil, - path: nil, - merge_request: nil, - fork_info: nil, - learn_gitlab_source: true - ) - ).to include('learn-gitlab-source' => 'true') - end - end - - context 'with vscode_web_ide=false' do before do - stub_feature_flags(vscode_web_ide: false) + stub_feature_flags(vscode_web_ide: true) end - context 'when instance vars and parameters are not set' do - it 'returns instance data in the hash as nil' do - expect( - helper.ide_data( - project: nil, - branch: nil, - path: nil, - merge_request: nil, - fork_info: nil, - learn_gitlab_source: nil - ) - ).to include( - 'can-use-new-web-ide' => 'false', - 'use-new-web-ide' => 'false', - 'user-preferences-path' => profile_preferences_path, - 'branch-name' => nil, - 'file-path' => nil, - 'merge-request' => nil, - 'fork-info' => nil, - 'project' => nil, - 'preview-markdown-path' => nil - ) - end + it 'returns hash' do + expect(helper.ide_data(project: nil, fork_info: fork_info, params: params)) + .to include(base_data) end - context 'when instance vars are set' do - it 'returns instance data in the hash' do - fork_info = { ide_path: '/test/ide/path' } - - serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json + it 'does not use new web ide if user.use_legacy_web_ide' do + allow(user).to receive(:use_legacy_web_ide).and_return(true) - expect( - helper.ide_data( - project: project, - branch: 'master', - path: 'foo/bar', - merge_request: '1', - fork_info: fork_info, - learn_gitlab_source: nil - ) - ).to include( - 'branch-name' => 'master', - 'file-path' => 'foo/bar', - 'merge-request' => '1', - 'fork-info' => fork_info.to_json, - 'project' => serialized_project, - 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project) - ) - end + expect(helper.ide_data(project: nil, fork_info: fork_info, params: params)) + .to include('use-new-web-ide' => 'false') end - context 'environments guidance experiment', :experiment do - before do - stub_experiments(in_product_guidance_environments_webide: :candidate) - end - - context 'when project has no enviornments' do - it 'enables environment guidance' do - expect( - helper.ide_data( - project: project, - branch: nil, - path: nil, - merge_request: nil, - fork_info: nil, - learn_gitlab_source: nil - ) - ).to include('enable-environments-guidance' => 'true') - end - - context 'and the callout has been dismissed' do - it 'disables environment guidance' do - callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator) - callout.update!(dismissed_at: Time.now - 1.week) - allow(helper).to receive(:current_user).and_return(User.find(project.creator.id)) - - expect( - helper.ide_data( - project: project, - branch: nil, - path: nil, - merge_request: nil, - fork_info: nil, - learn_gitlab_source: nil - ) - ).to include('enable-environments-guidance' => 'false') - end - end - end - - context 'when the project has environments' do - it 'disables environment guidance' do - create(:environment, project: project) - - expect( - helper.ide_data( - project: project, - branch: nil, - path: nil, - merge_request: nil, - fork_info: nil, - learn_gitlab_source: nil - ) - ).to include('enable-environments-guidance' => 'false') - end + context 'with project' do + it 'returns hash with parameters' do + expect( + helper.ide_data(project: project, fork_info: nil, params: params) + ).to include(base_data.merge( + 'branch-name' => params[:branch], + 'file-path' => params[:path], + 'merge-request' => params[:merge_request_id], + 'fork-info' => nil + )) end end end diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb index 174a5a668a8..5ae057dc97d 100644 --- a/spec/helpers/nav/new_dropdown_helper_spec.rb +++ b/spec/helpers/nav/new_dropdown_helper_spec.rb @@ -11,11 +11,10 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do let(:with_can_create_project) { false } let(:with_can_create_group) { false } let(:with_can_create_snippet) { false } - let(:with_context) { true } let(:title) { 'Create new...' } subject(:view_model) do - helper.new_dropdown_view_model(project: current_project, group: current_group, with_context: with_context) + helper.new_dropdown_view_model(project: current_project, group: current_group) end before do @@ -157,26 +156,12 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do it 'has base results' do results = { title: title, - menu_sections: [], - context: group + menu_sections: [] } expect(view_model).to eq(results) end - context 'without context' do - let(:with_context) { false } - - it 'has base results' do - results = { - title: title, - menu_sections: [] - } - - expect(view_model).to eq(results) - end - end - context 'when can create projects in group' do let(:with_can_create_projects_in_group) { true } @@ -247,29 +232,15 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do allow(helper).to receive(:can_admin_project_member?) { with_can_admin_project_member } end - it 'has base results with context' do + it 'has base results' do results = { title: title, - menu_sections: [], - context: project + menu_sections: [] } expect(view_model).to eq(results) end - context 'without context' do - let(:with_context) { false } - - it 'has base results without context' do - results = { - title: title, - menu_sections: [] - } - - expect(view_model).to eq(results) - end - end - context 'with show_new_issue_link?' do let(:with_show_new_issue_link) { true } @@ -375,20 +346,11 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do ) results = { title: title, - menu_sections: project_section, - context: project + menu_sections: project_section } expect(view_model).to eq(results) end - - context 'without context' do - let(:with_context) { false } - - it 'does not include context' do - expect(view_model.keys).to match_array([:title, :menu_sections]) - end - end end def expected_menu_section(title:, menu_item:) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 19a6735e439..8feae58cdc7 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1144,7 +1144,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do context 'HTML comment lines' do subject { described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX } - let(:expected) { %(<!-- an HTML comment -->) } + let(:expected) { [['<!-- an HTML comment -->'], ['<!-- another HTML comment -->']] } let(:markdown) do <<~MARKDOWN Regular text @@ -1152,13 +1152,15 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do <!-- an HTML comment --> more text + + <!-- another HTML comment --> MARKDOWN end it { is_expected.to match(%(<!-- single line comment -->)) } it { is_expected.not_to match(%(<!--\nblock comment\n-->)) } it { is_expected.not_to match(%(must start in first column <!-- comment -->)) } - it { expect(subject.match(markdown)[:html_comment_line]).to eq expected } + it { expect(markdown.scan(subject)).to eq expected } end context 'HTML comment blocks' do diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index e94445f17cd..58b654ed65e 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -243,6 +243,29 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do end end + describe '.non_trace' do + subject { described_class.non_trace } + + context 'when there is only a trace job artifact' do + let!(:trace) { create(:ci_job_artifact, :trace) } + + it { is_expected.to be_empty } + end + + context 'when there is only a non-trace job artifact' do + let!(:junit) { create(:ci_job_artifact, :junit) } + + it { is_expected.to eq([junit]) } + end + + context 'when there are both trace and non-trace job artifacts' do + let!(:trace) { create(:ci_job_artifact, :trace) } + let!(:junit) { create(:ci_job_artifact, :junit) } + + it { is_expected.to eq([junit]) } + end + end + describe '.downloadable' do subject { described_class.downloadable } diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb index 0ad29454ff3..14f346f353b 100644 --- a/spec/models/concerns/taskable_spec.rb +++ b/spec/models/concerns/taskable_spec.rb @@ -46,6 +46,22 @@ RSpec.describe Taskable, feature_category: :team_planning do subject { described_class.get_tasks(description) } it { is_expected.to match(expected_result) } + + describe 'with single line comments' do + let(:description) do + <<~MARKDOWN + <!-- line comment --> + + - [ ] only task item + + <!-- another line comment --> + MARKDOWN + end + + let(:expected_result) { [TaskList::Item.new('- [ ]', 'only task item')] } + + it { is_expected.to match(expected_result) } + end end describe '#task_list_items' do diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index 31a53949f2f..38708399519 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -19,7 +19,6 @@ RSpec.describe IdeController, feature_category: :web_ide do let_it_be(:top_nav_partial) { 'layouts/header/_default' } let(:user) { creator } - let(:branch) { '' } def find_csp_frame_src csp = response.headers['Content-Security-Policy'] @@ -42,14 +41,14 @@ RSpec.describe IdeController, feature_category: :web_ide do subject { get route } shared_examples 'user access rights check' do - context 'user can read project' do + context 'when user can read project' do it 'increases the views counter' do expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count) subject end - context 'user can read project but cannot push code' do + context 'when user can read project but cannot push code' do include ProjectForksHelper let(:user) { reporter } @@ -60,7 +59,15 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to have_gitlab_http_status(:ok) expect(assigns(:project)).to eq project - expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) }) + + expect(assigns(:fork_info)).to eq({ + fork_path: controller.helpers.ide_fork_and_edit_path( + project, + '', + '', + with_notice: false + ) + }) end it 'has nil fork_info if user cannot fork' do @@ -81,13 +88,13 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to have_gitlab_http_status(:ok) expect(assigns(:project)).to eq project - expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') }) + expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, '', '') }) end end end end - context 'user cannot read project' do + context 'when user cannot read project' do let(:user) { other_user } it 'returns 404' do @@ -98,7 +105,7 @@ RSpec.describe IdeController, feature_category: :web_ide do end end - context '/-/ide' do + context 'with /-/ide' do let(:route) { '/-/ide' } it 'returns 404' do @@ -108,7 +115,7 @@ RSpec.describe IdeController, feature_category: :web_ide do end end - context '/-/ide/project' do + context 'with /-/ide/project' do let(:route) { '/-/ide/project' } it 'returns 404' do @@ -118,7 +125,7 @@ RSpec.describe IdeController, feature_category: :web_ide do end end - context '/-/ide/project/:project' do + context 'with /-/ide/project/:project' do let(:route) { "/-/ide/project/#{project.full_path}" } it 'instantiates project instance var and returns 200' do @@ -126,33 +133,13 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to have_gitlab_http_status(:ok) expect(assigns(:project)).to eq project - expect(assigns(:branch)).to be_nil - expect(assigns(:path)).to be_nil - expect(assigns(:merge_request)).to be_nil expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to be_nil end it_behaves_like 'user access rights check' - context "/-/ide/project/:project?learn_gitlab_source=true" do - let(:route) { "/-/ide/project/#{project.full_path}?learn_gitlab_source=true" } - - it 'instantiates project instance var and returns 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:project)).to eq project - expect(assigns(:branch)).to be_nil - expect(assigns(:path)).to be_nil - expect(assigns(:merge_request)).to be_nil - expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to eq 'true' - end - end - - %w(edit blob tree).each do |action| - context "/-/ide/project/:project/#{action}" do + %w[edit blob tree].each do |action| + context "with /-/ide/project/:project/#{action}" do let(:route) { "/-/ide/project/#{project.full_path}/#{action}" } it 'instantiates project instance var and returns 200' do @@ -160,94 +147,13 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to have_gitlab_http_status(:ok) expect(assigns(:project)).to eq project - expect(assigns(:branch)).to be_nil - expect(assigns(:path)).to be_nil - expect(assigns(:merge_request)).to be_nil expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to be_nil end it_behaves_like 'user access rights check' - - context "/-/ide/project/:project/#{action}/:branch" do - let(:branch) { 'master' } - let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}" } - - it 'instantiates project and branch instance vars and returns 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:project)).to eq project - expect(assigns(:branch)).to eq branch - expect(assigns(:path)).to be_nil - expect(assigns(:merge_request)).to be_nil - expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to be_nil - end - - it_behaves_like 'user access rights check' - - context "/-/ide/project/:project/#{action}/:branch/-" do - let(:branch) { 'branch/slash' } - let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-" } - - it 'instantiates project and branch instance vars and returns 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:project)).to eq project - expect(assigns(:branch)).to eq branch - expect(assigns(:path)).to be_nil - expect(assigns(:merge_request)).to be_nil - expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to be_nil - end - - it_behaves_like 'user access rights check' - - context "/-/ide/project/:project/#{action}/:branch/-/:path" do - let(:branch) { 'master' } - let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-/foo/.bar" } - - it 'instantiates project, branch, and path instance vars and returns 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:project)).to eq project - expect(assigns(:branch)).to eq branch - expect(assigns(:path)).to eq 'foo/.bar' - expect(assigns(:merge_request)).to be_nil - expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to be_nil - end - - it_behaves_like 'user access rights check' - end - end - end end end - context '/-/ide/project/:project/merge_requests/:merge_request_id' do - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - - let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" } - - it 'instantiates project and merge_request instance vars and returns 200' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:project)).to eq project - expect(assigns(:branch)).to be_nil - expect(assigns(:path)).to be_nil - expect(assigns(:merge_request)).to eq merge_request.id.to_s - expect(assigns(:fork_info)).to be_nil - expect(assigns(:learn_gitlab_source)).to be_nil - end - - it_behaves_like 'user access rights check' - end - describe 'Snowplow view event', :snowplow do it 'is tracked' do subject diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb index 07e76f65232..d1ec2a1d3a6 100644 --- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb @@ -231,6 +231,16 @@ feature_category: :build_artifacts do end end + context 'when some artifacts are trace' do + let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) } + let!(:trace_artifact) { create(:ci_job_artifact, :trace, :expired, job: job, locked: job.pipeline.locked) } + + it 'destroys only non trace artifacts' do + expect { subject }.to change { Ci::JobArtifact.count }.by(-1) + expect(trace_artifact).to be_persisted + end + end + context 'when all artifacts are locked' do let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) } diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb index 0e27f79487c..f4839ccb04b 100644 --- a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb @@ -7,19 +7,32 @@ RSpec.describe Ci::JobArtifacts::DestroyAssociationsService, feature_category: : let_it_be(:project_2) { create(:project) } let_it_be(:artifact_1, refind: true) { create(:ci_job_artifact, :zip, project: project_1) } - let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :zip, project: project_2) } - let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :zip, project: project_1) } + let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :junit, project: project_2) } + let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :terraform, project: project_1) } + let_it_be(:artifact_4, refind: true) { create(:ci_job_artifact, :trace, project: project_2) } + let_it_be(:artifact_5, refind: true) { create(:ci_job_artifact, :metadata, project: project_2) } - let(:artifacts) { Ci::JobArtifact.where(id: [artifact_1.id, artifact_2.id, artifact_3.id]) } + let_it_be(:locked_artifact, refind: true) { create(:ci_job_artifact, :zip, :locked, project: project_1) } + + let(:artifact_ids_to_be_removed) { [artifact_1.id, artifact_2.id, artifact_3.id, artifact_4.id, artifact_5.id] } + let(:artifacts) { Ci::JobArtifact.where(id: artifact_ids_to_be_removed) } let(:service) { described_class.new(artifacts) } describe '#destroy_records' do - it 'removes artifacts without updating statistics' do + it 'removes all types of artifacts without updating statistics' do expect_next_instance_of(Ci::JobArtifacts::DestroyBatchService) do |service| expect(service).to receive(:execute).with(update_stats: false).and_call_original end - expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-3) + expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count) + end + + context 'with a locked artifact' do + let(:artifact_ids_to_be_removed) { [artifact_1.id, locked_artifact.id] } + + it 'removes all artifacts' do + expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count) + end end context 'when there are no artifacts' do @@ -42,7 +55,11 @@ RSpec.describe Ci::JobArtifacts::DestroyAssociationsService, feature_category: : have_attributes(amount: -artifact_1.size, ref: artifact_1.id), have_attributes(amount: -artifact_3.size, ref: artifact_3.id) ] - project2_increments = [have_attributes(amount: -artifact_2.size, ref: artifact_2.id)] + project2_increments = [ + have_attributes(amount: -artifact_2.size, ref: artifact_2.id), + have_attributes(amount: -artifact_4.size, ref: artifact_4.id), + have_attributes(amount: -artifact_5.size, ref: artifact_5.id) + ] expect(ProjectStatistics).to receive(:bulk_increment_statistic).once .with(project_1, :build_artifacts_size, match_array(project1_increments)) diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index ed3e3e6766f..6f9dcf47535 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_artifacts do - let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id, trace_artifact.id]) } + let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id]) } let(:skip_projects_on_refresh) { false } let(:service) do described_class.new( @@ -25,34 +25,9 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_a create(:ci_job_artifact) end - let_it_be(:trace_artifact, refind: true) do - create(:ci_job_artifact, :trace, :expired) - end - describe '#execute' do subject(:execute) { service.execute } - context 'with skip_trace_artifacts false' do - let(:service) do - described_class.new( - artifacts, - pick_up_at: Time.current, - skip_projects_on_refresh: skip_projects_on_refresh, - skip_trace_artifacts: false - ) - end - - subject(:execute) { service.execute } - - it 'deletes trace artifacts' do - expect { subject } - .to change { Ci::JobArtifact.exists?(trace_artifact.id) }.from(true).to(false) - - expected_destroyed_ids = [artifact_with_file.id, artifact_without_file.id, trace_artifact.id] - is_expected.to include(destroyed_artifacts_count: 3, destroyed_ids: expected_destroyed_ids) - end - end - it 'creates a deleted object for artifact with attached file' do expect { subject }.to change { Ci::DeletedObject.count }.by(1) end @@ -81,11 +56,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_a execute end - it 'preserves trace artifacts' do - expect { subject } - .to not_change { Ci::JobArtifact.exists?(trace_artifact.id) } - end - context 'when artifact belongs to a project that is undergoing stats refresh' do let!(:artifact_under_refresh_1) do create(:ci_job_artifact, :zip) diff --git a/spec/services/releases/links/create_service_spec.rb b/spec/services/releases/links/create_service_spec.rb index aa154647509..9928d2162d7 100644 --- a/spec/services/releases/links/create_service_spec.rb +++ b/spec/services/releases/links/create_service_spec.rb @@ -44,6 +44,7 @@ RSpec.describe Releases::Links::CreateService, feature_category: :release_orches is_expected.to be_error expect(execute.message).to include('Access Denied') + expect(execute.reason).to eq(:forbidden) end end @@ -55,6 +56,7 @@ RSpec.describe Releases::Links::CreateService, feature_category: :release_orches is_expected.to be_error expect(execute.message[0]).to include('Url is blocked') + expect(execute.reason).to eq(:bad_request) end end diff --git a/spec/services/releases/links/destroy_service_spec.rb b/spec/services/releases/links/destroy_service_spec.rb index fed98a62aa7..a248932eada 100644 --- a/spec/services/releases/links/destroy_service_spec.rb +++ b/spec/services/releases/links/destroy_service_spec.rb @@ -40,6 +40,7 @@ RSpec.describe Releases::Links::DestroyService, feature_category: :release_orche is_expected.to be_error expect(execute.message).to include('Access Denied') + expect(execute.reason).to eq(:forbidden) end end @@ -51,6 +52,7 @@ RSpec.describe Releases::Links::DestroyService, feature_category: :release_orche is_expected.to be_error expect(execute.message).to eq('Link does not exist') + expect(execute.reason).to eq(:not_found) end end @@ -63,6 +65,7 @@ RSpec.describe Releases::Links::DestroyService, feature_category: :release_orche expect { execute }.not_to change { release.links.count } is_expected.to be_error + expect(execute.reason).to eq(:bad_request) end end end diff --git a/spec/services/releases/links/update_service_spec.rb b/spec/services/releases/links/update_service_spec.rb index 40756c7eced..3f48985cf60 100644 --- a/spec/services/releases/links/update_service_spec.rb +++ b/spec/services/releases/links/update_service_spec.rb @@ -51,6 +51,7 @@ RSpec.describe Releases::Links::UpdateService, feature_category: :release_orches it 'returns an error' do is_expected.to be_error expect(execute.message).to include('Access Denied') + expect(execute.reason).to eq(:forbidden) end end @@ -60,6 +61,7 @@ RSpec.describe Releases::Links::UpdateService, feature_category: :release_orches it 'returns an error' do is_expected.to be_error expect(execute.message[0]).to include('Url is blocked') + expect(execute.reason).to eq(:bad_request) end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 2e3014b2f51..039459e6cd2 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -5138,7 +5138,6 @@ - './spec/helpers/groups/settings_helper_spec.rb' - './spec/helpers/hooks_helper_spec.rb' - './spec/helpers/icons_helper_spec.rb' -- './spec/helpers/ide_helper_spec.rb' - './spec/helpers/import_helper_spec.rb' - './spec/helpers/instance_configuration_helper_spec.rb' - './spec/helpers/integrations_helper_spec.rb' @@ -8951,7 +8950,6 @@ - './spec/requests/groups/settings/access_tokens_controller_spec.rb' - './spec/requests/groups/settings/applications_controller_spec.rb' - './spec/requests/health_controller_spec.rb' -- './spec/requests/ide_controller_spec.rb' - './spec/requests/import/gitlab_groups_controller_spec.rb' - './spec/requests/import/gitlab_projects_controller_spec.rb' - './spec/requests/import/url_controller_spec.rb' diff --git a/spec/views/layouts/group.html.haml_spec.rb b/spec/views/layouts/group.html.haml_spec.rb new file mode 100644 index 00000000000..0b8f735a1d6 --- /dev/null +++ b/spec/views/layouts/group.html.haml_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'layouts/group', feature_category: :subgroups do + let_it_be(:group) { create(:group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate + let(:invite_member) { true } + + before do + allow(view).to receive(:can_admin_group_member?).and_return(invite_member) + assign(:group, group) + allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(build_stubbed(:user))) + end + + subject do + render + + rendered + end + + context 'with ability to invite members' do + it { is_expected.to have_selector('.js-invite-members-modal') } + end + + context 'without ability to invite members' do + let(:invite_member) { false } + + it { is_expected.not_to have_selector('.js-invite-members-modal') } + end +end diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb index a547c1be2f4..2c5882fce3d 100644 --- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -8,14 +8,12 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do shared_examples_for 'invite member selector' do context 'with ability to invite members' do it { is_expected.to have_selector('.js-invite-members-trigger') } - it { is_expected.to have_selector('.js-invite-members-modal') } end context 'without ability to invite members' do let(:invite_member) { false } it { is_expected.not_to have_selector('.js-invite-members-trigger') } - it { is_expected.not_to have_selector('.js-invite-members-modal') } end end diff --git a/spec/views/layouts/project.html.haml_spec.rb b/spec/views/layouts/project.html.haml_spec.rb new file mode 100644 index 00000000000..588828f7bd6 --- /dev/null +++ b/spec/views/layouts/project.html.haml_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'layouts/project', feature_category: :projects do + let(:invite_member) { true } + + before do + allow(view).to receive(:can_admin_project_member?).and_return(invite_member) + assign(:project, build_stubbed(:project)) + allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(build_stubbed(:user))) + end + + subject do + render + + rendered + end + + context 'with ability to invite members' do + it { is_expected.to have_selector('.js-invite-members-modal') } + end + + context 'without ability to invite members' do + let(:invite_member) { false } + + it { is_expected.not_to have_selector('.js-invite-members-modal') } + end +end diff --git a/yarn.lock b/yarn.lock index 212ddc49fe6..de413a5f5c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1221,10 +1221,10 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.2.0" -"@gitlab/svgs@3.22.0": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.22.0.tgz#29e6789efd03b21c8e028063ff40b1272924bae8" - integrity sha512-To1MOwAvstlX1sZ9rB5SWxhkd0+rba1pzrHPgDdc6Ye15EPHHHUbJTZ4WPNAjWrxcqCkGNw+5NFyWx5y1GHuOQ== +"@gitlab/svgs@3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.23.0.tgz#92ed37ebd2058f1c1ed4651f86d4a20736790afb" + integrity sha512-rq6md86C+2AH75wk3zY0e+aPRRK1QuBdhNPex/Q7IfR8gm+kADhYj1GSS6bnU80rfG6Fk49xi6VpSHWRlQZ0Zg== "@gitlab/ui@56.2.0": version "56.2.0" |