diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-06 18:09:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-06 18:09:58 +0300 |
commit | 6d3676d61064af469f2fa1171bec4575235c6739 (patch) | |
tree | 80264790b844a7bdc60ad78d6a91580387194711 | |
parent | 30acb0522a609c438d60f5345243e96f9a041ae6 (diff) |
Add latest changes from gitlab-org/gitlab@master
101 files changed, 1160 insertions, 676 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5024436dec7..2ebe5c9e74d 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -c9160a6d435d904bb72f3627c702192706ad4642 +fa2d113f7d1122a4e9cff8158dcbb4cdff06e2f6 diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue index c49ab1ac43c..7ec3ec3f84d 100644 --- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue +++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue @@ -225,7 +225,7 @@ export default { </div> </div> <h5>{{ $options.translations.addTokenHeader }}</h5> - <p class="profile-settings-content"> + <p> <gl-sprintf :message="$options.translations.addTokenDescription" :placeholders="placeholders.link" diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 9cc8fe94fc5..dfb2e192711 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -25,7 +25,7 @@ import { FILE_DIFF_POSITION_TYPE, } from '../constants'; import eventHub from '../event_hub'; -import { DIFF_FILE, SOMETHING_WENT_WRONG, CONFLICT_TEXT } from '../i18n'; +import { DIFF_FILE, SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED, CONFLICT_TEXT } from '../i18n'; import { collapsedType, getShortShaFromFile } from '../utils/diff_file'; import DiffDiscussions from './diff_discussions.vue'; import DiffFileHeader from './diff_file_header.vue'; @@ -344,7 +344,7 @@ export default { hideForkMessage() { this.idState.forkMessageVisible = false; }, - handleSaveNote(note) { + handleSaveNote(note, parentElement, errorCallback) { this.saveDiffDiscussion({ note, formData: { @@ -353,8 +353,23 @@ export default { diffFile: this.file, positionType: FILE_DIFF_POSITION_TYPE, }, + }).catch((e) => { + const reason = e.response?.data?.errors; + const errorMessage = reason + ? sprintf(SAVING_THE_COMMENT_FAILED, { reason }) + : SOMETHING_WENT_WRONG; + + createAlert({ + message: errorMessage, + parent: parentElement, + }); + + errorCallback(); }); }, + handleSaveDraftNote(note, _, parentElement, errorCallback) { + this.addToReview(note, this.$options.FILE_DIFF_POSITION_TYPE, parentElement, errorCallback); + }, }, CONFLICT_TEXT, FILE_DIFF_POSITION_TYPE, @@ -484,9 +499,7 @@ export default { class="gl-py-3 gl-px-5" data-testid="file-note-form" @handleFormUpdate="handleSaveNote" - @handleFormUpdateAddToReview=" - (note) => addToReview(note, $options.FILE_DIFF_POSITION_TYPE) - " + @handleFormUpdateAddToReview="handleSaveDraftNote" @cancelForm="toggleFileCommentForm(file.file_path)" /> </div> diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue index 164ec46cdb9..4a68a8240ae 100644 --- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue +++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue @@ -114,7 +114,7 @@ export default { <div v-if="integrationViews.length" class="col-sm-12"> <hr data-testid="profile-preferences-integrations-rule" /> </div> - <div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar"> + <div v-if="integrationViews.length" class="col-lg-4"> <h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading"> {{ $options.i18n.integrations }} </h4> diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index 2966214e051..cf251bc7465 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -1,27 +1,18 @@ <script> -import { - GlDropdown, - GlDropdownSectionHeader, - GlDropdownItem, - GlSearchBoxByType, - GlDropdownDivider, - GlTooltipDirective, -} from '@gitlab/ui'; +import { GlAvatar, GlCollapsibleListbox, GlTooltipDirective } from '@gitlab/ui'; import { debounce } from 'lodash'; -import { mapState, mapActions } from 'vuex'; -import { redirectTo, queryToObject } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated -import { __ } from '~/locale'; +import { mapActions, mapState } from 'vuex'; +import { queryToObject, visitUrl } from '~/lib/utils/url_utility'; +import { n__, __ } from '~/locale'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; const tooltipMessage = __('Searching by both author and message is currently not supported.'); export default { name: 'AuthorSelect', components: { - GlDropdown, - GlDropdownSectionHeader, - GlDropdownItem, - GlSearchBoxByType, - GlDropdownDivider, + GlAvatar, + GlCollapsibleListbox, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,9 +26,9 @@ export default { data() { return { hasSearchParam: false, - searchTerm: '', - authorInput: '', currentAuthor: '', + searchTerm: '', + searching: false, }; }, computed: { @@ -45,9 +36,33 @@ export default { dropdownText() { return this.currentAuthor || __('Author'); }, + dropdownItems() { + const commitAuthorOptions = this.commitsAuthors.map((author) => ({ + value: author.name, + text: author.name, + secondaryText: author.username, + avatarUrl: author.avatar_url, + })); + if (this.searchTerm) return commitAuthorOptions; + + const defaultOptions = { + text: '', + options: [{ text: __('Any Author'), value: '' }], + textSrOnly: true, + }; + const authorOptionsGroup = { + text: 'authors', + options: commitAuthorOptions, + textSrOnly: true, + }; + return [defaultOptions, authorOptionsGroup]; + }, tooltipTitle() { return this.hasSearchParam && tooltipMessage; }, + searchSummarySrText() { + return n__('%d author', '%d authors', this.commitsAuthors.length); + }, }, mounted() { this.fetchAuthors(); @@ -73,9 +88,7 @@ export default { }, methods: { ...mapActions(['fetchAuthors']), - selectAuthor(author) { - const { name: user } = author || {}; - + selectAuthor(user) { // Follow up issue "Remove usage of $.fadeIn from the codebase" // > https://gitlab.com/gitlab-org/gitlab/-/issues/214395 @@ -89,13 +102,19 @@ export default { commitListElement.style.transition = 'opacity 200ms'; if (!user) { - return redirectTo(this.commitsPath); // eslint-disable-line import/no-deprecated + return visitUrl(this.commitsPath); } - return redirectTo(`${this.commitsPath}?author=${user}`); // eslint-disable-line import/no-deprecated + return visitUrl(`${this.commitsPath}?author=${user}`); }, - searchAuthors() { - this.fetchAuthors(this.authorInput); + searchAuthors: debounce(async function debouncedSearch() { + this.searching = true; + await this.fetchAuthors(this.searchTerm); + this.searching = false; + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), + handleSearch(input) { + this.searchTerm = input; + this.searchAuthors(); }, setSearchParam(value) { this.hasSearchParam = Boolean(value); @@ -105,36 +124,45 @@ export default { </script> <template> - <div ref="dropdownContainer" v-gl-tooltip :title="tooltipTitle" :disabled="!hasSearchParam"> - <gl-dropdown - :text="dropdownText" + <div ref="listboxContainer" v-gl-tooltip :title="tooltipTitle" :disabled="!hasSearchParam"> + <gl-collapsible-listbox + v-model="currentAuthor" + block + is-check-centered + searchable + class="gl-mt-3 gl-sm-mt-0" + :items="dropdownItems" + :header-text="__('Search by author')" + :toggle-text="dropdownText" + :search-placeholder="__('Search')" + :searching="searching" :disabled="hasSearchParam" - toggle-class="gl-py-3 gl-border-0" - class="w-100 gl-mt-3 mt-sm-0" + @search="handleSearch" + @select="selectAuthor" > - <gl-dropdown-section-header> - {{ __('Search by author') }} - </gl-dropdown-section-header> - <gl-dropdown-divider /> - <gl-search-box-by-type - v-model.trim="authorInput" - :placeholder="__('Search')" - @input="searchAuthors" - /> - <gl-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)"> - {{ __('Any Author') }} - </gl-dropdown-item> - <gl-dropdown-divider /> - <gl-dropdown-item - v-for="author in commitsAuthors" - :key="author.id" - :is-checked="author.name === currentAuthor" - :avatar-url="author.avatar_url" - :secondary-text="author.username" - @click="selectAuthor(author)" - > - {{ author.name }} - </gl-dropdown-item> - </gl-dropdown> + <template #search-summary-sr-only> + {{ searchSummarySrText }} + </template> + <template #list-item="{ item }"> + <span class="gl-display-flex gl-align-items-center"> + <gl-avatar + v-if="item.avatarUrl" + class="gl-mr-3" + :size="32" + :entity-name="item.text" + :src="item.avatarUrl" + :alt="item.text" + /> + <span + class="gl-display-flex gl-flex-direction-column gl-overflow-hidden gl-overflow-break-word" + > + {{ item.text }} + <span v-if="item.secondaryText" class="gl-text-secondary"> + {{ item.secondaryText }} + </span> + </span> + </span> + </template> + </gl-collapsible-listbox> </div> </template> diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 46dee9db69a..d498be0b2bb 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -1,14 +1,8 @@ <script> -import { - GlDropdown, - GlDropdownDivider, - GlDropdownSectionHeader, - GlDropdownItem, - GlIcon, - GlModalDirective, -} from '@gitlab/ui'; +import { GlDisclosureDropdown, GlModalDirective } from '@gitlab/ui'; import permissionsQuery from 'shared_queries/repository/permissions.query.graphql'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; +import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { __ } from '~/locale'; import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; @@ -16,21 +10,12 @@ import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import UploadBlobModal from './upload_blob_modal.vue'; import NewDirectoryModal from './new_directory_modal.vue'; -const ROW_TYPES = { - header: 'header', - divider: 'divider', -}; - const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob'; const NEW_DIRECTORY_MODAL_ID = 'modal-new-directory'; export default { components: { - GlDropdown, - GlDropdownDivider, - GlDropdownSectionHeader, - GlDropdownItem, - GlIcon, + GlDisclosureDropdown, UploadBlobModal, NewDirectoryModal, }, @@ -171,103 +156,99 @@ export default { canCreateMrFromFork() { return this.userPermissions?.forkProject && this.userPermissions?.createMergeRequestIn; }, + hasPushCodePermission() { + return this.userPermissions?.pushCode; + }, showUploadModal() { return this.canEditTree && !this.$apollo.queries.userPermissions.loading; }, showNewDirectoryModal() { return this.canEditTree && !this.$apollo.queries.userPermissions.loading; }, - dropdownItems() { - const items = []; - + dropdownDirectoryItems() { if (this.canEditTree) { - items.push( + return [ { - type: ROW_TYPES.header, - text: __('This directory'), - }, - { - attrs: { - href: `${this.newBlobPath}/${ - this.currentPath ? encodeURIComponent(this.currentPath) : '' - }`, + text: __('New file'), + href: joinPaths( + this.newBlobPath, + this.currentPath ? encodeURIComponent(this.currentPath) : '', + ), + extraAttrs: { 'data-qa-selector': 'new_file_menu_item', }, - text: __('New file'), }, { - attrs: { - href: '#modal-upload-blob', - }, text: __('Upload file'), - modalId: UPLOAD_BLOB_MODAL_ID, - }, - ); - - items.push({ - attrs: { - href: '#modal-create-new-dir', - }, - text: __('New directory'), - modalId: NEW_DIRECTORY_MODAL_ID, - }); - } else if (this.canCreateMrFromFork) { - items.push( - { - attrs: { - href: this.forkNewBlobPath, - 'data-method': 'post', - }, - text: __('New file'), + action: () => this.$root.$emit(BV_SHOW_MODAL, UPLOAD_BLOB_MODAL_ID), }, { - attrs: { - href: this.forkUploadBlobPath, - 'data-method': 'post', - }, - text: __('Upload file'), - }, - { - attrs: { - href: this.forkNewDirectoryPath, - 'data-method': 'post', - }, text: __('New directory'), + action: () => this.$root.$emit(BV_SHOW_MODAL, NEW_DIRECTORY_MODAL_ID), }, - ); + ]; } - if (this.userPermissions?.pushCode) { - items.push( + if (this.canCreateMrFromFork) { + return [ { - type: ROW_TYPES.divider, - }, - { - type: ROW_TYPES.header, - text: __('This repository'), + text: __('New file'), + href: this.forkNewBlobPath, + extraAttrs: { + 'data-method': 'post', + }, }, { - attrs: { - href: this.newBranchPath, + text: __('Upload file'), + href: this.forkUploadBlobPath, + extraAttrs: { + 'data-method': 'post', }, - text: __('New branch'), }, { - attrs: { - href: this.newTagPath, + text: __('New directory'), + href: this.forkNewDirectoryPath, + extraAttrs: { + 'data-method': 'post', }, - text: __('New tag'), }, - ); + ]; } - return items; + return []; + }, + dropdownRepositoryItems() { + if (!this.hasPushCodePermission) return []; + return [ + { + text: __('New branch'), + href: this.newBranchPath, + }, + { + text: __('New tag'), + href: this.newTagPath, + }, + ]; + }, + dropdownItems() { + if (this.isBlobPath) return []; + if (!this.canCollaborate && !this.canCreateMrFromFork) return []; + return [ + this.dropdownDirectoryItems?.length && { + name: __('This directory'), + items: this.dropdownDirectoryItems, + }, + this.dropdownRepositoryItems?.length && { + name: __('This repository'), + items: this.dropdownRepositoryItems, + }, + ].filter(Boolean); }, isBlobPath() { return this.$route.name === 'blobPath' || this.$route.name === 'blobPathDecoded'; }, renderAddToTreeDropdown() { - return !this.isBlobPath && (this.canCollaborate || this.canCreateMrFromFork); + return this.dropdownItems.length; }, newDirectoryPath() { return joinPaths(this.newDirPath, this.currentPath); @@ -277,16 +258,6 @@ export default { isLast(i) { return i === this.pathLinks.length - 1; }, - getComponent(type) { - switch (type) { - case ROW_TYPES.divider: - return 'gl-dropdown-divider'; - case ROW_TYPES.header: - return 'gl-dropdown-section-header'; - default: - return 'gl-dropdown-item'; - } - }, }, }; </script> @@ -300,27 +271,15 @@ export default { </router-link> </li> <li v-if="renderAddToTreeDropdown" class="breadcrumb-item"> - <gl-dropdown + <gl-disclosure-dropdown + :toggle-text="__('Add to tree')" toggle-class="add-to-tree gl-ml-2" data-testid="add-to-tree" data-qa-selector="add_to_tree_dropdown" - > - <template #button-content> - <span class="sr-only">{{ __('Add to tree') }}</span> - <gl-icon name="plus" :size="16" class="float-left" /> - <gl-icon name="chevron-down" :size="16" class="float-left" /> - </template> - <template v-for="(item, i) in dropdownItems"> - <component - :is="getComponent(item.type)" - :key="i" - v-bind="item.attrs" - v-gl-modal="item.modalId || null" - > - {{ item.text }} - </component> - </template> - </gl-dropdown> + text-sr-only + icon="plus" + :items="dropdownItems" + /> </li> </ol> <upload-blob-modal diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 0dc66750a85..609a9355d20 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,14 +1,12 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; import { n__, __ } from '~/locale'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'AssigneeTitle', components: { GlLoadingIcon, }, - mixins: [glFeatureFlagMixin()], props: { loading: { type: Boolean, @@ -23,11 +21,6 @@ export default { type: Boolean, required: true, }, - showToggle: { - type: Boolean, - required: false, - default: false, - }, changing: { type: Boolean, required: false, diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 062f63175a7..0563ed8394c 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -27,11 +27,6 @@ export default { type: String, required: true, }, - signedIn: { - type: Boolean, - required: false, - default: false, - }, issuableType: { type: String, required: false, @@ -143,7 +138,6 @@ export default { :number-of-assignees="store.assignees.length" :loading="loading || store.isFetching.assignees" :editable="store.editable" - :show-toggle="!signedIn" :changing="store.changing" /> <assignees diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 67e76b575e0..8f6b855ecd6 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -118,7 +118,6 @@ function mountSidebarAssigneesDeprecated(mediator) { issuableIid: String(iid), projectPath: fullPath, field: el.dataset.field, - signedIn: Object.prototype.hasOwnProperty.call(el.dataset, 'signedIn'), issuableType: isInIssuePage() || isInIncidentPage() || isInDesignPage() ? TYPE_ISSUE diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index 1d4c24c6853..8ce82116194 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -38,7 +38,7 @@ export default { shortcuts: __('Keyboard shortcuts'), version: __('Your GitLab version'), whatsnew: __("What's new"), - chat: s__('TanukiBot|Ask GitLab Chat'), + chat: s__('TanukiBot|Ask GitLab Duo'), }, props: { sidebarData: { diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js index a69f192f520..56453373bbf 100644 --- a/app/assets/javascripts/tracking/internal_events.js +++ b/app/assets/javascripts/tracking/internal_events.js @@ -5,28 +5,30 @@ import { GITLAB_INTERNAL_EVENT_CATEGORY, SERVICE_PING_SCHEMA } from './constants const InternalEvents = { /** + * + * @param {string} event + */ + track_event(event) { + API.trackRedisHllUserEvent(event); + Tracking.event(GITLAB_INTERNAL_EVENT_CATEGORY, event, { + context: { + schema: SERVICE_PING_SCHEMA, + data: { + event_name: event, + data_source: 'redis_hll', + }, + }, + }); + }, + /** * Returns an implementation of this class in the form of * a Vue mixin. - * - * @param {Object} opts - default options for all events - * @returns {Object} */ - mixin(opts = {}) { + mixin() { return { - mixins: [Tracking.mixin(opts)], methods: { track_event(event) { - API.trackRedisHllUserEvent(event); - this.track(event, { - context: { - schema: SERVICE_PING_SCHEMA, - data: { - event_name: event, - data_source: 'redis_hll', - }, - }, - category: GITLAB_INTERNAL_EVENT_CATEGORY, - }); + InternalEvents.track_event(event); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index e09f193310b..e930f3cac33 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -181,7 +181,7 @@ export default { <template> <!-- Delayed so not every mouseover triggers Popover --> <gl-popover - :css-classes="['gl-max-w-48']" + :css-classes="['user-popover', 'gl-max-w-48', 'gl-overflow-hidden']" :show="show" :target="target" :delay="$options.USER_POPOVER_DELAY" @@ -204,6 +204,7 @@ export default { :src="user.avatarUrl" :label="user.name" :sub-label="username" + class="gl-w-full" > <template v-if="isBlocked"> <span class="gl-mt-4 gl-font-style-italic">{{ $options.I18N_USER_BLOCKED }}</span> diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss index 6a6febbf7b4..946079e7e90 100644 --- a/app/assets/stylesheets/components/avatar.scss +++ b/app/assets/stylesheets/components/avatar.scss @@ -189,3 +189,18 @@ $avatar-sizes: ( .avatar-counter { @include avatar-counter(); } + +.user-popover { + // GlAvatarLabeled doesn't expose any prop to override internal classes + + // Max width of popover container is set by gl-max-w-48 + // so we need to ensure that name/username/status container doesn't overflow + .gl-avatar-labeled-labels { + max-width: px-to-rem(290px); + } + + .gl-avatar-labeled-label, + .gl-avatar-labeled-sublabel { + @include gl-text-truncate(); + } +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index a3b98e94b90..904c1a51471 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -34,11 +34,7 @@ --mr-review-bar-height: #{$mr-review-bar-height}; } -@include media-breakpoint-up(md) { - .page-with-contextual-sidebar { - --application-bar-left: #{$contextual-sidebar-collapsed-width}; - } - +@include media-breakpoint-up(sm) { .right-sidebar-collapsed { --application-bar-right: #{$right-sidebar-collapsed-width}; @@ -52,6 +48,12 @@ } } +@include media-breakpoint-up(md) { + .page-with-contextual-sidebar { + --application-bar-left: #{$contextual-sidebar-collapsed-width}; + } +} + @include media-breakpoint-up(xl) { .page-with-contextual-sidebar { --application-bar-left: #{$contextual-sidebar-width}; diff --git a/app/controllers/organizations/application_controller.rb b/app/controllers/organizations/application_controller.rb index 5f5a57d176b..43cc7014f62 100644 --- a/app/controllers/organizations/application_controller.rb +++ b/app/controllers/organizations/application_controller.rb @@ -4,6 +4,8 @@ module Organizations class ApplicationController < ::ApplicationController before_action :organization + layout 'organization' + private def organization diff --git a/app/controllers/organizations/organizations_controller.rb b/app/controllers/organizations/organizations_controller.rb index 0eb5c3aa6fd..4781ef995b7 100644 --- a/app/controllers/organizations/organizations_controller.rb +++ b/app/controllers/organizations/organizations_controller.rb @@ -6,6 +6,8 @@ module Organizations before_action { authorize_action!(:admin_organization) } - def directory; end + def show; end + + def groups_and_projects; end end end diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 3001a7ba441..90917cb96e0 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -23,6 +23,10 @@ module SidebarsHelper end end + def organization_sidebar_context(organization, user, **args) + Sidebars::Context.new(container: organization, current_user: user, **args) + end + def project_sidebar_context(project, user, current_ref, ref_type: nil, **args) context_data = project_sidebar_context_data(project, user, current_ref, ref_type: ref_type) Sidebars::Projects::Context.new(**context_data, **args) @@ -95,7 +99,7 @@ module SidebarsHelper def super_sidebar_nav_panel( nav: nil, project: nil, user: nil, group: nil, current_ref: nil, ref_type: nil, - viewed_user: nil) + viewed_user: nil, organization: nil) context_adds = { route_is_active: method(:active_nav_link?), is_super_sidebar: true } case nav when 'project' @@ -117,6 +121,9 @@ module SidebarsHelper Sidebars::Search::Panel.new(context) when 'admin' Sidebars::Admin::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds)) + when 'organization' + context = organization_sidebar_context(organization, user, **context_adds) + Sidebars::Organizations::SuperSidebarPanel.new(context) else context = your_work_sidebar_context(user, **context_adds) Sidebars::YourWork::Panel.new(context) diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb index c8b2513e702..5480b9e9c4a 100644 --- a/app/models/webauthn_registration.rb +++ b/app/models/webauthn_registration.rb @@ -3,10 +3,6 @@ # Registration information for WebAuthn credentials class WebauthnRegistration < ApplicationRecord - include IgnorableColumns - - ignore_column :u2f_registration_id, remove_with: '16.2', remove_after: '2023-06-22' - belongs_to :user validates :credential_xid, :public_key, :counter, presence: true diff --git a/app/views/admin/application_settings/appearances/_form.html.haml b/app/views/admin/application_settings/appearances/_form.html.haml index 1b0e974a0ca..fb5c320268e 100644 --- a/app/views/admin/application_settings/appearances/_form.html.haml +++ b/app/views/admin/application_settings/appearances/_form.html.haml @@ -3,9 +3,8 @@ = gitlab_ui_form_for @appearance, url: admin_application_settings_appearances_path, html: { class: 'gl-mt-3' } do |f| = form_errors(@appearance) - .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0= _('Navigation bar') .col-lg-8 @@ -25,7 +24,7 @@ = _('Maximum file size is 1MB. Pages are optimized for a 24px tall header logo') %hr .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 Favicon .col-lg-8 @@ -50,7 +49,7 @@ %hr .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0= _('Sign in/Sign up pages') .col-lg-8 @@ -79,7 +78,7 @@ %hr .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0= _('Progressive Web App (PWA)') .col-lg-8 @@ -111,7 +110,7 @@ %hr .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0= _('New project pages') .col-lg-8 @@ -124,7 +123,7 @@ %hr .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0= _('Profile image guideline') .col-lg-8 diff --git a/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml b/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml index d7bb3a85f3a..2ca037db532 100644 --- a/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml +++ b/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml @@ -2,7 +2,7 @@ %hr .row - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = _('System header and footer') diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index eb3b6587bef..3bb59db32aa 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -5,7 +5,7 @@ -# Render the parent group sidebar while creating a new subgroup/project, see GroupsController#new. - group = @parent_group || @group - - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user) + - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user, organization: @organization) - sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } } diff --git a/app/views/layouts/nav/_ask_duo_button.html.haml b/app/views/layouts/nav/_ask_duo_button.html.haml new file mode 100644 index 00000000000..f17ccfc8afe --- /dev/null +++ b/app/views/layouts/nav/_ask_duo_button.html.haml @@ -0,0 +1,13 @@ +- if Gitlab.ee? && ::Gitlab::Llm::TanukiBot.show_breadcrumbs_entry_point_for?(user: current_user) + - label = s_('TanukiBot|Ask GitLab Duo') + = render Pajamas::ButtonComponent.new(variant: :confirm, + category: :secondary, + icon: 'tanuki-ai', + size: 'small', + button_options: { class: 'js-tanuki-bot-chat-toggle gl-ml-3 gl-display-none gl-md-display-inline', data: { track_action: 'click_button', track_label: 'tanuki_bot_breadcrumbs_button' }, aria: { label: label }}) do + = label + = render Pajamas::ButtonComponent.new(variant: :confirm, + category: :secondary, + icon: 'tanuki-ai', + size: 'small', + button_options: { class: 'js-tanuki-bot-chat-toggle has-tooltip gl-ml-3 gl-md-display-none', title: label, data: { track_action: 'click_button', track_label: 'tanuki_bot_breadcrumbs_button', placement: 'left' }, aria: { label: label }}) diff --git a/app/views/layouts/nav/_top_bar.html.haml b/app/views/layouts/nav/_top_bar.html.haml index 4f7e321b3c5..73b253e18bd 100644 --- a/app/views/layouts/nav/_top_bar.html.haml +++ b/app/views/layouts/nav/_top_bar.html.haml @@ -5,10 +5,11 @@ - top_bar_class = [@no_top_bar_container ? 'container-fluid' : container_class, @content_class] - top_bar_container_class = 'gl-border-b' -%div{ class: top_bar_class } - .top-bar-container.gl-display-flex.gl-align-items-center{ :class => top_bar_container_class } +%div{ class: top_bar_class, data: { testid: 'top-bar' } } + .top-bar-container.gl-display-flex.gl-align-items-center.gl-gap-2{ :class => top_bar_container_class } - if show_super_sidebar? - = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'js-super-sidebar-toggle-expand super-sidebar-toggle gl-ml-n3 gl-mr-2', title: _('Expand sidebar'), aria: { controls: 'super-sidebar', expanded: 'false', label: _('Navigation sidebar') } }) + = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'js-super-sidebar-toggle-expand super-sidebar-toggle gl-ml-n3', title: _('Expand sidebar'), aria: { controls: 'super-sidebar', expanded: 'false', label: _('Navigation sidebar') } }) - elsif defined?(@left_sidebar) - = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3 gl-mr-2', data: { testid: 'toggle_mobile_nav_button' }, aria: { label: _('Open sidebar') } }) + = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3', data: { testid: 'toggle_mobile_nav_button' }, aria: { label: _('Open sidebar') } }) = render "layouts/nav/breadcrumbs/breadcrumbs" + = render "layouts/nav/ask_duo_button" diff --git a/app/views/layouts/nav/sidebar/_organization.html.haml b/app/views/layouts/nav/sidebar/_organization.html.haml new file mode 100644 index 00000000000..de6c87f97d7 --- /dev/null +++ b/app/views/layouts/nav/sidebar/_organization.html.haml @@ -0,0 +1 @@ += render partial: 'shared/nav/sidebar', object: Sidebars::Organizations::Panel.new(organization_sidebar_context(@organization, current_user)) diff --git a/app/views/layouts/organization.html.haml b/app/views/layouts/organization.html.haml new file mode 100644 index 00000000000..5a357c6f805 --- /dev/null +++ b/app/views/layouts/organization.html.haml @@ -0,0 +1,6 @@ +- page_title @organization.name +- header_title @organization.name, organization_path(@organization) +- nav "organization" +- @left_sidebar = true + += render template: "layouts/application" diff --git a/app/views/organizations/organizations/directory.html.haml b/app/views/organizations/organizations/directory.html.haml deleted file mode 100644 index 1d2fb66112b..00000000000 --- a/app/views/organizations/organizations/directory.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- breadcrumb_title @organization.name -- page_title @organization.name diff --git a/app/views/organizations/organizations/groups_and_projects.html.haml b/app/views/organizations/organizations/groups_and_projects.html.haml new file mode 100644 index 00000000000..6c0a0e25b2a --- /dev/null +++ b/app/views/organizations/organizations/groups_and_projects.html.haml @@ -0,0 +1 @@ +- page_title _('Groups and projects') diff --git a/app/views/organizations/organizations/show.html.haml b/app/views/organizations/organizations/show.html.haml new file mode 100644 index 00000000000..8ba2a3d96ac --- /dev/null +++ b/app/views/organizations/organizations/show.html.haml @@ -0,0 +1,2 @@ +- page_title s_('Organization|Organization overview') +- @skip_current_level_breadcrumb = true diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a29a0d1b668..09b6117969c 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -15,11 +15,10 @@ = html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe } .js-search-settings-section.gl-pb-6 - .profile-settings-sidebar - %h4.gl-my-0 - = s_('Profiles|Two-factor authentication') - %p.gl-text-secondary - = s_("Profiles|Increase your account's security by enabling two-factor authentication (2FA).") + %h4.gl-my-0 + = s_('Profiles|Two-factor authentication') + %p.gl-text-secondary + = s_("Profiles|Increase your account's security by enabling two-factor authentication (2FA).") %div %p %span.gl-font-weight-bold @@ -34,37 +33,33 @@ - if display_providers_on_profile? .js-search-settings-section.gl-border-t.gl-py-6 - .profile-settings-sidebar - %h4.gl-my-0 - = s_('Profiles|Service sign-in') - %p.gl-text-secondary - = s_('Profiles|Connect a service for sign-in.') + %h4.gl-my-0 + = s_('Profiles|Service sign-in') + %p.gl-text-secondary + = s_('Profiles|Connect a service for sign-in.') = render 'providers', providers: button_based_providers, group_saml_identities: local_assigns[:group_saml_identities] - if current_user.can_change_username? .js-search-settings-section.gl-border-t.gl-py-6 - .profile-settings-sidebar - %h4.gl-my-0.warning-title - = s_('Profiles|Change username') - %p.gl-text-secondary - = s_('Profiles|Changing your username can have unintended side effects.') - = succeed '.' do - = link_to _('Learn more'), help_page_path('user/profile/index', anchor: 'change-your-username'), target: '_blank', rel: 'noopener noreferrer' + %h4.gl-my-0.warning-title + = s_('Profiles|Change username') + %p.gl-text-secondary + = s_('Profiles|Changing your username can have unintended side effects.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/index', anchor: 'change-your-username'), target: '_blank', rel: 'noopener noreferrer' - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) } #update-username{ data: data } - if prevent_delete_account? .js-search-settings-section.gl-border-t.gl-py-6 - .profile-settings-sidebar - %h4.gl-my-0.danger-title - = s_('Profiles|Delete account') + %h4.gl-my-0.danger-title + = s_('Profiles|Delete account') %p.gl-text-secondary = s_('Profiles|Account deletion is not allowed by your administrator.') - else .js-search-settings-section.gl-border-t.gl-py-6 - .profile-settings-sidebar - %h4.gl-mt-0.danger-title - = s_('Profiles|Delete account') + %h4.gl-mt-0.danger-title + = s_('Profiles|Delete account') - if current_user.can_be_removed? && can?(current_user, :destroy_user, current_user) %p.gl-text-secondary = s_('Profiles|Deleting an account has the following effects:') diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml index 1952655937e..967918eb5c2 100644 --- a/app/views/profiles/active_sessions/index.html.haml +++ b/app/views/profiles/active_sessions/index.html.haml @@ -2,7 +2,7 @@ - @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 44cfbc1f74f..21f77d7d98e 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -2,7 +2,7 @@ - @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml index 264ee040d7d..a03ccc2c284 100644 --- a/app/views/profiles/chat_names/index.html.haml +++ b/app/views/profiles/chat_names/index.html.haml @@ -3,7 +3,7 @@ - @force_desktop_expanded_sidebar = true .row.gl-mt-5.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 5686adaa3f5..41e061ebfee 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -2,7 +2,7 @@ - @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml index b21a4da16b9..6e268e546a8 100644 --- a/app/views/profiles/gpg_keys/index.html.haml +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -3,7 +3,7 @@ - @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p @@ -11,7 +11,7 @@ .col-lg-8 %h5.gl-mt-0 = _('Add a GPG key') - %p.profile-settings-content + %p - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/gpg_signed_commits/index.md') } = _('Add a GPG key for secure access to GitLab. %{help_link_start}Learn more.%{help_link_end}').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe } = render 'form' diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index e7c0cf813b5..15f5dcef798 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -3,7 +3,7 @@ - @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p @@ -17,7 +17,7 @@ .col-lg-8 %h5.gl-mt-0 = _('Add an SSH key') - %p.profile-settings-content + %p - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/ssh.md') } = _('Add an SSH key for secure access to GitLab. %{help_link_start}Learn more.%{help_link_end}').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe } = render 'form' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 06d37787d2e..baca20cdbec 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -12,7 +12,7 @@ = hidden_field_tag :notification_type, 'global' .row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar + .col-lg-4 %h4.gl-mt-0 = page_title %p diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 6182c649836..4e2caaa8190 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -33,7 +33,7 @@ %hr .row.js-preferences-form.js-search-settings-section - .col-lg-4.profile-settings-sidebar#syntax-highlighting-theme + .col-lg-4#syntax-highlighting-theme %h4.gl-mt-0 = s_('Preferences|Syntax highlighting theme') %p @@ -52,7 +52,7 @@ %hr .row.js-preferences-form.js-search-settings-section - .col-lg-4.profile-settings-sidebar#diffs-colors + .col-lg-4#diffs-colors %h4.gl-mt-0 = s_('Preferences|Diff colors') %p @@ -65,7 +65,7 @@ %hr .row.js-preferences-form.js-search-settings-section - .col-lg-4.profile-settings-sidebar#behavior + .col-lg-4#behavior %h4.gl-mt-0 = s_('Preferences|Behavior') %p @@ -122,7 +122,7 @@ .col-sm-12 %hr .row.js-preferences-form.js-search-settings-section - .col-lg-4.profile-settings-sidebar#localization + .col-lg-4#localization %h4.gl-mt-0 = _('Localization') %p @@ -143,7 +143,7 @@ .col-sm-12 %hr .row.js-preferences-form.js-search-settings-section - .col-lg-4.profile-settings-sidebar#time-preferences + .col-lg-4#time-preferences %h4.gl-mt-0 = s_('Preferences|Time preferences') %p @@ -159,7 +159,7 @@ .row.js-preferences-form.js-search-settings-section .col-sm-12 %hr - .col-lg-4.profile-settings-sidebar#enabled_following + .col-lg-4#enabled_following %h4.gl-mt-0 = s_('Preferences|Enable follow users feature') %p diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml index 0f290f34a95..8821804ce6b 100644 --- a/app/views/shared/deploy_tokens/_form.html.haml +++ b/app/views/shared/deploy_tokens/_form.html.haml @@ -1,4 +1,4 @@ -%p.profile-settings-content +%p - group_deploy_tokens_help_link_url = help_page_path('user/project/deploy_tokens/index.md') - group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url } = s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe } diff --git a/app/views/shared/doorkeeper/applications/_index.html.haml b/app/views/shared/doorkeeper/applications/_index.html.haml index 31407c60e84..284fbebaa57 100644 --- a/app/views/shared/doorkeeper/applications/_index.html.haml +++ b/app/views/shared/doorkeeper/applications/_index.html.haml @@ -1,17 +1,16 @@ - @force_desktop_expanded_sidebar = true .js-search-settings-section - .profile-settings-sidebar - %h4.gl-my-0 - = page_title - %p.gl-text-secondary - - if oauth_applications_enabled - - if oauth_authorized_applications_enabled - = _("Manage applications that can use GitLab as an OAuth provider, and applications that you've authorized to use your account.") - - else - = _("Manage applications that use GitLab as an OAuth provider.") + %h4.gl-my-0 + = page_title + %p.gl-text-secondary + - if oauth_applications_enabled + - if oauth_authorized_applications_enabled + = _("Manage applications that can use GitLab as an OAuth provider, and applications that you've authorized to use your account.") - else - = _("Manage applications that you've authorized to use your account.") + = _("Manage applications that use GitLab as an OAuth provider.") + - else + = _("Manage applications that you've authorized to use your account.") - if oauth_applications_enabled %h5.gl-mt-0 = _('Add new application') diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index c6f3e4d97a8..a27bb506c87 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -2,7 +2,6 @@ - dropdown_options = assignees_dropdown_options(issuable_type) .js-sidebar-assignees-root{ data: { field: issuable_type, - signed_in: signed_in, max_assignees: dropdown_options[:data][:"max-select"], directly_invite_members: can_admin_project_member?(@project) } } .title.hide-collapsed diff --git a/config/feature_flags/development/tanuki_bot_breadcrumbs_entry_point.yml b/config/feature_flags/development/tanuki_bot_breadcrumbs_entry_point.yml new file mode 100644 index 00000000000..92fbcddd3cc --- /dev/null +++ b/config/feature_flags/development/tanuki_bot_breadcrumbs_entry_point.yml @@ -0,0 +1,8 @@ +--- +name: tanuki_bot_breadcrumbs_entry_point +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123530 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416423 +milestone: '16.2' +type: development +group: group::foundations +default_enabled: false diff --git a/config/routes/organizations.rb b/config/routes/organizations.rb index c35059cf411..4a8c9c25363 100644 --- a/config/routes/organizations.rb +++ b/config/routes/organizations.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -resources :organizations, only: [], param: :organization_path, controller: 'organizations/organizations' do +resources :organizations, only: [:show], param: :organization_path, controller: 'organizations/organizations' do member do - get :directory + get :groups_and_projects end end diff --git a/db/post_migrate/20230705145827_drop_wrong_index_on_vulnerability_occurrences.rb b/db/post_migrate/20230705145827_drop_wrong_index_on_vulnerability_occurrences.rb new file mode 100644 index 00000000000..e8a41cf9f04 --- /dev/null +++ b/db/post_migrate/20230705145827_drop_wrong_index_on_vulnerability_occurrences.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class DropWrongIndexOnVulnerabilityOccurrences < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'tmp_idx_vulns_on_converted_uuid' + + def up + # We do not want to drop this from Gitlab.com + # because it was created correctly there + return if Gitlab.com? + + remove_concurrent_index_by_name( + :vulnerability_occurrences, + INDEX_NAME + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20230705150100_recreate_type_migration_index_on_vulnerability_occurrences.rb b/db/post_migrate/20230705150100_recreate_type_migration_index_on_vulnerability_occurrences.rb new file mode 100644 index 00000000000..9e426c61874 --- /dev/null +++ b/db/post_migrate/20230705150100_recreate_type_migration_index_on_vulnerability_occurrences.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class RecreateTypeMigrationIndexOnVulnerabilityOccurrences < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'tmp_idx_vulns_on_converted_uuid' + WHERE_CLAUSE = "uuid_convert_string_to_uuid = '00000000-0000-0000-0000-000000000000'::uuid" + + def up + add_concurrent_index( + :vulnerability_occurrences, + %i[id uuid_convert_string_to_uuid], + name: INDEX_NAME, + where: WHERE_CLAUSE + ) + end + + def down + remove_concurrent_index_by_name( + :vulnerability_occurrences, + INDEX_NAME + ) + end +end diff --git a/db/schema_migrations/20230705145827 b/db/schema_migrations/20230705145827 new file mode 100644 index 00000000000..48bf419a084 --- /dev/null +++ b/db/schema_migrations/20230705145827 @@ -0,0 +1 @@ +c6bd06a0e306916809474790126ba90089c8f0dd8d25e3858f2e7b416996c9c8
\ No newline at end of file diff --git a/db/schema_migrations/20230705150100 b/db/schema_migrations/20230705150100 new file mode 100644 index 00000000000..07da63247f9 --- /dev/null +++ b/db/schema_migrations/20230705150100 @@ -0,0 +1 @@ +62ea862f8911a5114cfa8140862e1a3c5be7cf4f80064d72c453f8c81d8a7623
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f50f65fbd6b..a7dc468425f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -33650,6 +33650,8 @@ CREATE INDEX tmp_idx_vuln_reads_where_dismissal_reason_null ON vulnerability_rea CREATE INDEX tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99 ON vulnerability_occurrences USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99])); +CREATE INDEX tmp_idx_vulns_on_converted_uuid ON vulnerability_occurrences USING btree (id, uuid_convert_string_to_uuid) WHERE (uuid_convert_string_to_uuid = '00000000-0000-0000-0000-000000000000'::uuid); + CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL)); CREATE INDEX tmp_index_cis_vulnerability_reads_on_id ON vulnerability_reads USING btree (id) WHERE (report_type = 7); diff --git a/doc/administration/raketasks/service_desk_email.md b/doc/administration/raketasks/service_desk_email.md index 1cbdec35171..9bf0846fef2 100644 --- a/doc/administration/raketasks/service_desk_email.md +++ b/doc/administration/raketasks/service_desk_email.md @@ -12,7 +12,7 @@ The following are Service Desk email-related Rake tasks. ## Secrets -GitLab can use [Service Desk email](../../user/project/service_desk.md#configure-a-custom-mailbox) secrets read from an encrypted file instead of storing them in plaintext in the file system. The following Rake tasks are provided for updating the contents of the encrypted file. +GitLab can use [Service Desk email](../../user/project/service_desk.md#configure-service-desk-alias-email) secrets read from an encrypted file instead of storing them in plaintext in the file system. The following Rake tasks are provided for updating the contents of the encrypted file. ### Show secret diff --git a/doc/ci/docker/authenticate_registry.md b/doc/ci/docker/authenticate_registry.md index 224d0cdf7aa..52cc3071fda 100644 --- a/doc/ci/docker/authenticate_registry.md +++ b/doc/ci/docker/authenticate_registry.md @@ -17,14 +17,14 @@ In [`before_script`](../yaml/index.md#before_script), run `docker login`: ```yaml -image: docker:20.10.16 +default: + image: docker:20.10.16 + services: + - docker:20.10.16-dind variables: DOCKER_TLS_CERTDIR: "/certs" -services: - - docker:20.10.16-dind - build: stage: build before_script: @@ -125,14 +125,14 @@ The following example shows [`before_script`](../yaml/index.md#before_script). The same commands apply for any solution you implement. ```yaml -image: docker:20.10.16 +default: + image: docker:20.10.16 + services: + - docker:20.10.16-dind variables: DOCKER_TLS_CERTDIR: "/certs" -services: - - docker:20.10.16-dind - build: stage: build before_script: diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index e1393c7feca..5e8f2b36620 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -556,10 +556,10 @@ You do not need to include the `docker:20.10.16-dind` service, like you do when you use the Docker-in-Docker executor: ```yaml -image: docker:20.10.16 - -before_script: - - docker info +default: + image: docker:20.10.16 + before_script: + - docker info build: stage: build diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 022aa7c3093..d3a2f4ece06 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -74,10 +74,8 @@ services that you want to use during runtime: ```yaml default: image: ruby:2.6 - services: - postgres:11.7 - before_script: - bundle install diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index f2fb8eaa27e..871d8854416 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -159,10 +159,10 @@ deploy_review_app: environment: name: review/$CI_COMMIT_REF_SLUG url: https://$CI_ENVIRONMENT_SLUG.example.com - only: - - branches - except: - - main + rules: + - if: $CI_COMMIT_BRANCH == "main" + when: never + - if: $CI_COMMIT_BRANCH ``` #### Set a dynamic environment URL diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md index b8243c2f2cb..5d1ae0048af 100644 --- a/doc/ci/examples/deployment/composer-npm-deploy.md +++ b/doc/ci/examples/deployment/composer-npm-deploy.md @@ -84,8 +84,8 @@ stage_deploy: artifacts: paths: - build/ - only: - - dev + rules: + - if: $CI_COMMIT_BRANCH == "dev" script: - ssh-add <(echo "$STAGING_PRIVATE_KEY") - ssh -p22 server_user@server_host "mkdir htdocs/wp-content/themes/_tmp" @@ -96,7 +96,7 @@ stage_deploy: Here's the breakdown: -1. `only:dev` means that this build runs only when something is pushed to the `dev` branch. You can remove this block completely and have everything run on every push (but probably this is something you don't want). +1. `rules:if: $CI_COMMIT_BRANCH == "dev"` means that this build runs only when something is pushed to the `dev` branch. You can remove this block completely and have everything run on every push (but probably this is something you don't want). 1. `ssh-add ...` we add that private key you added on the web UI to the Docker container. 1. We connect via `ssh` and create a new `_tmp` folder. 1. We connect via `scp` and upload the `build` folder (which was generated by a `npm` script) to our previously created `_tmp` folder. @@ -136,8 +136,8 @@ stage_deploy: artifacts: paths: - build/ - only: - - dev + rules: + - if: $CI_COMMIT_BRANCH == "dev" before_script: - apt-get update - apt-get install zip unzip diff --git a/doc/ci/examples/deployment/index.md b/doc/ci/examples/deployment/index.md index a156cf1acb0..793e60d517c 100644 --- a/doc/ci/examples/deployment/index.md +++ b/doc/ci/examples/deployment/index.md @@ -69,8 +69,8 @@ staging: - apt-get install -y ruby-dev - gem install dpl - dpl heroku api --app=my-app-staging --api_key=$HEROKU_STAGING_API_KEY - only: - - main + rules: + - if: $CI_COMMIT_BRANCH == "main" environment: staging ``` @@ -93,8 +93,8 @@ staging: script: - gem install dpl - dpl heroku api --app=my-app-staging --api_key=$HEROKU_STAGING_API_KEY - only: - - main + rules: + - if: $CI_COMMIT_BRANCH == "main" environment: staging production: @@ -102,8 +102,8 @@ production: script: - gem install dpl - dpl heroku api --app=my-app-production --api_key=$HEROKU_PRODUCTION_API_KEY - only: - - tags + rules: + - if: $CI_COMMIT_TAG environment: production ``` diff --git a/doc/ci/examples/end_to_end_testing_webdriverio/index.md b/doc/ci/examples/end_to_end_testing_webdriverio/index.md index 4dc3af2d642..33d1cf579c0 100644 --- a/doc/ci/examples/end_to_end_testing_webdriverio/index.md +++ b/doc/ci/examples/end_to_end_testing_webdriverio/index.md @@ -219,22 +219,27 @@ on GitLab CI/CD! To recap, our `.gitlab-ci.yml` configuration file looks something like this: ```yaml -image: node:8.10 +default: + image: node:8.10 + stages: - deploy - confidence-check + deploy_terraform: stage: deploy script: # Your Review App deployment scripts - for a working example please check https://gitlab.com/Flockademic/Flockademic/blob/5a45f1c2412e93810fab50e2dab8949e2d0633c7/.gitlab-ci.yml#L315 - echo environment: production + e2e:firefox: stage: confidence-check services: - selenium/standalone-firefox script: - npm run confidence-check --host=selenium__standalone-firefox + e2e:chrome: stage: confidence-check services: diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index cf12943d279..14d8e0fc72f 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -481,10 +481,10 @@ In order to build and test our app with GitLab CI/CD, we need a file called `.gi Our `.gitlab-ci.yml` file will look like this: ```yaml -image: registry.gitlab.com/<USERNAME>/laravel-sample:latest - -services: - - mysql:5.7 +default: + image: registry.gitlab.com/<USERNAME>/laravel-sample:latest + services: + - mysql:5.7 variables: MYSQL_DATABASE: homestead @@ -513,14 +513,13 @@ deploy_production: - ssh-add <(echo "$SSH_PRIVATE_KEY") - mkdir -p ~/.ssh - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - - ~/.composer/vendor/bin/envoy run deploy --commit="$CI_COMMIT_SHA" environment: name: production url: http://192.168.1.1 when: manual - only: - - main + rules: + - if: $CI_COMMIT_BRANCH == "main" ``` That's a lot to take in, isn't it? Let's run through it step by step. @@ -533,10 +532,10 @@ The `services` keyword defines additional images [that are linked to the main im Here we use the container image we created before as our main image and also use MySQL 5.7 as a service. ```yaml -image: registry.gitlab.com/<USERNAME>/laravel-sample:latest - -services: - - mysql:5.7 +default: + image: registry.gitlab.com/<USERNAME>/laravel-sample:latest + services: + - mysql:5.7 ... ``` @@ -592,7 +591,7 @@ If the SSH keys have added successfully, we can run Envoy. As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well. The [environment](../../yaml/index.md#environment) keyword tells GitLab that this job deploys to the `production` environment. The `url` keyword is used to generate a link to our application on the GitLab Environments page. -The `only` keyword tells GitLab CI/CD that the job should be executed only when the pipeline is building the `main` branch. +The `rules:if` keyword tells GitLab CI/CD that the job should be executed only when the pipeline is building the `main` branch. Lastly, `when: manual` is used to turn the job from running automatically to a manual action. ```yaml @@ -604,16 +603,14 @@ deploy_production: - ssh-add <(echo "$SSH_PRIVATE_KEY") - mkdir -p ~/.ssh - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - # Run Envoy - ~/.composer/vendor/bin/envoy run deploy - environment: name: production url: http://192.168.1.1 when: manual - only: - - main + rules: + - if: $CI_COMMIT_BRANCH == "main" ``` You may also want to add another job for [staging environment](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/), to final test your application before deploying to production. diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md index dadf631e2bb..cdd00fd2412 100644 --- a/doc/ci/pipelines/pipeline_architectures.md +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -66,7 +66,8 @@ stages: - test - deploy -image: alpine +default: + image: alpine build_a: stage: build @@ -132,7 +133,8 @@ stages: - test - deploy -image: alpine +default: + image: alpine build_a: stage: build @@ -251,7 +253,8 @@ stages: - test - deploy -image: alpine +default: + image: alpine build_a: stage: build @@ -281,7 +284,8 @@ stages: - test - deploy -image: alpine +default: + image: alpine build_b: stage: build diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index ddb8f0aaa61..ed40d3533ee 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -255,7 +255,7 @@ to replace those values at runtime when each review app is created: variable. - `data-merge-request-id` is the merge request ID, which can be found by the `CI_MERGE_REQUEST_IID` variable. `CI_MERGE_REQUEST_IID` is available only if - [`only: [merge_requests]`](../pipelines/merge_request_pipelines.md) + [`rules:if: $CI_PIPELINE_SOURCE == "merge_request_event`](../pipelines/merge_request_pipelines.md#use-rules-to-add-jobs) is used and the merge request is created. - `data-mr-url` is the URL of the GitLab instance and is the same for all review apps. diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md index efcb1cac755..f4c90934e06 100644 --- a/doc/ci/services/index.md +++ b/doc/ci/services/index.md @@ -145,13 +145,11 @@ default: image: name: ruby:2.6 entrypoint: ["/bin/bash"] - services: - name: my-postgres:11.7 alias: db-postgres entrypoint: ["/usr/local/bin/db-postgres"] command: ["start"] - before_script: - bundle install @@ -411,7 +409,7 @@ To enable service logging, add the `CI_DEBUG_SERVICES` variable to the project's ```yaml variables: - CI_DEBUG_SERVICES: "true" + CI_DEBUG_SERVICES: "true" ``` Accepted values are: diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index afb14bd976f..ab38d2ce934 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -23,8 +23,9 @@ For more information, see [GitLab CI/CD variables](../variables/index.md). First, in your `.gitlab-ci.yml` add: ```yaml -services: - - postgres:12.2-alpine +default: + services: + - postgres:12.2-alpine variables: POSTGRES_DB: $POSTGRES_DB diff --git a/doc/ci/testing/browser_performance_testing.md b/doc/ci/testing/browser_performance_testing.md index ae31679c62b..059cd637f9e 100644 --- a/doc/ci/testing/browser_performance_testing.md +++ b/doc/ci/testing/browser_performance_testing.md @@ -133,9 +133,9 @@ be extended for dynamic environments, but a few extra steps are required: 1. In the `review` job: 1. Generate a URL list file with the dynamic URL. 1. Save the file as an artifact, for example with `echo $CI_ENVIRONMENT_URL > environment_url.txt` - in your job's `script`. + in your job's `script`. 1. Pass the list as the URL environment variable (which can be a URL or a file containing URLs) - to the `browser_performance` job. + to the `browser_performance` job. 1. You can now run the sitespeed.io container against the desired hostname and paths. @@ -160,10 +160,10 @@ review: artifacts: paths: - environment_url.txt - only: - - branches - except: - - master + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: never + - if: $CI_COMMIT_BRANCH browser_performance: dependencies: diff --git a/doc/ci/yaml/includes.md b/doc/ci/yaml/includes.md index 6a616854c0b..503b9f164dd 100644 --- a/doc/ci/yaml/includes.md +++ b/doc/ci/yaml/includes.md @@ -129,7 +129,8 @@ Content of `.gitlab-ci.yml`: ```yaml include: 'https://company.com/autodevops-template.yml' -image: alpine:latest +default: + image: alpine:latest variables: POSTGRES_USER: root diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 0db2fb0320f..49a3e8fd70e 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3158,8 +3158,7 @@ CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitl To create a release when a new tag is added to the project: - Use the `$CI_COMMIT_TAG` CI/CD variable as the `tag_name`. -- Use [`rules:if`](#rulesif) or [`only: tags`](#onlyrefs--exceptrefs) to configure - the job to run only for new tags. +- Use [`rules:if`](#rulesif) to configure the job to run only for new tags. ```yaml job: @@ -3171,7 +3170,7 @@ job: - if: $CI_COMMIT_TAG ``` -To create a release and a new tag at the same time, your [`rules`](#rules) or [`only`](#only--except) +To create a release and a new tag at the same time, your [`rules`](#rules) should **not** configure the job to run only for new tags. A semantic versioning example: ```yaml diff --git a/doc/development/pipelines/internals.md b/doc/development/pipelines/internals.md index 83ab63e5812..be72fc50988 100644 --- a/doc/development/pipelines/internals.md +++ b/doc/development/pipelines/internals.md @@ -121,7 +121,7 @@ Docker Hub unless `${GITLAB_DEPENDENCY_PROXY}` is also defined there. ### Work around for when a pipeline is started by a Project access token user -When a pipeline is started by a Project access token user (e.g. the `release-tools approver bot` user which +When a pipeline is started by a Project access token user (for example, the `release-tools approver bot` user which automatically updates the Gitaly version used in the main project), [the Dependency proxy isn't accessible](https://gitlab.com/gitlab-org/gitlab/-/issues/332411#note_1130388163) and the job fails at the `Preparing the "docker+machine" executor` step. @@ -243,7 +243,7 @@ and included in `rules` definitions via [YAML anchors](../../ci/yaml/yaml_optimi - If you need to **extend a hash**, you should use `extends` - If you need to **extend an array**, you'll need to use `!reference`, or `YAML anchors` as last resort -- For more complex cases (e.g. extend hash inside array, extend array inside hash, ...), you'll have to use `!reference` or `YAML anchors` +- For more complex cases (for example, extend hash inside array, extend array inside hash, ...), you'll have to use `!reference` or `YAML anchors` #### What can `extends` and `YAML anchors` do? diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 7785819d1e0..950f4e19a5f 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -158,7 +158,7 @@ We should reduce test dependencies, and avoiding capabilities also reduces the amount of set-up needed. `:js` is particularly important to avoid. This must only be used if the feature -test requires JavaScript reactivity in the browser (e.g. clicking a Vue.js component). Using a headless +test requires JavaScript reactivity in the browser (for example, clicking a Vue.js component). Using a headless browser is much slower than parsing the HTML response from the app. #### Profiling: see where your test spend its time @@ -190,7 +190,7 @@ speedscope tmp/<your-json-report>.json Below are some useful tips to interpret and navigate the flamegraph: -- There are [several views available](https://github.com/jlfwong/speedscope#views) for the flamegraph. `Left Heavy` is particularly useful when there are a lot of function calls (e.g. feature specs). +- There are [several views available](https://github.com/jlfwong/speedscope#views) for the flamegraph. `Left Heavy` is particularly useful when there are a lot of function calls (for example, feature specs). - You can zoom in or out! See [the navigation documentation](https://github.com/jlfwong/speedscope#navigation) - If you are working on a slow feature test, search for `Capybara::DSL#` in the search to see the capybara actions that are made, and how long they take! @@ -413,7 +413,7 @@ Refrain from using this stub helper if the test code relies on persisting We can use the `rspec_profiling` gem to diagnose, for instance, the number of SQL queries we're making when running a test. -This could be caused by some application side SQL queries **triggered by a test that could mock parts that are not under test** (e.g. [!123810](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123810)). +This could be caused by some application side SQL queries **triggered by a test that could mock parts that are not under test** (for example, [!123810](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123810)). [See the instructions in the performance docs](../performance.md#rspec-profiling). diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index 9114ec3d179..d111021f486 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -63,7 +63,7 @@ difficult to achieve locally. Ordering issues are easier to reproduce by repeate `master` if the order of tests changes. - [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91016/diffs): A test asserts that trying to find a record with an nonexistent ID returns an error message. The test uses an - hardcoded ID that's supposed to not exist (e.g. `42`). If the test is run early in the test + hardcoded ID that's supposed to not exist (for example, `42`). If the test is run early in the test suite, it might pass as not enough records were created before it, but as soon as it would run later in the suite, there could be a record that actually has the ID `42`, hence the test would start to fail. diff --git a/doc/development/testing_guide/test_results_tracking.md b/doc/development/testing_guide/test_results_tracking.md index 586c6059fc0..92c1e0917c7 100644 --- a/doc/development/testing_guide/test_results_tracking.md +++ b/doc/development/testing_guide/test_results_tracking.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w We developed the [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling) gem that includes several commands to automate test results tracking. -The goal of this gem is to have a consolidated set of tooling that we use across our various test suite (e.g. GitLab Rails & E2E test suites). +The goal of this gem is to have a consolidated set of tooling that we use across our various test suite (for example, GitLab Rails & E2E test suites). The initial motivation and development was tracked by [this epic](https://gitlab.com/groups/gitlab-org/-/epics/10536). diff --git a/doc/development/workhorse/configuration.md b/doc/development/workhorse/configuration.md index e69f16c41f8..84fb557d9ec 100644 --- a/doc/development/workhorse/configuration.md +++ b/doc/development/workhorse/configuration.md @@ -53,9 +53,9 @@ Options: -logFormat string Log format to use defaults to text (text, json, structured, none) (default "text") -pprofListenAddr string - pprof listening address, e.g. 'localhost:6060' + pprof listening address, for example, 'localhost:6060' -prometheusListenAddr string - Prometheus listening address, e.g. 'localhost:9229' + Prometheus listening address, for example, 'localhost:9229' -propagateCorrelationID X-Request-ID Reuse existing Correlation-ID from the incoming request header X-Request-ID if present -proxyHeadersTimeout duration diff --git a/doc/security/hardening_operating_system_recommendations.md b/doc/security/hardening_operating_system_recommendations.md index 8b4b706815c..33f88d43d22 100644 --- a/doc/security/hardening_operating_system_recommendations.md +++ b/doc/security/hardening_operating_system_recommendations.md @@ -116,7 +116,7 @@ vm.mmap_min_addr=4096 # Default is 0, randomize virtual address space in memory, makes vuln exploitation # harder kernel.randomize_va_space=2 -# Restrict kernel pointer access (e.g. cat /proc/kallsyms) for exploit assistance +# Restrict kernel pointer access (for example, cat /proc/kallsyms) for exploit assistance kernel.kptr_restrict=2 # Restrict verbose kernel errors in dmesg kernel.dmesg_restrict=1 diff --git a/doc/topics/offline/quick_start_guide.md b/doc/topics/offline/quick_start_guide.md index cc450fed56a..27ca8784876 100644 --- a/doc/topics/offline/quick_start_guide.md +++ b/doc/topics/offline/quick_start_guide.md @@ -147,7 +147,7 @@ Updating CA certificates... Runtime platform arch=amd64 os=linux pid=7 revision=1b659122 version=12.8.0 Running in system-mode. -Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/): +Please enter the gitlab-ci coordinator URL (for example, https://gitlab.com/): https://my-host.internal Please enter the gitlab-ci token for this runner: XXXXXXXXXXX @@ -158,7 +158,7 @@ Please enter the gitlab-ci tags for this runner (comma separated): Registering runner... succeeded runner=FSMwkvLZ Please enter the executor: custom, docker, virtualbox, kubernetes, docker+machine, docker-ssh+machine, docker-ssh, parallels, shell, ssh: docker -Please enter the default Docker image (e.g. ruby:2.6): +Please enter the default Docker image (for example, ruby:2.6): ruby:2.6 Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! ``` diff --git a/doc/user/application_security/comparison_dependency_and_container_scanning.md b/doc/user/application_security/comparison_dependency_and_container_scanning.md index 736bb57c387..d02d94b7a3e 100644 --- a/doc/user/application_security/comparison_dependency_and_container_scanning.md +++ b/doc/user/application_security/comparison_dependency_and_container_scanning.md @@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Dependency Scanning compared to Container Scanning -GitLab offers both Dependency Scanning and Container Scanning to ensure coverage for all of these +GitLab offers both [Dependency Scanning](dependency_scanning/index.md) and +[Container Scanning](container_scanning/index.md) to ensure coverage for all of these dependency types. To cover as much of your risk area as possible, we encourage you to use all of our security scanning tools: diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 82d0bfb035a..66a26ebc66a 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -61,11 +61,11 @@ and has its own dedicated IP addresses: The IP addresses for `mg.gitlab.com` are subject to change at any time. -### Service Desk custom mailbox +### Service Desk alias email address On GitLab.com, there's a mailbox configured for Service Desk with the email address: `contact-project+%{key}@incoming.gitlab.com`. To use this mailbox, configure the -[custom suffix](../project/service_desk.md#configure-a-custom-email-address-suffix) in project +[custom suffix](../project/service_desk.md#configure-a-suffix-for-service-desk-alias-email) in project settings. ## Backups diff --git a/doc/user/group/access_and_permissions.md b/doc/user/group/access_and_permissions.md index b7acded6720..060e47992b8 100644 --- a/doc/user/group/access_and_permissions.md +++ b/doc/user/group/access_and_permissions.md @@ -253,6 +253,8 @@ For more information on the administration of LDAP and group sync, refer to the NOTE: When you add LDAP synchronization, if an LDAP user is a group member and they are not part of the LDAP group, they are removed from the group. +You can use a workaround to [manage project access through LDAP groups](../project/settings/index.md#manage-project-access-through-ldap-groups). + ### Create group links via CN **(PREMIUM SELF)** To create group links via CN: diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md index 8ea762777ac..4b7ca6bbd8e 100644 --- a/doc/user/project/canary_deployments.md +++ b/doc/user/project/canary_deployments.md @@ -134,7 +134,7 @@ Here's an example using [GraphiQL](../../api/graphql/getting_started.md#graphiql mutation { environmentsCanaryIngressUpdate(input:{ id: "gid://gitlab/Environment/29", # Your Environment ID. You can get the ID from the URL of the environment page. - weight: 45 # The new traffic weight. e.g. If you set `45`, 45% of traffic goes to a canary deployment and 55% of traffic goes to a stable deployment. + weight: 45 # The new traffic weight. for example, If you set `45`, 45% of traffic goes to a canary deployment and 55% of traffic goes to a stable deployment. }) { errors } diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md index 71b4bff41d1..7331ce9c5ba 100644 --- a/doc/user/project/file_lock.md +++ b/doc/user/project/file_lock.md @@ -213,7 +213,9 @@ To lock a file: If you do not have permission to lock the file, the button is not enabled. -To view the user who locked the file (if it was not you), hover over the button. +To view the user who locked a directory (if it was not you), hover over the button. Reinstatement of +similar functionality for locked files is discussed in +[issue 376222](https://gitlab.com/gitlab-org/gitlab/-/issues/376222). ### View and remove existing locks diff --git a/doc/user/project/repository/mirror/push.md b/doc/user/project/repository/mirror/push.md index 9da8ce7acc5..857cd80cde2 100644 --- a/doc/user/project/repository/mirror/push.md +++ b/doc/user/project/repository/mirror/push.md @@ -77,8 +77,12 @@ through the [remote mirrors API](../../../../api/remote_mirrors.md). To configure a mirror from GitLab to GitHub: -1. Create a [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) - with `public_repo` selected. +1. Create a [GitHub fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens) + with at least read and write permissions on the [repository contents](https://docs.github.com/en/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-contents). If your + repository contains a `.github/workflows` directory, you must also grant + read and write access for the [Workflows](https://docs.github.com/en/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-workflows). + For a more fine-grained access, you can configure your token to only apply + to the specific repository. 1. Enter a **Git repository URL** with this format, changing the variables as needed: ```plaintext diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index aed491c4215..ca2d3ba59f9 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -64,7 +64,7 @@ To enable Service Desk in your project: 1. Expand **Service Desk**. 1. Turn on the **Activate Service Desk** toggle. 1. Optional. Complete the fields. - - [Add a suffix](#configure-a-custom-email-address-suffix) to your Service Desk email address. + - [Add a suffix](#configure-a-suffix-for-service-desk-alias-email) to your Service Desk email address. - If the list below **Template to append to all Service Desk issues** is empty, create a [description template](description_templates.md) in your repository. 1. Select **Save changes**. @@ -192,27 +192,26 @@ To edit the custom email display name: 1. Below **Email display name**, enter a new name. 1. Select **Save changes**. -### Use a custom email address **(FREE SELF)** +### Use an additional Service Desk alias email **(FREE SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2201) in GitLab 13.0. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284656) in GitLab 13.8. -You can use a custom email address with Service Desk. +You can use an additional alias email address for Service Desk on an instance level. To do this, you must configure -a [custom mailbox](#configure-a-custom-mailbox). You can also configure a -[custom suffix](#configure-a-custom-email-address-suffix). +a [`service_desk_email`](#configure-service-desk-alias-email) in the instance configuration. You can also configure a +[custom suffix](#configure-a-suffix-for-service-desk-alias-email) that replaces the default `-issue-` portion on the sub-addressing part. -#### Configure a custom mailbox +#### Configure Service Desk alias email NOTE: -On GitLab.com a custom mailbox is already configured with `contact-project+%{key}@incoming.gitlab.com` as the email address, you can still configure the -[custom suffix](#configure-a-custom-email-address-suffix) in project settings. +On GitLab.com a custom mailbox is already configured with `contact-project+%{key}@incoming.gitlab.com` as the email address. You can still configure the +[custom suffix](#configure-a-suffix-for-service-desk-alias-email) in project settings. Service Desk uses the [incoming email](../../administration/incoming_email.md) -configuration by default. However, by using the `service_desk_email` configuration, -you can customize the mailbox used by Service Desk. This allows you to have -a separate email address for Service Desk by also configuring a [custom suffix](#configure-a-custom-email-address-suffix) +configuration by default. However, to have a separate email address for Service Desk, +configure `service_desk_email` with a [custom suffix](#configure-a-suffix-for-service-desk-alias-email) in project settings. Prerequisites: @@ -408,7 +407,7 @@ read about [Helm IMAP secrets](https://docs.gitlab.com/charts/installation/secre > - Alternative Azure deployments [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5978) in GitLab 14.9. > - [Introduced for self-compiled (source) installs](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116494) in GitLab 15.11. -Service Desk can be configured to read Microsoft Exchange Online mailboxes with the Microsoft +`service_desk_email` can be configured to read Microsoft Exchange Online mailboxes with the Microsoft Graph API instead of IMAP. Set up an OAuth 2.0 application for Microsoft Graph [the same way as for incoming email](../../administration/incoming_email.md#microsoft-graph). @@ -620,7 +619,7 @@ configure the `azure_ad_endpoint` and `graph_endpoint` settings: ::EndTabs -#### Configure a custom email address suffix +#### Configure a suffix for Service Desk alias email You can set a custom suffix in your project's Service Desk settings. @@ -631,7 +630,7 @@ When configured, the custom suffix creates a new Service Desk email address, con Prerequisites: -- You must have configured a [custom mailbox](#configure-a-custom-mailbox). +- You must have configured a [Service Desk alias email](#configure-service-desk-alias-email). 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. 1. Select **Settings > General**. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index c2e411a2bb4..31f91d8dc02 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -43,6 +43,9 @@ To assign topics to a project: If you're an instance administrator, you can administer all project topics from the [Admin Area's Topics page](../../admin_area/index.md#administering-topics). +NOTE: +The assigned topics are visible only to users with access to the project, but everyone can see which topics exist on the GitLab instance. Do not include sensitive information in the name of a topic. + ## Add a compliance framework to a project **(PREMIUM)** You can @@ -111,6 +114,23 @@ When you disable a feature, the following additional features are also disabled: - Metrics dashboard access requires reading project environments and deployments. Users with access to the metrics dashboard can also access environments and deployments. +### Manage project access through LDAP groups + +You can [use LDAP to manage group membership](../../group/access_and_permissions.md#manage-group-memberships-via-ldap). + +You cannot use LDAP groups to manage project access, but you can use the following +workaround. + +Prerequisites: + +- You must [integrate LDAP with GitLab](../../../administration/auth/ldap/index.md). +- You must be an administrator. + +1. [Create a group](../../group/index.md#create-a-group) to track membership of your project. +1. [Set up LDAP synchronization](../../../administration/auth/ldap/ldap_synchronization.md) for that group. +1. To use LDAP groups to manage access to a project, [add the LDAP-synchronized group as a member](../members/index.md#add-groups-to-a-project) + to the project. + ## Disable CVE identifier request in issues **(FREE SAAS)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41203) in GitLab 13.4, only for public projects on GitLab.com. diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index ea937ed9244..2d47fa4cdf1 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -44,7 +44,21 @@ For users without permission to view the project's code, the landing page shows: To access a project from the GitLab UI using the project ID, visit the `/projects/:id` URL in your browser or other tool accessing the project. -## Explore topics +## Organizing projects with topics + +Topics are labels that you can assign to projects to help you organize and find them. +A topic is typically a short name that describes the content or purpose of a project. +You can assign a topic to several projects. + +For example, you can create and assign the topics `python` and `hackathon` to all projects that use Python and are intended for Hackathon contributions. + +Topics assigned to a project are listed in the **Project overview**, below the project name and activity information. + +Only users with access to the project can see the topics assigned to that project, +but everyone (including unauthenticated users) can see the topics available on the GitLab instance. +Do not include sensitive information in the name of a topic. + +### Explore topics To explore project topics: @@ -53,15 +67,57 @@ To explore project topics: 1. On the left sidebar, select **Topics**. 1. To view projects associated with a topic, select a topic. -The **Explore topics** page shows a list of topics, sorted by the number of associated projects. +The **Explore topics** page shows a list of projects with this topic. + +### Filter and sort topics -If you want to know when new projects are added to the topic, you can use -its RSS feed for that, which is the RSS icon on the right of the filter -bar. +You can filter the list of projects that have a certain topic by: + +- Name +- Language +- Owner +- Archive status +- Visibility + +You can sort the projects by: + +- Date created +- Date updated +- Name +- Number of stars + +### Subscribe to a topic + +If you want to know when new projects are added to a topic, you can use its RSS feed. + +You can do this either from the **Explore topics** page or a project with topics. + +To subscribe to a topic: + +- From the **Explore topics** page: + + 1. On the left sidebar, expand the top-most chevron ({**chevron-down**}). + 1. Select **Explore**. + 1. Select **Topics**. + 1. Select the topic you want to subscribe to. + 1. In the upper-right corner, select **Subscribe to the new projects feed** (**{rss}**). + +- From a project: + + 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. + 1. In the **Project overview** page, from the **Topics** list select the topic you want to subscribe to. + 1. In the upper-right corner, select **Subscribe to the new projects feed** (**{rss}**). + +The results are displayed as an RSS feed in Atom format. +The URL of the result contains a feed token and the list of projects that have the topic. You can add this URL to your feed reader. + +### Assign a topic to a project You can assign topics to a project on the [Project Settings page](settings/index.md#assign-topics-to-a-project). -If you're an instance administrator, you can administer all project topics from the +### Administer topics + +Instance administrators can administer all project topics from the [Admin Area's Topics page](../admin_area/index.md#administering-topics). ## Star a project diff --git a/lib/sidebars/organizations/menus/manage_menu.rb b/lib/sidebars/organizations/menus/manage_menu.rb new file mode 100644 index 00000000000..0df716cdd3f --- /dev/null +++ b/lib/sidebars/organizations/menus/manage_menu.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Sidebars + module Organizations + module Menus + class ManageMenu < ::Sidebars::Menu + override :title + def title + s_('Navigation|Manage') + end + + override :sprite_icon + def sprite_icon + 'users' + end + + override :pick_into_super_sidebar? + def pick_into_super_sidebar? + true + end + + override :configure_menu_items + def configure_menu_items + add_item( + ::Sidebars::MenuItem.new( + title: _('Groups and projects'), + link: groups_and_projects_organization_path(context.container), + super_sidebar_parent: ::Sidebars::Organizations::Menus::ManageMenu, + active_routes: { path: 'organizations/organizations#groups_and_projects' }, + item_id: :organization_groups_and_projects + ) + ) + end + end + end + end +end diff --git a/lib/sidebars/organizations/menus/scope_menu.rb b/lib/sidebars/organizations/menus/scope_menu.rb new file mode 100644 index 00000000000..86f0a083731 --- /dev/null +++ b/lib/sidebars/organizations/menus/scope_menu.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Sidebars + module Organizations + module Menus + class ScopeMenu < ::Sidebars::Menu + override :link + def link + organization_path(context.container) + end + + override :title + def title + context.container.name + end + + override :active_routes + def active_routes + { path: 'organizations/organizations#show' } + end + + override :render? + def render? + true + end + + override :extra_nav_link_html_options + def extra_nav_link_html_options + { + class: 'context-header' + } + end + + override :serialize_as_menu_item_args + def serialize_as_menu_item_args + super.merge({ + title: s_('Organization|Organization overview'), + sprite_icon: 'organization', + super_sidebar_parent: ::Sidebars::StaticMenu, + item_id: :organization_overview + }) + end + end + end + end +end diff --git a/lib/sidebars/organizations/panel.rb b/lib/sidebars/organizations/panel.rb new file mode 100644 index 00000000000..159ccb6cbe9 --- /dev/null +++ b/lib/sidebars/organizations/panel.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Sidebars + module Organizations + class Panel < ::Sidebars::Panel + include ::Sidebars::Concerns::SuperSidebarPanel + + override :aria_label + def aria_label + s_('Organization|Organization navigation') + end + + override :configure_menus + def configure_menus + set_scope_menu(Sidebars::Organizations::Menus::ScopeMenu.new(context)) + add_menu(Sidebars::StaticMenu.new(context)) + add_menu(Sidebars::Organizations::Menus::ManageMenu.new(context)) + end + end + end +end diff --git a/lib/sidebars/organizations/super_sidebar_panel.rb b/lib/sidebars/organizations/super_sidebar_panel.rb new file mode 100644 index 00000000000..acc3d9a0ddf --- /dev/null +++ b/lib/sidebars/organizations/super_sidebar_panel.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Sidebars + module Organizations + class SuperSidebarPanel < ::Sidebars::Organizations::Panel + include ::Sidebars::Concerns::SuperSidebarPanel + extend ::Gitlab::Utils::Override + + override :configure_menus + def configure_menus + super + old_menus = @menus + @menus = [] + + add_menu(Sidebars::StaticMenu.new(context)) + + # Pick old menus, will be obsolete once everything is in their own + # super sidebar menu + pick_from_old_menus(old_menus) + + transform_old_menus(@menus, @scope_menu, *old_menus) + end + + override :super_sidebar_context_header + def super_sidebar_context_header + { + title: context.container.name, + id: context.container.id + } + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a2b7debdce3..9b572bcf0f9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -184,6 +184,11 @@ msgid_plural "%d assigned issues" msgstr[0] "" msgstr[1] "" +msgid "%d author" +msgid_plural "%d authors" +msgstr[0] "" +msgstr[1] "" + msgid "%d changed file" msgid_plural "%d changed files" msgstr[0] "" @@ -15122,6 +15127,12 @@ msgid_plural "Dependencies|%d vulnerabilities detected" msgstr[0] "" msgstr[1] "" +msgid "Dependencies|%{locationCount} locations" +msgstr "" + +msgid "Dependencies|%{projectCount} projects" +msgstr "" + msgid "Dependencies|%{remainingLicensesCount} more" msgstr "" @@ -32062,6 +32073,12 @@ msgstr "" msgid "Organizations" msgstr "" +msgid "Organization|Organization navigation" +msgstr "" + +msgid "Organization|Organization overview" +msgstr "" + msgid "Orphaned member" msgstr "" @@ -45175,7 +45192,7 @@ msgstr "" msgid "Take a look at the documentation to discover all of GitLab’s capabilities." msgstr "" -msgid "TanukiBot|Ask GitLab Chat" +msgid "TanukiBot|Ask GitLab Duo" msgstr "" msgid "TanukiBot|Ask a question about GitLab" diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 1c7c0de09fe..56627e51b76 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -198,56 +198,6 @@ module QA destination_storage[:type] == :praefect ? verify_storage_move_to_praefect(repo_path, destination_storage[:name]) : verify_storage_move_to_gitaly(repo_path, destination_storage[:name]) end - def praefect_dataloss_information(project_id) - dataloss_info = [] - cmd = "docker exec #{@praefect} praefect -config /var/opt/gitlab/praefect/config.toml dataloss --partially-unavailable=true" - shell(cmd) { |line| dataloss_info << line.strip } - - # Expected will have a record for each repository in the storage, in the following format - # @hashed/bc/52/bc52dd634277c4a34a2d6210994a9a5e2ab6d33bb4a3a8963410e00ca6c15a02.git: - # Primary: gitaly1 - # In-Sync Storages: - # gitaly1, assigned host - # gitaly3, assigned host - # Outdated Storages: - # gitaly2 is behind by 1 change or less, assigned host - # - # Alternatively, if all repositories are in sync, a concise message is returned - # Virtual storage: default - # All repositories are fully available on all assigned storages! - - # extract the relevant project under test info if it is identified - start_index = dataloss_info.index { |line| line.include?("#{Digest::SHA256.hexdigest(project_id.to_s)}.git") } - unless start_index.nil? - dataloss_info = dataloss_info[start_index, 7] - end - - dataloss_info&.each { |info| QA::Runtime::Logger.debug(info) } - dataloss_info - end - - def wait_for_project_synced_across_all_storages(project_id) - Support::Retrier.retry_until(max_duration: 60) do - praefect_dataloss_information(project_id).include?('All repositories are fully available on all assigned storages!') - end - end - - def accept_dataloss_for_project(project_id, authoritative_storage) - repository_hash = "#{Digest::SHA256.hexdigest(project_id.to_s)}" - repository = "@hashed/#{repository_hash[0, 2]}/#{repository_hash[2, 2]}/#{repository_hash}.git" - - cmd = %{ - docker exec #{@praefect} \ - praefect \ - -config /var/opt/gitlab/praefect/config.toml \ - accept-dataloss \ - --virtual-storage=default \ - --repository=#{repository} \ - --authoritative-storage=#{authoritative_storage} - } - shell(cmd) - end - def wait_for_health_check_all_nodes gitaly_nodes.each { |node| wait_for_gitaly_health_check(node) } end @@ -310,27 +260,6 @@ module QA Support::Waiter.wait_until(sleep_interval: 1) { replication_queue_incomplete_count == 0 && replicated?(project_id) } end - def wait_for_replication_to_node(project_id, node) - Support::Waiter.wait_until(sleep_interval: 1) do - result = [] - shell sql_to_docker_exec_cmd(%{ - select * from replication_queue - where state = 'ready' - and job ->> 'change' = 'update' - and job ->> 'target_node_storage' = '#{node}' - and job ->> 'relative_path' = '#{Digest::SHA256.hexdigest(project_id.to_s)}.git'; - }) do |line| - result << line.strip - QA::Runtime::Logger.debug(line.strip) - end - # The result should look like this when all items are replicated - # id | state | created_at | updated_at | attempt | lock_id | job | meta - # ----+-------+------------+------------+---------+---------+-----+------ - # (0 rows) - result[2] == '(0 rows)' - end - end - def replication_pending? result = [] shell sql_to_docker_exec_cmd( diff --git a/qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb deleted file mode 100644 index 7b9164c1fc9..00000000000 --- a/qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Systems' do - describe 'Praefect dataloss commands', :orchestrated, :gitaly_cluster, product_group: :gitaly do - let(:praefect_manager) { Service::PraefectManager.new } - - let(:project) do - Resource::Project.fabricate! do |project| - project.name = 'gitaly_cluster-dataloss-project' - project.initialize_with_readme = true - end - end - - before do - praefect_manager.start_all_nodes - end - - it 'confirms that changes are synced across all storages', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352691' do - expect { praefect_manager.praefect_dataloss_information(project.id) } - .to(eventually_include('All repositories are fully available on all assigned storages!') - .within(max_duration: 60)) - end - - it 'identifies how many changes are not in sync across storages', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352692' do - # Ensure our test repository is replicated and in a consistent state prior to test - praefect_manager.wait_for_project_synced_across_all_storages(project.id) - - # testing for gitaly2 'out of sync' - praefect_manager.stop_node(praefect_manager.secondary_node) - - number_of_changes = 3 - 1.upto(number_of_changes) do |i| - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.branch = "newbranch-#{SecureRandom.hex(8)}" - commit.start_branch = project.default_branch - commit.commit_message = 'Add new file' - commit.add_files( - [{ - file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'new file' - }] - ) - end - end - - # testing for gitaly3 'in sync' but marked unhealthy - praefect_manager.stop_node(praefect_manager.tertiary_node) - - project_data_loss = praefect_manager.praefect_dataloss_information(project.id) - aggregate_failures "validate dataloss identified" do - expect(project_data_loss).to include('gitaly1, assigned host') - expect(project_data_loss) - .to include("gitaly2 is behind by #{number_of_changes} changes or less, assigned host, unhealthy") - expect(project_data_loss).to include('gitaly3, assigned host, unhealthy') - end - end - - it 'allows admin resolve scenario where data cannot be recovered', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352708' do - # Ensure everything is in sync before begining test - praefect_manager.wait_for_project_synced_across_all_storages(project.id) - - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.commit_message = 'accept-dataloss-1' - commit.add_files( - [{ - file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly1,gitaly2,gitaly3' - }] - ) - end - - praefect_manager.wait_for_replication_to_node(project.id, praefect_manager.primary_node) - praefect_manager.stop_node(praefect_manager.primary_node) - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.commit_message = 'accept-dataloss-2' - commit.add_files( - [{ - file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly2,gitaly3' - }] - ) - end - - praefect_manager.wait_for_replication_to_node(project.id, praefect_manager.secondary_node) - praefect_manager.stop_node(praefect_manager.secondary_node) - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.commit_message = 'accept-dataloss-3' - commit.add_files([ - { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly3' } - ]) - end - - # Confirms that they want to accept dataloss, using gitaly2 as authoritative storage to use as a base - praefect_manager.accept_dataloss_for_project(project.id, praefect_manager.secondary_node) - - # Restart nodes, and allow replication to apply dataloss changes - praefect_manager.start_all_nodes - praefect_manager.wait_for_project_synced_across_all_storages(project.id) - - # Validate that gitaly2 was accepted as the authorative storage - aggregate_failures "validate correct set of commits available" do - expect(project.commits.map { |commit| commit[:message].chomp }).to include('accept-dataloss-1') - expect(project.commits.map { |commit| commit[:message].chomp }).to include('accept-dataloss-2') - expect(project.commits.map { |commit| commit[:message].chomp }).not_to include('accept-dataloss-3') - end - end - end - end -end diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index 070b6dbec7d..d824b3b1759 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ context 'with default target branch' do before do first('.add-to-tree').click - click_link('New directory') + click_button('New directory') end it 'creates the directory in the default branch' do @@ -55,7 +55,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ end first('.add-to-tree').click - click_link('New directory') + click_button('New directory') fill_in(:dir_name, with: 'new_directory') click_button('Create directory') @@ -68,7 +68,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ context 'with a new target branch' do before do first('.add-to-tree').click - click_link('New directory') + click_button('New directory') fill_in(:dir_name, with: 'new_directory') fill_in(:branch_name, with: 'new-feature') click_button('Create directory') @@ -99,7 +99,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ find('.add-to-tree').click wait_for_requests - click_link('New directory') + click_button('New directory') fill_in(:dir_name, with: 'new_directory') fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Create directory') diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb index 29fb20841fd..ee017336acc 100644 --- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb +++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb @@ -43,8 +43,8 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : aggregate_failures 'dropdown links above the repo tree' do expect(page).to have_link('New file') - expect(page).to have_link('Upload file') - expect(page).to have_link('New directory') + expect(page).to have_button('Upload file') + expect(page).to have_button('New directory') expect(page).to have_link('New branch') expect(page).to have_link('New tag') end diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index 72e1149a01c..9b9a1a84b1d 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -1,7 +1,11 @@ -import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import waitForPromises from 'helpers/wait_for_promises'; +import { sprintf } from '~/locale'; +import { createAlert } from '~/alert'; import DiffContentComponent from 'jh_else_ce/diffs/components/diff_content.vue'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; @@ -11,19 +15,33 @@ import { EVT_EXPAND_ALL_FILES, EVT_PERF_MARK_DIFF_FILES_END, EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, + FILE_DIFF_POSITION_TYPE, } from '~/diffs/constants'; import eventHub from '~/diffs/event_hub'; -import createDiffsStore from '~/diffs/store/modules'; import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import axios from '~/lib/utils/axios_utils'; import { scrollToElement } from '~/lib/utils/common_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import createNotesStore from '~/notes/stores/modules'; +import diffsModule from '~/diffs/store/modules'; +import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n'; +import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import { getDiffFileMock } from '../mock_data/diff_file'; import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable'; +import diffsMockData from '../mock_data/merge_request_diffs'; jest.mock('~/lib/utils/common_utils'); +jest.mock('~/alert'); +jest.mock('~/notes/mixins/diff_line_note_form', () => ({ + methods: { + addToReview: jest.fn(), + }, +})); + +Vue.use(Vuex); + +const saveDiffDiscussionMock = jest.fn(); function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) { const file = store.state.diffs.diffFiles[index]; @@ -70,18 +88,29 @@ function markFileToBeRendered(store, index = 0) { } function createComponent({ file, first = false, last = false, options = {}, props = {} }) { - Vue.use(Vuex); + const diffs = diffsModule(); + diffs.actions = { + ...diffs.actions, + saveDiffDiscussion: saveDiffDiscussionMock, + }; + + diffs.getters = { + ...diffs.getters, + diffCompareDropdownTargetVersions: () => [], + diffCompareDropdownSourceVersions: () => [], + }; const store = new Vuex.Store({ ...createNotesStore(), - modules: { - diffs: createDiffsStore(), - }, + modules: { diffs }, }); - store.state.diffs.diffFiles = [file]; + store.state.diffs = { + mergeRequestDiff: diffsMockData[0], + diffFiles: [file], + }; - const wrapper = shallowMount(DiffFileComponent, { + const wrapper = shallowMountExtended(DiffFileComponent, { store, propsData: { file, @@ -101,9 +130,10 @@ function createComponent({ file, first = false, last = false, options = {}, prop } const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent); -const findDiffContentArea = (wrapper) => wrapper.find('[data-testid="content-area"]'); -const findLoader = (wrapper) => wrapper.find('[data-testid="loader-icon"]'); -const findToggleButton = (wrapper) => wrapper.find('[data-testid="expand-button"]'); +const findDiffContentArea = (wrapper) => wrapper.findByTestId('content-area'); +const findLoader = (wrapper) => wrapper.findByTestId('loader-icon'); +const findToggleButton = (wrapper) => wrapper.findByTestId('expand-button'); +const findNoteForm = (wrapper) => wrapper.findByTestId('file-note-form'); const toggleFile = (wrapper) => findDiffHeader(wrapper).vm.$emit('toggleFile'); const getReadableFile = () => getDiffFileMock(); @@ -118,6 +148,12 @@ const makeFileManuallyCollapsed = (store, index = 0) => const changeViewerType = (store, newType, index = 0) => changeViewer(store, index, { name: diffViewerModes[newType] }); +const triggerSaveNote = (wrapper, note, parent, error) => + findNoteForm(wrapper).vm.$emit('handleFormUpdate', note, parent, error); + +const triggerSaveDraftNote = (wrapper, note, parent, error) => + findNoteForm(wrapper).vm.$emit('handleFormUpdateAddToReview', note, false, parent, error); + describe('DiffFile', () => { let wrapper; let store; @@ -502,7 +538,7 @@ describe('DiffFile', () => { await nextTick(); - const button = wrapper.find('[data-testid="blob-button"]'); + const button = wrapper.findByTestId('blob-button'); expect(wrapper.text()).toContain('Changes are too large to be shown.'); expect(button.html()).toContain('View file @'); @@ -538,7 +574,7 @@ describe('DiffFile', () => { ({ wrapper, store } = createComponent({ file })); - expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(false); + expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(false); }); it('renders conflict alert when conflict_type is present', () => { @@ -550,7 +586,7 @@ describe('DiffFile', () => { ({ wrapper, store } = createComponent({ file })); - expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(true); + expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(true); }); }); @@ -574,7 +610,7 @@ describe('DiffFile', () => { file, })); - expect(wrapper.find('[data-testid="file-discussions"]').exists()).toEqual(exists); + expect(wrapper.findByTestId('file-discussions').exists()).toEqual(exists); }, ); @@ -594,7 +630,7 @@ describe('DiffFile', () => { file, })); - expect(wrapper.find('[data-testid="file-note-form"]').exists()).toEqual(exists); + expect(findNoteForm(wrapper).exists()).toEqual(exists); }, ); @@ -612,7 +648,97 @@ describe('DiffFile', () => { file, })); - expect(wrapper.find('[data-testid="diff-file-discussions"]').exists()).toEqual(exists); + expect(wrapper.findByTestId('diff-file-discussions').exists()).toEqual(exists); + }); + + describe('when note-form emits `handleFormUpdate`', () => { + const file = { + ...getReadableFile(), + hasCommentForm: true, + }; + + const note = {}; + const parentElement = null; + const errorCallback = jest.fn(); + + beforeEach(() => { + ({ wrapper, store } = createComponent({ + file, + options: { provide: { glFeatures: { commentOnFiles: true } } }, + })); + }); + + it('calls saveDiffDiscussionMock', () => { + triggerSaveNote(wrapper, note, parentElement, errorCallback); + + expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), { + note, + formData: { + noteableData: expect.any(Object), + diffFile: file, + positionType: FILE_DIFF_POSITION_TYPE, + noteableType: store.getters.noteableType, + }, + }); + }); + + describe('when saveDiffDiscussionMock throws an error', () => { + describe.each` + scenario | serverError | message + ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED} + ${'without server error'} | ${{}} | ${SOMETHING_WENT_WRONG} + `('$scenario', ({ serverError, message }) => { + beforeEach(async () => { + saveDiffDiscussionMock.mockRejectedValue({ response: serverError }); + + triggerSaveNote(wrapper, note, parentElement, errorCallback); + + await waitForPromises(); + }); + + it(`renders ${serverError ? 'server' : 'generic'} error message`, () => { + expect(createAlert).toHaveBeenCalledWith({ + message: sprintf(message, { reason: serverError?.data?.errors }), + parent: parentElement, + }); + }); + + it('calls errorCallback', () => { + expect(errorCallback).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('when note-form emits `handleFormUpdateAddToReview`', () => { + const file = { + ...getReadableFile(), + hasCommentForm: true, + }; + + const note = {}; + const parentElement = null; + const errorCallback = jest.fn(); + + beforeEach(async () => { + ({ wrapper, store } = createComponent({ + file, + options: { provide: { glFeatures: { commentOnFiles: true } } }, + })); + + triggerSaveDraftNote(wrapper, note, parentElement, errorCallback); + + await nextTick(); + }); + + it('calls addToReview mixin', () => { + expect(diffLineNoteFormMixin.methods.addToReview).toHaveBeenCalledWith( + note, + FILE_DIFF_POSITION_TYPE, + parentElement, + errorCallback, + ); + }); }); }); }); diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js index 630b8feafbc..50e3f2d0f37 100644 --- a/spec/frontend/projects/commits/components/author_select_spec.js +++ b/spec/frontend/projects/commits/components/author_select_spec.js @@ -1,12 +1,17 @@ -import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; +import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; import setWindowLocation from 'helpers/set_window_location_helper'; -import * as urlUtility from '~/lib/utils/url_utility'; import AuthorSelect from '~/projects/commits/components/author_select.vue'; import { createStore } from '~/projects/commits/store'; +import { visitUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); Vue.use(Vuex); @@ -44,6 +49,10 @@ describe('Author Select', () => { propsData: { projectCommitsEl: document.querySelector('.js-project-commits-show'), }, + stubs: { + GlCollapsibleListbox, + GlListboxItem, + }, }); }; @@ -58,11 +67,9 @@ describe('Author Select', () => { resetHTMLFixture(); }); - const findDropdownContainer = () => wrapper.findComponent({ ref: 'dropdownContainer' }); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findListboxContainer = () => wrapper.findComponent({ ref: 'listboxContainer' }); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findListboxItems = () => wrapper.findAllComponents(GlListboxItem); describe('user is searching via "filter by commit message"', () => { beforeEach(() => { @@ -70,24 +77,28 @@ describe('Author Select', () => { createComponent(); }); - it('does not disable dropdown container', () => { - expect(findDropdownContainer().attributes('disabled')).toBeUndefined(); + it('does not disable listbox container', () => { + expect(findListboxContainer().attributes('disabled')).toBeUndefined(); }); it('has correct tooltip message', () => { - expect(findDropdownContainer().attributes('title')).toBe( + expect(findListboxContainer().attributes('title')).toBe( 'Searching by both author and message is currently not supported.', ); }); - it('disables dropdown', () => { - expect(findDropdown().attributes('disabled')).toBeDefined(); + it('disables listbox', () => { + expect(findListbox().attributes('disabled')).toBeDefined(); }); }); - describe('dropdown', () => { + describe('listbox', () => { + beforeEach(() => { + store.state.commitsPath = commitsPath; + }); + it('displays correct default text', () => { - expect(findDropdown().attributes('text')).toBe('Author'); + expect(findListbox().props('toggleText')).toBe('Author'); }); it('displays the current selected author', async () => { @@ -95,81 +106,62 @@ describe('Author Select', () => { createComponent(); await nextTick(); - expect(findDropdown().attributes('text')).toBe(currentAuthor); + expect(findListbox().props('toggleText')).toBe(currentAuthor); }); it('displays correct header text', () => { - expect(findDropdownHeader().text()).toBe('Search by author'); + expect(findListbox().props('headerText')).toBe('Search by author'); }); it('does not have popover text by default', () => { expect(wrapper.attributes('title')).toBeUndefined(); }); + + it('passes selected author to redirectPath', () => { + const redirectPath = `${commitsPath}?author=${currentAuthor}`; + + findListbox().vm.$emit('select', currentAuthor); + + expect(visitUrl).toHaveBeenCalledWith(redirectPath); + }); + + it('does not pass any author to redirectPath', () => { + const redirectPath = commitsPath; + + findListbox().vm.$emit('select', ''); + + expect(visitUrl).toHaveBeenCalledWith(redirectPath); + }); }); - describe('dropdown search box', () => { + describe('listbox search box', () => { it('has correct placeholder', () => { - expect(findSearchBox().attributes('placeholder')).toBe('Search'); + expect(findListbox().props('searchPlaceholder')).toBe('Search'); }); it('fetch authors on input change', () => { const authorName = 'lorem'; - findSearchBox().vm.$emit('input', authorName); + findListbox().vm.$emit('search', authorName); expect(store.actions.fetchAuthors).toHaveBeenCalledWith(expect.anything(), authorName); }); }); - describe('dropdown list', () => { + describe('listbox list', () => { beforeEach(() => { store.state.commitsAuthors = authors; - store.state.commitsPath = commitsPath; }); it('has a "Any Author" as the first list item', () => { - expect(findDropdownItems().at(0).text()).toBe('Any Author'); + expect(findListboxItems().at(0).text()).toBe('Any Author'); }); it('displays the project authors', () => { - expect(findDropdownItems()).toHaveLength(authors.length + 1); - }); - - it('has the correct props', async () => { - setWindowLocation(`?author=${currentAuthor}`); - createComponent(); - - const [{ avatar_url: avatarUrl, username }] = authors; - const result = { - avatarUrl, - secondaryText: username, - isChecked: true, - }; - - await nextTick(); - expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result)); + expect(findListboxItems()).toHaveLength(authors.length + 1); }); it("display the author's name", () => { - expect(findDropdownItems().at(1).text()).toBe(currentAuthor); - }); - - it('passes selected author to redirectPath', () => { - const redirectToUrl = `${commitsPath}?author=${currentAuthor}`; - const spy = jest.spyOn(urlUtility, 'redirectTo'); - spy.mockImplementation(() => 'mock'); - - findDropdownItems().at(1).vm.$emit('click'); - - expect(spy).toHaveBeenCalledWith(redirectToUrl); - }); - - it('does not pass any author to redirectPath', () => { - const redirectToUrl = commitsPath; - const spy = jest.spyOn(urlUtility, 'redirectTo'); - spy.mockImplementation(); - - findDropdownItems().at(0).vm.$emit('click'); - expect(spy).toHaveBeenCalledWith(redirectToUrl); + expect(findListboxItems().at(1).text()).toContain(currentAuthor); }); }); }); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index f4baa817d32..46a7f2ee1bb 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -1,6 +1,6 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlDropdown } from '@gitlab/ui'; +import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; @@ -11,6 +11,7 @@ import permissionsQuery from 'shared_queries/repository/permissions.query.graphq import projectPathQuery from '~/repository/queries/project_path.query.graphql'; import createApolloProvider from 'helpers/mock_apollo_helper'; +import { __ } from '~/locale'; const defaultMockRoute = { name: 'blobPath', @@ -61,6 +62,7 @@ describe('Repository breadcrumbs component', () => { }, stubs: { RouterLink: RouterLinkStub, + GlDisclosureDropdown, }, mocks: { $route: { @@ -71,7 +73,8 @@ describe('Repository breadcrumbs component', () => { }); }; - const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal); const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub); @@ -146,7 +149,11 @@ describe('Repository breadcrumbs component', () => { `( 'does render add to tree dropdown $isRendered when route is $routeName', ({ routeName, isRendered }) => { - factory('app/assets/javascripts.js', { canCollaborate: true }, { name: routeName }); + factory( + 'app/assets/javascripts.js', + { canCollaborate: true, canEditTree: true }, + { name: routeName }, + ); expect(findDropdown().exists()).toBe(isRendered); }, ); @@ -156,7 +163,7 @@ describe('Repository breadcrumbs component', () => { createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }), ); - factory('/', { canCollaborate: true }); + factory('/', { canCollaborate: true, canEditTree: true }); await nextTick(); expect(findDropdown().exists()).toBe(true); @@ -193,4 +200,32 @@ describe('Repository breadcrumbs component', () => { expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir'); }); }); + + describe('"this repository" dropdown group', () => { + it('renders when user has pushCode permissions', async () => { + permissionsQuerySpy.mockResolvedValue( + createPermissionsQueryResponse({ + pushCode: true, + }), + ); + + factory('/', { canCollaborate: true }); + await waitForPromises(); + + expect(findDropdownGroup().props('group').name).toBe(__('This repository')); + }); + + it('does not render when user does not have pushCode permissions', async () => { + permissionsQuerySpy.mockResolvedValue( + createPermissionsQueryResponse({ + pushCode: false, + }), + ); + + factory('/', { canCollaborate: true }); + await waitForPromises(); + + expect(findDropdownGroup().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index 6af1172e4d8..c92f8a68678 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -104,7 +104,7 @@ describe('HelpCenter component', () => { createWrapper({ ...sidebarData, show_tanuki_bot: true }); }); - it('shows Ask GitLab Chat with the help items', () => { + it('shows Ask GitLab Duo with the help items', () => { expect(findDropdownGroup(0).props('group').items).toEqual([ expect.objectContaining({ icon: 'tanuki-ai', @@ -115,9 +115,9 @@ describe('HelpCenter component', () => { ]); }); - describe('when Ask GitLab Chat button is clicked', () => { + describe('when Ask GitLab Duo button is clicked', () => { beforeEach(() => { - findButton('Ask GitLab Chat').click(); + findButton('Ask GitLab Duo').click(); }); it('sets helpCenterState.showTanukiBotChatDrawer to true', () => { diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js new file mode 100644 index 00000000000..2179b2e489e --- /dev/null +++ b/spec/frontend/tracking/internal_events_spec.js @@ -0,0 +1,63 @@ +import API from '~/api'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import InternalEvents from '~/tracking/internal_events'; +import { GITLAB_INTERNAL_EVENT_CATEGORY, SERVICE_PING_SCHEMA } from '~/tracking/constants'; + +jest.mock('~/api', () => ({ + trackRedisHllUserEvent: jest.fn(), +})); + +describe('InternalEvents', () => { + describe('track_event', () => { + it('track_event calls trackRedisHllUserEvent with correct arguments', () => { + const event = 'TestEvent'; + + InternalEvents.track_event(event); + + expect(API.trackRedisHllUserEvent).toHaveBeenCalledTimes(1); + expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event); + }); + + it('track_event calls tracking.event functions with correct arguments', () => { + const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn); + + const event = 'TestEvent'; + + InternalEvents.track_event(event); + + expect(trackingSpy).toHaveBeenCalledTimes(1); + expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, { + context: { + schema: SERVICE_PING_SCHEMA, + data: { + event_name: event, + data_source: 'redis_hll', + }, + }, + }); + }); + }); + + describe('mixin', () => { + let wrapper; + + beforeEach(() => { + const Component = { + render() {}, + mixins: [InternalEvents.mixin()], + }; + wrapper = shallowMountExtended(Component); + }); + + it('this.track_event function calls InternalEvent`s track function with an event', () => { + const event = 'TestEvent'; + const trackEventSpy = jest.spyOn(InternalEvents, 'track_event'); + + wrapper.vm.track_event(event); + + expect(trackEventSpy).toHaveBeenCalledTimes(1); + expect(trackEventSpy).toHaveBeenCalledWith(event); + }); + }); +}); diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index fa259f244bd..8d8bbcd2737 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -483,6 +483,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do let(:user) { build(:user) } let(:group) { build(:group) } let(:project) { build(:project) } + let(:organization) { build(:organization) } before do allow(helper).to receive(:project_sidebar_context_data).and_return( @@ -517,6 +518,12 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do expect(helper.super_sidebar_nav_panel(nav: 'admin')).to be_a(Sidebars::Admin::Panel) end + it 'returns Organization Panel for organization nav' do + expect( + helper.super_sidebar_nav_panel(nav: 'organization', organization: organization) + ).to be_a(Sidebars::Organizations::SuperSidebarPanel) + end + it 'returns "Your Work" Panel for your_work nav', :use_clean_rails_memory_store_caching do expect(helper.super_sidebar_nav_panel(nav: 'your_work', user: user)).to be_a(Sidebars::YourWork::Panel) end diff --git a/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb new file mode 100644 index 00000000000..08fc352a6cd --- /dev/null +++ b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Organizations::Menus::ManageMenu, feature_category: :navigation do + let_it_be(:organization) { build(:organization) } + let_it_be(:user) { build(:user) } + let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) } + + let(:items) { subject.instance_variable_get(:@items) } + + subject { described_class.new(context) } + + it 'has title and sprite_icon' do + expect(subject.title).to eq(s_("Navigation|Manage")) + expect(subject.sprite_icon).to eq("users") + end + + describe 'Menu items' do + subject { described_class.new(context).renderable_items.find { |e| e.item_id == item_id } } + + describe 'Groups and projects' do + let(:item_id) { :organization_groups_and_projects } + + it { is_expected.not_to be_nil } + end + end +end diff --git a/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb b/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb new file mode 100644 index 00000000000..bc03787e95f --- /dev/null +++ b/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Organizations::Menus::ScopeMenu, feature_category: :navigation do + let_it_be(:organization) { build(:organization) } + let_it_be(:user) { build(:user) } + let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) } + + it_behaves_like 'serializable as super_sidebar_menu_args' do + let(:menu) { described_class.new(context) } + let(:extra_attrs) do + { + title: s_('Organization|Organization overview'), + sprite_icon: 'organization', + super_sidebar_parent: ::Sidebars::StaticMenu, + item_id: :organization_overview + } + end + end +end diff --git a/spec/lib/sidebars/organizations/panel_spec.rb b/spec/lib/sidebars/organizations/panel_spec.rb new file mode 100644 index 00000000000..1f0b8d72aef --- /dev/null +++ b/spec/lib/sidebars/organizations/panel_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Organizations::Panel, feature_category: :navigation do + let_it_be(:organization) { build(:organization) } + let_it_be(:user) { build(:user) } + let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) } + + subject { described_class.new(context) } + + it 'has a scope menu' do + expect(subject.scope_menu).to be_a(Sidebars::Organizations::Menus::ScopeMenu) + end + + it_behaves_like 'a panel with uniquely identifiable menu items' +end diff --git a/spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb b/spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb new file mode 100644 index 00000000000..99b33a5edf8 --- /dev/null +++ b/spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Organizations::SuperSidebarPanel, feature_category: :navigation do + let_it_be(:organization) { build(:organization) } + let_it_be(:user) { build(:user) } + let_it_be(:context) do + Sidebars::Context.new( + current_user: user, + container: organization + ) + end + + subject { described_class.new(context) } + + it 'implements #super_sidebar_context_header' do + expect(subject.super_sidebar_context_header).to eq( + { + title: organization.name, + id: organization.id + }) + end + + describe '#renderable_menus' do + let(:category_menu) do + [ + Sidebars::StaticMenu, + Sidebars::Organizations::Menus::ManageMenu + ] + end + + it "is exposed as a renderable menu" do + expect(subject.instance_variable_get(:@menus).map(&:class)).to eq(category_menu) + end + end + + it_behaves_like 'a panel with uniquely identifiable menu items' +end diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb index a51a5751831..bd54b50de99 100644 --- a/spec/requests/organizations/organizations_controller_spec.rb +++ b/spec/requests/organizations/organizations_controller_spec.rb @@ -5,9 +5,7 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationsController, feature_category: :cell do let_it_be(:organization) { create(:organization) } - describe 'GET #directory' do - subject(:gitlab_request) { get directory_organization_path(organization) } - + RSpec.shared_examples 'basic organization controller action' do before do sign_in(user) end @@ -42,4 +40,16 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d end end end + + describe 'GET #show' do + subject(:gitlab_request) { get organization_path(organization) } + + it_behaves_like 'basic organization controller action' + end + + describe 'GET #groups_and_projects' do + subject(:gitlab_request) { get groups_and_projects_organization_path(organization) } + + it_behaves_like 'basic organization controller action' + end end diff --git a/spec/routing/organizations/organizations_controller_routing_spec.rb b/spec/routing/organizations/organizations_controller_routing_spec.rb index 5b6124300ba..2b43f6d3afa 100644 --- a/spec/routing/organizations/organizations_controller_routing_spec.rb +++ b/spec/routing/organizations/organizations_controller_routing_spec.rb @@ -5,8 +5,13 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationsController, :routing, feature_category: :cell do let_it_be(:organization) { build(:organization) } - it 'routes to #directory' do - expect(get("/-/organizations/#{organization.path}/directory")) - .to route_to('organizations/organizations#directory', organization_path: organization.path) + it 'routes to #show' do + expect(get("/-/organizations/#{organization.path}")) + .to route_to('organizations/organizations#show', organization_path: organization.path) + end + + it 'routes to #groups_and_projects' do + expect(get("/-/organizations/#{organization.path}/groups_and_projects")) + .to route_to('organizations/organizations#groups_and_projects', organization_path: organization.path) end end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 7737f8a73c5..806ffdad2f1 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -4,8 +4,8 @@ RSpec.shared_examples 'it uploads and commits a new text file' do |drop: false| it 'uploads and commits a new text file', :js do find('.add-to-tree').click - page.within('.dropdown-menu') do - click_link('Upload file') + page.within('.repo-breadcrumb') do + click_button('Upload file') wait_for_requests end @@ -40,8 +40,8 @@ RSpec.shared_examples 'it uploads and commits a new image file' do |drop: false| it 'uploads and commits a new image file', :js do find('.add-to-tree').click - page.within('.dropdown-menu') do - click_link('Upload file') + page.within('.repo-breadcrumb') do + click_button('Upload file') wait_for_requests end @@ -70,8 +70,8 @@ RSpec.shared_examples 'it uploads and commits a new pdf file' do |drop: false| it 'uploads and commits a new pdf file', :js do find('.add-to-tree').click - page.within('.dropdown-menu') do - click_link('Upload file') + page.within('.repo-breadcrumb') do + click_button('Upload file') wait_for_requests end @@ -111,7 +111,7 @@ RSpec.shared_examples 'it uploads and commits a new file to a forked project' do wait_for_all_requests find('.add-to-tree').click - click_link('Upload file') + click_button('Upload file') if drop find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) @@ -149,7 +149,7 @@ RSpec.shared_examples 'it uploads a file to a sub-directory' do |drop: false| end find('.add-to-tree').click - click_link('Upload file') + click_button('Upload file') if drop find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) |