diff options
64 files changed, 847 insertions, 331 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 3a22b06c72e..fcd1440841b 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,4 +1,4 @@ -import { sortBy, cloneDeep } from 'lodash'; +import { sortBy, cloneDeep, find } from 'lodash'; import { TYPENAME_BOARD, TYPENAME_ITERATION, @@ -318,6 +318,13 @@ export function getBoardQuery(boardType) { return boardQuery[boardType].query; } +export function getListByTypeId(lists, type, id) { + // type can be assignee/label/milestone/iteration + if (type && id) return find(lists, (l) => l.listType === ListType[type] && l[type]?.id === id); + + return null; +} + export default { getMilestone, formatIssue, diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue index 90f7059da86..985b9798b36 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column.vue @@ -1,4 +1,6 @@ <script> +import produce from 'immer'; +import { debounce } from 'lodash'; import { GlTooltipDirective as GlTooltip, GlButton, @@ -6,8 +8,12 @@ import { GlIcon, } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import { __ } from '~/locale'; +import { createListMutations, listsQuery, BoardType, ListType } from 'ee_else_ce/boards/constants'; +import boardLabelsQuery from '../graphql/board_labels.query.graphql'; +import { getListByTypeId } from '../boards_util'; export default { i18n: { @@ -23,60 +29,150 @@ export default { directives: { GlTooltip, }, - inject: ['scopedLabelsAvailable'], + inject: ['scopedLabelsAvailable', 'issuableType', 'fullPath', 'boardType', 'isApolloBoard'], + props: { + listQueryVariables: { + type: Object, + required: true, + }, + boardId: { + type: String, + required: true, + }, + lists: { + type: Object, + required: true, + }, + }, data() { return { selectedId: null, selectedLabel: null, selectedIdValid: true, + labelsApollo: [], + searchTerm: '', }; }, + apollo: { + labelsApollo: { + query: boardLabelsQuery, + variables() { + return { + fullPath: this.fullPath, + searchTerm: this.searchTerm, + isGroup: this.boardType === BoardType.group, + isProject: this.boardType === BoardType.project, + }; + }, + update(data) { + return data[this.boardType].labels.nodes; + }, + skip() { + return !this.isApolloBoard; + }, + }, + }, computed: { ...mapState(['labels', 'labelsLoading']), ...mapGetters(['getListByLabelId']), + labelsToUse() { + return this.isApolloBoard ? this.labelsApollo : this.labels; + }, + isLabelsLoading() { + return this.isApolloBoard ? this.$apollo.queries.labelsApollo.loading : this.labelsLoading; + }, columnForSelected() { + if (this.isApolloBoard) { + return getListByTypeId(this.lists, ListType.label, this.selectedId); + } return this.getListByLabelId(this.selectedId); }, items() { - return ( - this.labels.map((i) => ({ - ...i, - text: i.title, - value: i.id, - })) || [] - ); + return (this.labelsToUse || []).map((i) => ({ + ...i, + text: i.title, + value: i.id, + })); }, }, created() { - this.filterItems(); + if (!this.isApolloBoard) { + this.filterItems(); + } }, methods: { - ...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']), + ...mapActions(['createList', 'fetchLabels', 'highlightList']), + createListApollo({ labelId }) { + return this.$apollo.mutate({ + mutation: createListMutations[this.issuableType].mutation, + variables: { + labelId, + boardId: this.boardId, + }, + update: ( + store, + { + data: { + boardListCreate: { list }, + }, + }, + ) => { + const sourceData = store.readQuery({ + query: listsQuery[this.issuableType].query, + variables: this.listQueryVariables, + }); + const data = produce(sourceData, (draftData) => { + draftData[this.boardType].board.lists.nodes.push(list); + }); + store.writeQuery({ + query: listsQuery[this.issuableType].query, + variables: this.listQueryVariables, + data, + }); + this.$emit('highlight-list', list.id); + }, + }); + }, addList() { if (!this.selectedLabel) { this.selectedIdValid = false; return; } - this.setAddColumnFormVisibility(false); - if (this.columnForSelected) { const listId = this.columnForSelected.id; - this.highlightList(listId); + if (this.isApolloBoard) { + this.$emit('highlight-list', listId); + } else { + this.highlightList(listId); + } return; } - this.createList({ labelId: this.selectedId }); + if (this.isApolloBoard) { + this.createListApollo({ labelId: this.selectedId }); + } else { + this.createList({ labelId: this.selectedId }); + } + + this.$emit('setAddColumnFormVisibility', false); }, filterItems(searchTerm) { this.fetchLabels(searchTerm); }, + onSearch: debounce(function debouncedSearch(searchTerm) { + this.searchTerm = searchTerm; + if (!this.isApolloBoard) { + this.filterItems(searchTerm); + } + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), + setSelectedItem(selectedId) { this.selectedId = selectedId; - const label = this.labels.find(({ id }) => id === selectedId); + const label = this.labelsToUse.find(({ id }) => id === selectedId); if (!selectedId || !label) { this.selectedLabel = null; } else { @@ -95,8 +191,8 @@ export default { <template> <board-add-new-column-form :selected-id-valid="selectedIdValid" - @filter-items="filterItems" @add-list="addList" + @setAddColumnFormVisibility="$emit('setAddColumnFormVisibility', $event)" > <template #dropdown> <gl-collapsible-listbox @@ -104,11 +200,11 @@ export default { :items="items" searchable :search-placeholder="__('Search labels')" - :searching="labelsLoading" + :searching="isLabelsLoading" :selected="selectedId" :no-results-text="$options.i18n.noResults" @select="setSelectedItem" - @search="filterItems" + @search="onSearch" @hidden="onHide" > <template #toggle> diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue index 259423df07f..419d0b41d69 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlFormGroup } from '@gitlab/ui'; -import { mapActions } from 'vuex'; import { __ } from '~/locale'; export default { @@ -33,7 +32,6 @@ export default { }; }, methods: { - ...mapActions(['setAddColumnFormVisibility']), onSubmit() { this.$emit('add-list'); }, @@ -83,9 +81,11 @@ export default { @click="onSubmit" >{{ $options.i18n.add }}</gl-button > - <gl-button data-testid="cancelAddNewColumn" @click="setAddColumnFormVisibility(false)">{{ - $options.i18n.cancel - }}</gl-button> + <gl-button + data-testid="cancelAddNewColumn" + @click="$emit('setAddColumnFormVisibility', false)" + >{{ $options.i18n.cancel }}</gl-button + > </div> </div> </div> diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue index 14c84d3c4e5..d91c8ab4727 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; import { __ } from '~/locale'; import Tracking from '~/tracking'; @@ -12,16 +11,20 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [Tracking.mixin()], + props: { + isNewListShowing: { + type: Boolean, + required: true, + }, + }, computed: { - ...mapState({ isNewListShowing: ({ addColumnForm }) => addColumnForm.visible }), tooltip() { return this.isNewListShowing ? __('The list creation wizard is already open') : ''; }, }, methods: { - ...mapActions(['setAddColumnFormVisibility']), handleClick() { - this.setAddColumnFormVisibility(true); + this.$emit('setAddColumnFormVisibility', true); this.track('click_button', { label: 'create_list' }); }, }, diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue index 3a247819850..0b9243c07c5 100644 --- a/app/assets/javascripts/boards/components/board_app.vue +++ b/app/assets/javascripts/boards/components/board_app.vue @@ -35,6 +35,7 @@ export default { activeListId: '', boardId: this.initialBoardId, filterParams: { ...this.initialFilterParams }, + addColumnFormVisible: false, isShowingEpicsSwimlanes: Boolean(queryToObject(window.location.search).group_by), apolloError: null, }; @@ -79,6 +80,7 @@ export default { computed: { ...mapGetters(['isSidebarOpen']), listQueryVariables() { + if (this.filterParams.groupBy) delete this.filterParams.groupBy; return { ...(this.isIssueBoard && { isGroup: this.isGroupBoard, @@ -129,19 +131,24 @@ export default { <div class="boards-app gl-relative" :class="{ 'is-compact': isAnySidebarOpen }"> <board-top-bar :board-id="boardId" + :add-column-form-visible="addColumnFormVisible" :is-swimlanes-on="isSwimlanesOn" @switchBoard="switchBoard" @setFilters="setFilters" + @setAddColumnFormVisibility="addColumnFormVisible = $event" @toggleSwimlanes="isShowingEpicsSwimlanes = $event" /> <board-content v-if="!isApolloBoard || boardListsApollo" :board-id="boardId" + :add-column-form-visible="addColumnFormVisible" :is-swimlanes-on="isSwimlanesOn" :filter-params="filterParams" :board-lists-apollo="boardListsApollo" :apollo-error="apolloError" + :list-query-variables="listQueryVariables" @setActiveList="setActiveId" + @setAddColumnFormVisibility="addColumnFormVisible = $event" /> <board-settings-sidebar v-if="!isApolloBoard || activeList" diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index b2054d76e95..2ee0b4593d6 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -24,12 +24,20 @@ export default { type: Object, required: true, }, + highlightedListsApollo: { + type: Array, + required: false, + default: () => [], + }, }, computed: { ...mapState(['filterParams', 'highlightedLists']), ...mapGetters(['getBoardItemsByList']), + highlightedListsToUse() { + return this.isApolloBoard ? this.highlightedListsApollo : this.highlightedLists; + }, highlighted() { - return this.highlightedLists.includes(this.list.id); + return this.highlightedListsToUse.includes(this.list.id); }, listItems() { return this.isApolloBoard ? [] : this.getBoardItemsByList(this.list.id); diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 9416cbf1884..ba4e3af79b5 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -6,7 +6,7 @@ import { mapState, mapActions } from 'vuex'; import eventHub from '~/boards/eventhub'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; import { defaultSortableOptions } from '~/sortable/constants'; -import { DraggableItemTypes } from 'ee_else_ce/boards/constants'; +import { DraggableItemTypes, flashAnimationDuration } from 'ee_else_ce/boards/constants'; import BoardColumn from './board_column.vue'; export default { @@ -44,16 +44,25 @@ export default { required: false, default: null, }, + listQueryVariables: { + type: Object, + required: true, + }, + addColumnFormVisible: { + type: Boolean, + required: true, + }, }, data() { return { boardHeight: null, + highlightedLists: [], }; }, computed: { - ...mapState(['boardLists', 'error', 'addColumnForm']), - addColumnFormVisible() { - return this.addColumnForm?.visible; + ...mapState(['boardLists', 'error']), + boardListsById() { + return this.isApolloBoard ? this.boardListsApollo : this.boardLists; }, boardListsToUse() { const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists; @@ -101,6 +110,13 @@ export default { refetchLists() { this.$apollo.queries.boardListsApollo.refetch(); }, + highlightList(listId) { + this.highlightedLists.push(listId); + + setTimeout(() => { + this.highlightedLists = this.highlightedLists.filter((id) => id !== listId); + }, flashAnimationDuration); + }, }, }; </script> @@ -129,13 +145,22 @@ export default { :board-id="boardId" :list="list" :filters="filterParams" + :highlighted-lists-apollo="highlightedLists" :data-draggable-item-type="$options.draggableItemTypes.list" :class="{ 'gl-display-none! gl-sm-display-inline-block!': addColumnFormVisible }" @setActiveList="$emit('setActiveList', $event)" /> <transition name="slide" @after-enter="afterFormEnters"> - <board-add-new-column v-if="addColumnFormVisible" class="gl-xs-w-full!" /> + <board-add-new-column + v-if="addColumnFormVisible" + class="gl-xs-w-full!" + :board-id="boardId" + :list-query-variables="listQueryVariables" + :lists="boardListsById" + @setAddColumnFormVisibility="$emit('setAddColumnFormVisibility', $event)" + @highlight-list="highlightList" + /> </transition> </component> @@ -146,8 +171,20 @@ export default { :lists="boardListsToUse" :can-admin-list="canAdminList" :filters="filterParams" + :highlighted-lists="highlightedLists" @setActiveList="$emit('setActiveList', $event)" - /> + > + <board-add-new-column + v-if="addColumnFormVisible" + class="gl-sticky gl-top-5" + :filter-params="filterParams" + :list-query-variables="listQueryVariables" + :board-id="boardId" + :lists="boardListsById" + @setAddColumnFormVisibility="$emit('setAddColumnFormVisibility', $event)" + @highlight-list="highlightList" + /> + </epics-swimlanes> <board-content-sidebar v-if="isIssueBoard" data-testid="issue-boards-sidebar" /> diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 23e0f2510a7..0f43aae3936 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -114,10 +114,10 @@ export default { showScopedLabels(label) { return this.scopedLabelsAvailable && isScopedLabel(label); }, - async deleteBoardList() { + deleteBoardList() { this.track('click_button', { label: 'remove_list' }); if (this.isApolloBoard) { - await this.deleteList(this.activeListId); + this.deleteList(this.activeListId); } else { this.removeList(this.activeId); } @@ -157,7 +157,7 @@ export default { <mounting-portal mount-to="#js-right-sidebar-portal" name="board-settings-sidebar" append> <gl-drawer v-bind="$attrs" - class="js-board-settings-sidebar gl-absolute" + class="js-board-settings-sidebar gl-absolute boards-sidebar" :open="showSidebar" variant="sidebar" @close="unsetActiveListId" diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue index c186346b2ac..54d456867e5 100644 --- a/app/assets/javascripts/boards/components/board_top_bar.vue +++ b/app/assets/javascripts/boards/components/board_top_bar.vue @@ -35,6 +35,10 @@ export default { type: String, required: true, }, + addColumnFormVisible: { + type: Boolean, + required: true, + }, isSwimlanesOn: { type: Boolean, required: true, @@ -117,7 +121,11 @@ export default { @toggleSwimlanes="$emit('toggleSwimlanes', $event)" /> <config-toggle :board-has-scope="hasScope" /> - <board-add-new-column-trigger v-if="canAdminList" /> + <board-add-new-column-trigger + v-if="canAdminList" + :is-new-list-showing="addColumnFormVisible" + @setAddColumnFormVisibility="$emit('setAddColumnFormVisibility', $event)" + /> <toggle-focus /> </div> </div> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 7fe89ffbb52..ca188c741a9 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -3,6 +3,7 @@ import { TYPE_EPIC, TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/iss import { s__, __ } from '~/locale'; import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql'; import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql'; +import createBoardListMutation from './graphql/board_list_create.mutation.graphql'; import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql'; import updateBoardListMutation from './graphql/board_list_update.mutation.graphql'; @@ -71,6 +72,12 @@ export const listsQuery = { }, }; +export const createListMutations = { + [TYPE_ISSUE]: { + mutation: createBoardListMutation, + }, +}; + export const updateListQueries = { [TYPE_ISSUE]: { mutation: updateBoardListMutation, diff --git a/app/assets/javascripts/error_tracking/components/stacktrace.vue b/app/assets/javascripts/error_tracking/components/stacktrace.vue index f58d54f2933..54b9d37be73 100644 --- a/app/assets/javascripts/error_tracking/components/stacktrace.vue +++ b/app/assets/javascripts/error_tracking/components/stacktrace.vue @@ -25,7 +25,7 @@ export default { v-for="(entry, index) in entries" :key="`stacktrace-entry-${index}`" :lines="entry.context" - :file-path="entry.filename" + :file-path="entry.filename || entry.abs_path" :error-line="entry.lineNo" :error-fn="entry.function" :error-column="entry.colNo" diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue index 6ddd982ebf1..bf549063031 100644 --- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue +++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlTooltip, GlSprintf, GlIcon, GlTruncate } from '@gitlab/ui'; import SafeHtml from '~/vue_shared/directives/safe_html'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; @@ -10,6 +10,7 @@ export default { FileIcon, GlIcon, GlSprintf, + GlTruncate, }, directives: { GlTooltip, @@ -22,7 +23,8 @@ export default { }, filePath: { type: String, - required: true, + required: false, + default: '', }, errorFn: { type: String, @@ -79,26 +81,23 @@ export default { <template> <div class="file-holder"> <div ref="header" class="file-title file-title-flex-parent"> - <div class="file-header-content d-flex align-content-center"> + <div class="file-header-content d-flex align-content-center gl-flex-wrap overflow-hidden"> <div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()"> <gl-icon :name="collapseIcon" :size="16" class="gl-mr-2" /> </div> - <file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-2" /> - <strong - v-gl-tooltip - :title="filePath" - class="file-title-name d-inline-block overflow-hidden text-truncate limited-width" - data-container="body" - > - {{ filePath }} - </strong> - <clipboard-button - :title="__('Copy file path')" - :text="filePath" - category="tertiary" - size="small" - css-class="gl-mr-1" - /> + <template v-if="filePath"> + <file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-2" /> + <strong class="file-title-name d-inline-block overflow-hidden limited-width"> + <gl-truncate with-tooltip :text="filePath" position="middle" /> + </strong> + <clipboard-button + :title="__('Copy file path')" + :text="filePath" + category="tertiary" + size="small" + css-class="gl-mr-1" + /> + </template> <gl-sprintf v-if="errorFn" :message="__('%{spanStart}in%{spanEnd} %{errorFn}')"> <template #span="{ content }"> diff --git a/app/assets/javascripts/issues/show/components/fields/type.vue b/app/assets/javascripts/issues/show/components/fields/type.vue index 775f25bdbc0..576d157e0fc 100644 --- a/app/assets/javascripts/issues/show/components/fields/type.vue +++ b/app/assets/javascripts/issues/show/components/fields/type.vue @@ -71,7 +71,7 @@ export default { :label="$options.i18n.label" label-class="sr-only" label-for="issuable-type" - class="mb-2 mb-md-0" + class="gl-mb-0" > <gl-collapsible-listbox v-model="selectedIssueType" diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue index 2e99c03d250..c9e21b296e4 100644 --- a/app/assets/javascripts/issues/show/components/form.vue +++ b/app/assets/javascripts/issues/show/components/form.vue @@ -222,7 +222,7 @@ export default { <convert-description-modal v-if="issueId && glFeatures.generateDescriptionAi" - class="gl-pl-5 gl-sm-pl-0" + class="gl-pl-5 gl-md-pl-0" :resource-id="resourceId" :user-id="userId" @contentGenerated="setDescription" diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 4d9b69ddf99..f8c323fccae 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -326,7 +326,7 @@ export default { </script> <template> - <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3"> + <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-sm-gap-3"> <gl-dropdown v-if="hasMobileDropdown" class="gl-sm-display-none! w-100" diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 884cb70cb9f..0e6530bccb7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -169,7 +169,7 @@ .dropdown-menu-toggle-icon { position: absolute; - right: $gl-padding-8; + right: $gl-padding-12; color: $gray-500; } } diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss index 98fa45e0e3d..c25e15c1af9 100644 --- a/app/assets/stylesheets/page_bundles/login.scss +++ b/app/assets/stylesheets/page_bundles/login.scss @@ -103,6 +103,10 @@ .username .validation-error { color: $red-500; } + + .terms .gl-form-checkbox { + @include gl-reset-font-size; + } } } diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 09b82e36b1a..31675a58163 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -156,7 +156,7 @@ module MembershipActions [:inherited] else if Feature.enabled?(:webui_members_inherited_users, current_user) - [:inherited, :direct, :shared_from_groups] + [:inherited, :direct, :shared_from_groups, (:invited_groups if params[:project_id])].compact else [:inherited, :direct] end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 0ee08ba1820..758aa3e294e 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -51,7 +51,8 @@ module AuthHelper { saml: 'saml_login_button', openid_connect: 'oidc_login_button', - github: 'github_login_button' + github: 'github_login_button', + gitlab: 'gitlab_oauth_login_button' }[provider.to_sym] end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 5d51b2562c7..56ab09459ad 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -23,7 +23,8 @@ class Packages::Package < ApplicationRecord rubygems: 10, helm: 11, terraform_module: 12, - rpm: 13 + rpm: 13, + ml_model: 14 } enum status: { default: 0, hidden: 1, processing: 2, error: 3, pending_destruction: 4 } diff --git a/app/models/user.rb b/app/models/user.rb index 87e31f84839..2a016df7ee3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2393,11 +2393,19 @@ class User < ApplicationRecord end def authorized_groups_without_shared_membership - Group.from_union( - [ - groups.select(*Namespace.cached_column_list), - authorized_projects.joins(:namespace).select(*Namespace.cached_column_list) - ]) + if Feature.enabled?(:authorize_groups_query_without_column_cache) + Group.from_union( + [ + groups.select(Namespace.default_select_columns), + authorized_projects.joins(:namespace).select(Namespace.default_select_columns) + ]) + else + Group.from_union( + [ + groups.select(*Namespace.cached_column_list), + authorized_projects.joins(:namespace).select(*Namespace.cached_column_list) + ]) + end end def authorized_groups_with_shared_membership diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index e5883ca06f4..f0243d844d9 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -81,11 +81,17 @@ module Releases tag: tag.name, sha: tag.dereferenced_target.sha, released_at: released_at, - links_attributes: params.dig(:assets, 'links') || [], + links_attributes: links_attributes, milestones: milestones ) end + def links_attributes + (params.dig(:assets, 'links') || []).map do |link_params| + Releases::Links::Params.new(link_params).allowed_params + end + end + def create_evidence!(release, pipeline) return if release.historical_release? || release.upcoming_release? diff --git a/app/services/releases/links/base_service.rb b/app/services/releases/links/base_service.rb index 8bab258f80a..4c260e3183f 100644 --- a/app/services/releases/links/base_service.rb +++ b/app/services/releases/links/base_service.rb @@ -18,17 +18,7 @@ module Releases private def allowed_params - @allowed_params ||= params.slice(:name, :url, :link_type).tap do |hash| - hash[:filepath] = filepath if provided_filepath? - end - end - - def provided_filepath? - params.key?(:direct_asset_path) || params.key?(:filepath) - end - - def filepath - params[:direct_asset_path] || params[:filepath] + Params.new(params).allowed_params end end end diff --git a/app/services/releases/links/params.rb b/app/services/releases/links/params.rb new file mode 100644 index 00000000000..124ab333bbc --- /dev/null +++ b/app/services/releases/links/params.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Releases + module Links + class Params + def initialize(params) + @params = params.with_indifferent_access + end + + def allowed_params + @allowed_params ||= params.slice(:name, :url, :link_type).tap do |hash| + hash[:filepath] = filepath if provided_filepath? + end + end + + private + + attr_reader :params + + def provided_filepath? + params.key?(:direct_asset_path) || params.key?(:filepath) + end + + def filepath + params[:direct_asset_path] || params[:filepath] + end + end + end +end diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/shared/issue_type/_details_content.html.haml index fdbe247c6ba..40a02fddbf3 100644 --- a/app/views/shared/issue_type/_details_content.html.haml +++ b/app/views/shared/issue_type/_details_content.html.haml @@ -2,7 +2,7 @@ - api_awards_path = local_assigns.fetch(:api_awards_path, nil) .issue-details.issuable-details.js-issue-details - .detail-page-description.content-block.js-detail-page-description.gl-pt-4.gl-pb-0.gl-border-none + .detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none #js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, issuable_id: issuable.id, full_path: @project.full_path, diff --git a/config/feature_flags/development/authorize_groups_query_without_column_cache.yml b/config/feature_flags/development/authorize_groups_query_without_column_cache.yml new file mode 100644 index 00000000000..48440649d63 --- /dev/null +++ b/config/feature_flags/development/authorize_groups_query_without_column_cache.yml @@ -0,0 +1,8 @@ +--- +name: authorize_groups_query_without_column_cache +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121613 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412833 +milestone: '16.1' +type: development +group: group::tenant scale +default_enabled: false diff --git a/db/migrate/20230516110414_add_ml_model_max_file_size_to_plan_limits.rb b/db/migrate/20230516110414_add_ml_model_max_file_size_to_plan_limits.rb new file mode 100644 index 00000000000..938536dc4bc --- /dev/null +++ b/db/migrate/20230516110414_add_ml_model_max_file_size_to_plan_limits.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddMlModelMaxFileSizeToPlanLimits < Gitlab::Database::Migration[2.1] + def change + add_column(:plan_limits, :ml_model_max_file_size, :bigint, default: 10.gigabytes, null: false) + end +end diff --git a/db/post_migrate/20230515142300_add_unique_index_for_ml_model_packages.rb b/db/post_migrate/20230515142300_add_unique_index_for_ml_model_packages.rb new file mode 100644 index 00000000000..a776c227032 --- /dev/null +++ b/db/post_migrate/20230515142300_add_unique_index_for_ml_model_packages.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddUniqueIndexForMlModelPackages < Gitlab::Database::Migration[2.1] + INDEX_NAME = 'uniq_idx_packages_packages_on_project_id_name_version_ml_model' + PACKAGE_TYPE_ML_MODEL = 14 + + disable_ddl_transaction! + + def up + add_concurrent_index :packages_packages, [:project_id, :name, :version], + unique: true, + where: "package_type = #{PACKAGE_TYPE_ML_MODEL}", + name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name(:packages_packages, INDEX_NAME) + end +end diff --git a/db/schema_migrations/20230515142300 b/db/schema_migrations/20230515142300 new file mode 100644 index 00000000000..362c553959a --- /dev/null +++ b/db/schema_migrations/20230515142300 @@ -0,0 +1 @@ +365a9c05c660ba40b1b66256fa696cf8064388b4589b6d0bc507780e081526f2
\ No newline at end of file diff --git a/db/schema_migrations/20230516110414 b/db/schema_migrations/20230516110414 new file mode 100644 index 00000000000..207e7fe65aa --- /dev/null +++ b/db/schema_migrations/20230516110414 @@ -0,0 +1 @@ +8a7d9123c689553d0aed06eea5714e9abd9f52cf6bc07b7349dcc723a3d8552a
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5dfb95d10ef..440e5a3e086 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20109,7 +20109,8 @@ CREATE TABLE plan_limits ( dashboard_limit_enabled_at timestamp with time zone, web_hook_calls integer DEFAULT 0 NOT NULL, project_access_token_limit integer DEFAULT 0 NOT NULL, - google_cloud_logging_configurations integer DEFAULT 5 NOT NULL + google_cloud_logging_configurations integer DEFAULT 5 NOT NULL, + ml_model_max_file_size bigint DEFAULT '10737418240'::bigint NOT NULL ); CREATE SEQUENCE plan_limits_id_seq @@ -33197,6 +33198,8 @@ CREATE INDEX tmp_index_vulnerability_dismissal_info ON vulnerabilities USING btr CREATE INDEX tmp_index_vulnerability_overlong_title_html ON vulnerabilities USING btree (id) WHERE (length(title_html) > 800); +CREATE UNIQUE INDEX uniq_idx_packages_packages_on_project_id_name_version_ml_model ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 14); + CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name); CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name); diff --git a/doc/administration/auth/ldap/ldap_synchronization.md b/doc/administration/auth/ldap/ldap_synchronization.md index f32a4af9e27..cc788d6d4c8 100644 --- a/doc/administration/auth/ldap/ldap_synchronization.md +++ b/doc/administration/auth/ldap/ldap_synchronization.md @@ -15,7 +15,7 @@ You can change when synchronization occurs. ## User sync -> Preventing LDAP username synchronization [introduced](<https://gitlab.com/gitlab-org/gitlab/-/issues/11336>) in GitLab 15.11. +> Preventing LDAP user's profile name synchronization [introduced](<https://gitlab.com/gitlab-org/gitlab/-/issues/11336>) in GitLab 15.11. Once per day, GitLab runs a worker to check and update GitLab users against LDAP. @@ -45,9 +45,9 @@ The process also updates the following user information: - SSH public keys if `sync_ssh_keys` is set. - Kerberos identity if Kerberos is enabled. -### Synchronize LDAP username +### Synchronize LDAP user's profile name -By default, GitLab synchronizes the LDAP username field. +By default, GitLab synchronizes the LDAP user's profile name field. To prevent this synchronization, you can set `sync_name` to `false`. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 974da5560e4..a50de40069e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -25545,6 +25545,7 @@ Values for sorting package. | <a id="packagetypeenumgolang"></a>`GOLANG` | Packages from the Golang package manager. | | <a id="packagetypeenumhelm"></a>`HELM` | Packages from the Helm package manager. | | <a id="packagetypeenummaven"></a>`MAVEN` | Packages from the Maven package manager. | +| <a id="packagetypeenumml_model"></a>`ML_MODEL` | Packages from the Ml_model package manager. | | <a id="packagetypeenumnpm"></a>`NPM` | Packages from the npm package manager. | | <a id="packagetypeenumnuget"></a>`NUGET` | Packages from the Nuget package manager. | | <a id="packagetypeenumpypi"></a>`PYPI` | Packages from the PyPI package manager. | diff --git a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md index 146ad95b255..0544b331b1a 100644 --- a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md +++ b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md @@ -585,18 +585,18 @@ You can verify whether GitLab is appropriately redirecting your session to the ` The above spec is verbose, written specifically this way to ensure the idea behind the implementation is clear. We recommend following the practices detailed within our [Beginner's guide to writing end-to-end tests](beginners_guide.md). -## OpenID Connect (OIDC) tests +## Tests for GitLab as OpenID Connect (OIDC) and OAuth provider -To run the [`login_via_oidc_with_gitlab_as_idp_spec`](https://gitlab.com/gitlab-org/gitlab/-/blob/188e2c876a17a097448d7f3ed35bdf264fed0d3b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb) on your local machine: +To run the [`login_via_oauth_and_oidc_with_gitlab_as_idp_spec`](https://gitlab.com/gitlab-org/gitlab/-/blob/2e2c8bcfa4f68cd39041806af531038ce4d2ab04/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb) on your local machine: 1. Make sure your GDK is set to run on a non-localhost address such as `gdk.test:3000`. 1. Configure a [loopback interface to 172.16.123.1](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/6fe7b46403229f12ab6d903f99b024e0b82cb94a/doc/howto/local_network.md#create-loopback-interface). 1. Make sure Docker Desktop or Rancher Desktop is running. -1. Add an entry to your `/etc/hosts` file for `gitlab-oidc-consumer.bridge` pointing to `127.0.0.1`. +1. Add an entry to your `/etc/hosts` file for `gitlab-oidc-consumer.bridge` and `gitlab-oauth-consumer.bridge` pointing to `127.0.0.1`. 1. From the `qa` directory, run the following command. To set the GitLab image you want to use, update the `RELEASE` variable. For example, to use the latest EE image, set `RELEASE` to `gitlab/gitlab-ee:latest`: ```shell bundle install - RELEASE_REGISTRY_URL='registry.gitlab.com' RELEASE_REGISTRY_USERNAME='<your_gitlab_username>' RELEASE_REGISTRY_PASSWORD='<your_gitlab_personal_access_token>' RELEASE='registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:1d5a644145dfe901ea7648d825f8f9f3006d0acf' GITLAB_QA_ADMIN_ACCESS_TOKEN="<your_gdk_admin_personal_access_token>" QA_DEBUG=true CHROME_HEADLESS=false bundle exec bin/qa Test::Instance::All http://gdk.test:3000 qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb + RELEASE_REGISTRY_URL='registry.gitlab.com' RELEASE_REGISTRY_USERNAME='<your_gitlab_username>' RELEASE_REGISTRY_PASSWORD='<your_gitlab_personal_access_token>' RELEASE='registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:c0ae46db6b31ea231b2de88961cd687acf634179' GITLAB_QA_ADMIN_ACCESS_TOKEN="<your_gdk_admin_personal_access_token>" QA_DEBUG=true CHROME_HEADLESS=false bundle exec bin/qa Test::Instance::All http://gdk.test:3000 qa/specs/features/browser_ui/1_manage/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb ``` diff --git a/doc/integration/google.md b/doc/integration/google.md index d60c1b43ed6..e9c52bc3e95 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -23,10 +23,9 @@ In Google's side: 1. Refresh the page and you should see your new project in the list 1. Go to the [Google API Console](https://console.developers.google.com/apis/dashboard) 1. In the upper-left corner, select the previously created project -1. Select **Credentials** from the sidebar -1. Select **OAuth consent screen** and fill the form with the required information -1. In the **Credentials** tab, select **Create credentials > OAuth client ID** -1. Fill in the required information +1. Select **OAuth consent screen** from the sidebar and fill the form with the required information +1. Select **Credentials** from the sidebar, then select **Create credentials > OAuth client ID** +1. Fill in the required information: - **Application type** - Choose "Web Application" - **Name** - Use the default one or provide your own - **Authorized JavaScript origins** -This isn't really used by GitLab but go diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index b7d8f6aba58..35f6f07f0be 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -363,7 +363,7 @@ When you sign in, three cookies are set: - A session cookie called `_gitlab_session`. This cookie has no set expiration date. However, it expires based on its `session_expire_delay`. -- A session cookied called `about_gitlab_active_user`. +- A session cookie called `about_gitlab_active_user`. This cookie is used by the [marketing site](https://about.gitlab.com/) to determine if a user has an active GitLab session. No user information is passed to the cookie and it expires with the session. - A persistent cookie called `remember_user_token`, which is set only if you selected **Remember me** on the sign-in page. diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index e075a917fa9..241cd93f380 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -60,16 +60,6 @@ module API if stored_sha256 == expected_sha256 no_content! else - # Track sha1 conflicts. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/367356 - Gitlab::ErrorTracking.log_exception( - ArgumentError.new, - message: 'maven package file sha1 conflict', - stored_sha1: package_file.file_sha1, - received_sha256: uploaded_file.sha256, - sha256_hexdigest_of_stored_sha1: stored_sha256 - ) - conflict! end end diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index 311fcf9aba1..d0234439057 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -20,7 +20,7 @@ module API end resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do params do - requires :tag_name, type: String, desc: 'The tag associated with the release', as: :tag + requires :tag_name, type: String, desc: 'The tag associated with the release' end resource 'releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do resource :assets do @@ -56,7 +56,8 @@ module API params do requires :name, type: String, desc: 'The name of the link. Link names must be unique in the release' requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique in the release.' - optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath + optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link' + optional :filepath, type: String, desc: 'Deprecated: optional path for a direct asset link' optional :link_type, type: String, values: %w[other runbook image package], @@ -110,7 +111,8 @@ module API params do optional :name, type: String, desc: 'The name of the link' optional :url, type: String, desc: 'The URL of the link' - optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath + optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link' + optional :filepath, type: String, desc: 'Deprecated: optional path for a direct asset link' optional :link_type, type: String, values: %w[other runbook image package], @@ -164,11 +166,11 @@ module API helpers do def release - @release ||= user_project.releases.find_by_tag!(params[:tag]) + @release ||= user_project.releases.find_by_tag!(declared_params(include_parent_namespaces: true)[:tag_name]) end def link - @link ||= release.links.find(params[:link_id]) + @link ||= release.links.find(declared_params(include_parent_namespaces: true)[:link_id]) end end end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 0b31a3e0309..5d056ade3da 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -109,7 +109,7 @@ module API cache_context: -> (_) { "user:{#{current_user&.id}}" }, expires_in: 5.minutes, current_user: current_user, - include_html_description: params[:include_html_description] + include_html_description: declared_params[:include_html_description] end desc 'Get a release by a tag name' do @@ -135,7 +135,7 @@ module API not_found! unless release - present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description] + present release, with: Entities::Release, current_user: current_user, include_html_description: declared_params[:include_html_description] end desc 'Download a project release asset file' do @@ -162,8 +162,8 @@ module API not_found! unless release - link = release.links.find_by_filepath!("/#{params[:filepath]}") - + filepath = declared_params(include_missing: false)[:filepath] + link = release.links.find_by_filepath!("/#{filepath}") not_found! unless link redirect link.url @@ -196,7 +196,7 @@ module API redirect_url = api_v4_projects_releases_path(id: user_project.id, tag_name: latest_release.tag) # Include the additional suffix_path if present - redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path].present? + redirect_url += "/#{declared_params[:suffix_path]}" if declared_params[:suffix_path].present? # Include any query parameter except `order_by` since we have plans to extend it in the future. # See https://gitlab.com/gitlab-org/gitlab/-/issues/352945 for reference. @@ -238,7 +238,8 @@ module API optional :links, type: Array do requires :name, type: String, desc: 'The name of the link. Link names must be unique within the release' requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique within the release' - optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath + optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link' + optional :filepath, type: String, desc: 'Deprecated: optional path for a direct asset link' optional :link_type, type: String, desc: 'The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`' end end @@ -392,7 +393,7 @@ module API end def release - @release ||= user_project.releases.find_by_tag(params[:tag]) + @release ||= user_project.releases.find_by_tag(declared_params[:tag]) end def find_latest_release diff --git a/lib/error_tracking/stacktrace_builder.rb b/lib/error_tracking/stacktrace_builder.rb index 024587e8683..a2d7091a62a 100644 --- a/lib/error_tracking/stacktrace_builder.rb +++ b/lib/error_tracking/stacktrace_builder.rb @@ -19,6 +19,7 @@ module ErrorTracking 'lineNo' => entry['lineno'], 'context' => build_stacktrace_context(entry), 'filename' => entry['filename'], + 'abs_path' => entry['abs_path'], 'function' => entry['function'], 'colNo' => 0 # we don't support colNo yet. } diff --git a/package.json b/package.json index 6c979699811..a41671efb23 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@gitlab/cluster-client": "^1.2.0", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", - "@gitlab/svgs": "3.47.0", + "@gitlab/svgs": "3.49.0", "@gitlab/ui": "62.12.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230524134151", diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index bea01a5bbc7..f18e227424b 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -42,6 +42,7 @@ module QA element :saml_login_button element :github_login_button element :oidc_login_button + element :gitlab_oauth_login_button end view 'app/views/layouts/devise.html.haml' do @@ -189,11 +190,16 @@ module QA click_element :saml_login_button end - def sign_in_with_oidc + def sign_in_with_gitlab_oidc set_initial_password_if_present click_element :oidc_login_button end + def sign_in_with_gitlab_oauth + set_initial_password_if_present + click_element :gitlab_oauth_login_button + end + def sign_out_and_sign_in_as(user:) Menu.perform(&:sign_out_if_signed_in) has_sign_in_tab? diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb new file mode 100644 index 00000000000..943ec6681b2 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage', :skip_live_env, requires_admin: 'creates users and instance OAuth application', + product_group: :authentication_and_authorization, quarantine: { + only: { pipeline: :nightly }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408317' + } do + let!(:user) { Resource::User.fabricate_via_api! } + let(:consumer_host) { "http://#{consumer_name}.#{Runtime::Env.running_in_ci? ? 'test' : 'bridge'}" } + let(:instance_oauth_app) do + Resource::InstanceOauthApplication.fabricate! do |application| + application.redirect_uri = redirect_uri + application.scopes = scopes + end + end + + after do + instance_oauth_app.remove_via_api! + remove_gitlab_service(consumer_name) + end + + def run_gitlab_service(name:, app_id:, app_secret:) + Service::DockerRun::Gitlab.new( + image: Runtime::Env.release, + name: name, + omnibus_config: omnibus_configuration(app_id: app_id, app_secret: app_secret)).tap do |gitlab| + gitlab.login + gitlab.pull + gitlab.register! + end + end + + def remove_gitlab_service(name) + Service::DockerRun::Gitlab.new(name: name).remove! + end + + def wait_for_service(service) + Support::Waiter.wait_until(max_duration: 900, sleep_interval: 5, raise_on_failure: true) do + service.health == "healthy" + end + end + + shared_examples 'Instance OAuth Application' do |app_type, testcase| + it "creates #{app_type} application and uses it to login", testcase: testcase do + instance_oauth_app + + Page::Main::Menu.perform(&:sign_out_if_signed_in) + + app_id = instance_oauth_app.application_id + app_secret = instance_oauth_app.application_secret + + consumer_gitlab_service = run_gitlab_service(name: consumer_name, app_id: app_id, app_secret: app_secret) + + wait_for_service(consumer_gitlab_service) + + page.visit consumer_host + + expect(page.driver.current_url).to include(consumer_host) + + Page::Main::Login.perform do |login_page| + login_page.public_send("sign_in_with_gitlab_#{app_type}") + end + + expect(page.driver.current_url).to include(Runtime::Scenario.gitlab_address) + + Flow::Login.sign_in(as: user) + + expect(page.driver.current_url).to include(consumer_host) + + Page::Dashboard::Welcome.perform do |welcome| + expect(welcome).to have_welcome_title("Welcome to GitLab") + end + end + end + + describe 'OIDC' do + let(:consumer_name) { 'gitlab-oidc-consumer' } + let(:redirect_uri) { "#{consumer_host}/users/auth/openid_connect/callback" } + let(:scopes) { %w[openid profile email] } + + def omnibus_configuration(app_id:, app_secret:) + <<~OMNIBUS + gitlab_rails['initial_root_password']='5iveL\!fe'; + gitlab_rails['omniauth_enabled'] = true; + gitlab_rails['omniauth_allow_single_sign_on'] = true; + gitlab_rails['omniauth_block_auto_created_users'] = false; + gitlab_rails['omniauth_providers'] = [ + { + name: 'openid_connect', + label: 'GitLab OIDC', + args: { + name: 'openid_connect', + scope: ['openid','profile','email'], + response_type: 'code', + issuer: '#{Runtime::Scenario.gitlab_address}', + discovery: false, + uid_field: 'preferred_username', + send_scope_to_token_endpoint: 'false', + client_options: { + identifier: '#{app_id}', + secret: '#{app_secret}', + redirect_uri: '#{consumer_host}/users/auth/openid_connect/callback', + jwks_uri: '#{Runtime::Scenario.gitlab_address}/oauth/discovery/keys', + userinfo_endpoint: '#{Runtime::Scenario.gitlab_address}/oauth/userinfo', + token_endpoint: '#{Runtime::Scenario.gitlab_address}/oauth/token', + authorization_endpoint: '#{Runtime::Scenario.gitlab_address}/oauth/authorize' + } + } + } + ]; + OMNIBUS + end + + # The host GitLab instance with address Runtime::Scenario.gitlab_address is the OIDC idP - OIDC application will + # be created here. + # GitLab instance stood up in docker with address gitlab-oidc-consumer.test (or gitlab-oidc-consumer.bridge) is + # the consumer - The GitLab OIDC Login button will be displayed here. + it_behaves_like 'Instance OAuth Application', :oidc, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/405137' + end + + describe 'OAuth' do + let(:consumer_name) { 'gitlab-oauth-consumer' } + let(:redirect_uri) { "#{consumer_host}/users/auth/gitlab/callback" } + let(:scopes) { %w[read_user] } + + def omnibus_configuration(app_id:, app_secret:) + <<~OMNIBUS + gitlab_rails['initial_root_password']='5iveL\!fe'; + gitlab_rails['omniauth_enabled'] = true; + gitlab_rails['omniauth_allow_single_sign_on'] = true; + gitlab_rails['omniauth_block_auto_created_users'] = false; + gitlab_rails['omniauth_providers'] = [ + { + name: 'gitlab', + label: 'GitLab OAuth', + app_id: '#{app_id}', + app_secret: '#{app_secret}', + args: { + scope: 'read_user', + client_options: { + site: '#{Runtime::Scenario.gitlab_address}' + } + } + } + ]; + OMNIBUS + end + + # The host GitLab instance with address Runtime::Scenario.gitlab_address is the OAuth idP - OAuth application will + # be created here. + # GitLab instance stood up in docker with address gitlab-oauth-consumer.test (or gitlab-oauth-consumer.bridge) is + # the consumer - The GitLab OAuth Login button will be displayed here. + it_behaves_like 'Instance OAuth Application', :oauth, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412111' + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb deleted file mode 100644 index 1be51f40f0e..00000000000 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Manage', :skip_live_env, requires_admin: 'creates users and instance OAuth application', - product_group: :authentication_and_authorization do - let!(:user) { Resource::User.fabricate_via_api! } - let(:oidc_consumer_name) { 'gitlab-oidc-consumer' } - let(:oidc_consumer_host) { "http://#{oidc_consumer_name}.#{Runtime::Env.running_in_ci? ? 'test' : 'bridge'}" } - let(:instance_oauth_app) do - Resource::InstanceOauthApplication.fabricate! do |application| - application.redirect_uri = "#{oidc_consumer_host}/users/auth/openid_connect/callback" - application.scopes = %w[openid profile email] - end - end - - after do - instance_oauth_app.remove_via_api! - remove_gitlab_service(oidc_consumer_name) - end - - # The host GitLab instance with address Runtime::Scenario.gitlab_address is the OIDC idP - OIDC application will be - # created here. - # GitLab instance stood up in docker with address gitlab-oidc-consumer.test (or gitlab-oidc-consumer.bridge) is - # the consumer - The GitLab OIDC Login button will be displayed here. - describe 'OIDC' do - it( - 'creates GitLab OIDC application and uses it to login', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/405137', - quarantine: { - only: { pipeline: :nightly }, - type: :investigating, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408317' - } - ) do - instance_oauth_app - - Page::Main::Menu.perform(&:sign_out_if_signed_in) - - app_id = instance_oauth_app.application_id - app_secret = instance_oauth_app.application_secret - - consumer_gitlab_service = run_gitlab_service(name: oidc_consumer_name, app_id: app_id, app_secret: app_secret) - - wait_for_service(consumer_gitlab_service) - - page.visit oidc_consumer_host - - expect(page.driver.current_url).to include(oidc_consumer_host) - - Page::Main::Login.perform(&:sign_in_with_oidc) - - expect(page.driver.current_url).to include(Runtime::Scenario.gitlab_address) - - Flow::Login.sign_in(as: user) - - expect(page.driver.current_url).to include(oidc_consumer_host) - - Page::Dashboard::Welcome.perform do |welcome| - expect(welcome).to have_welcome_title("Welcome to GitLab") - end - end - - def run_gitlab_service(name:, app_id:, app_secret:) - Service::DockerRun::Gitlab.new( - image: Runtime::Env.release, - name: name, - omnibus_config: omnibus_configuration(app_id: app_id, app_secret: app_secret)).tap do |gitlab| - gitlab.login - gitlab.pull - gitlab.register! - end - end - - def remove_gitlab_service(name) - Service::DockerRun::Gitlab.new(name: name).remove! - end - - def wait_for_service(service) - Support::Waiter.wait_until(max_duration: 900, sleep_interval: 5, raise_on_failure: true) do - service.health == "healthy" - end - end - - def omnibus_configuration(app_id:, app_secret:) - <<~OMNIBUS - gitlab_rails['initial_root_password']='5iveL\!fe'; - gitlab_rails['omniauth_enabled'] = true; - gitlab_rails['omniauth_allow_single_sign_on'] = true; - gitlab_rails['omniauth_block_auto_created_users'] = false; - gitlab_rails['omniauth_providers'] = [ - { - name: 'openid_connect', - label: 'GitLab OIDC', - args: { - name: 'openid_connect', - scope: ['openid','profile','email'], - response_type: 'code', - issuer: '#{Runtime::Scenario.gitlab_address}', - discovery: false, - uid_field: 'preferred_username', - send_scope_to_token_endpoint: 'false', - client_options: { - identifier: '#{app_id}', - secret: '#{app_secret}', - redirect_uri: '#{oidc_consumer_host}/users/auth/openid_connect/callback', - jwks_uri: '#{Runtime::Scenario.gitlab_address}/oauth/discovery/keys', - userinfo_endpoint: '#{Runtime::Scenario.gitlab_address}/oauth/userinfo', - token_endpoint: '#{Runtime::Scenario.gitlab_address}/oauth/token', - authorization_endpoint: '#{Runtime::Scenario.gitlab_address}/oauth/authorize' - } - } - } - ]; - OMNIBUS - end - end - end -end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index dbea3592e24..ad49529b426 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -97,6 +97,37 @@ RSpec.describe Projects::ProjectMembersController do expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email) end end + + context 'when invited group members are present' do + let_it_be(:invited_group_member) { create(:user) } + + before do + group.add_owner(invited_group_member) + + project.invited_groups << group + project.add_maintainer(user) + + sign_in(user) + end + + context 'when webui_members_inherited_users is disabled' do + before do + stub_feature_flags(webui_members_inherited_users: false) + end + + it 'lists only direct members' do + get :index, params: { namespace_id: project.namespace, project_id: project } + + expect(assigns(:project_members).map(&:user_id)).not_to include(invited_group_member.id) + end + end + + it 'lists invited group members by default' do + get :index, params: { namespace_id: project.namespace, project_id: project } + + expect(assigns(:project_members).map(&:user_id)).to include(invited_group_member.id) + end + end end context 'invited members' do diff --git a/spec/factories/packages/packages.rb b/spec/factories/packages/packages.rb index 09f710d545d..75f540fabbe 100644 --- a/spec/factories/packages/packages.rb +++ b/spec/factories/packages/packages.rb @@ -300,5 +300,11 @@ FactoryBot.define do end end end + + factory :ml_model_package do + sequence(:name) { |n| "mlmodel-package-#{n}" } + version { '1.0.0' } + package_type { :ml_model } + end end end diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js index 4fc9a6859a6..35296f36b89 100644 --- a/spec/frontend/boards/components/board_add_new_column_form_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -29,10 +29,7 @@ describe('BoardAddNewColumnForm', () => { }, slots, store: createStore({ - actions: { - setAddColumnFormVisibility: jest.fn(), - ...actions, - }, + actions, }), }); }; @@ -48,16 +45,11 @@ describe('BoardAddNewColumnForm', () => { }); it('clicking cancel hides the form', () => { - const setAddColumnFormVisibility = jest.fn(); - mountComponent({ - actions: { - setAddColumnFormVisibility, - }, - }); + mountComponent(); cancelButton().vm.$emit('click'); - expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); + expect(wrapper.emitted('setAddColumnFormVisibility')).toEqual([[false]]); }); describe('Add list button', () => { diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js index a09c3aaa55e..8d6cc9373af 100644 --- a/spec/frontend/boards/components/board_add_new_column_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -1,18 +1,36 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import defaultState from '~/boards/stores/state'; -import { mockLabelList } from '../mock_data'; +import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; +import boardLabelsQuery from '~/boards/graphql/board_labels.query.graphql'; +import { + mockLabelList, + createBoardListResponse, + labelsQueryResponse, + boardListsQueryResponse, +} from '../mock_data'; Vue.use(Vuex); +Vue.use(VueApollo); -describe('Board card layout', () => { +describe('BoardAddNewColumn', () => { let wrapper; + const createBoardListQueryHandler = jest.fn().mockResolvedValue(createBoardListResponse); + const labelsQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse); + const mockApollo = createMockApollo([ + [boardLabelsQuery, labelsQueryHandler], + [createBoardListMutation, createBoardListQueryHandler], + ]); + const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); + const findAddNewColumnForm = () => wrapper.findComponent(BoardAddNewColumnForm); const selectLabel = (id) => { findDropdown().vm.$emit('select', id); }; @@ -33,8 +51,22 @@ describe('Board card layout', () => { labels = [], getListByLabelId = jest.fn(), actions = {}, + provide = {}, + lists = {}, } = {}) => { wrapper = shallowMountExtended(BoardAddNewColumn, { + apolloProvider: mockApollo, + propsData: { + listQueryVariables: { + isGroup: false, + isProject: true, + fullPath: 'gitlab-org/gitlab', + boardId: 'gid://gitlab/Board/1', + filters: {}, + }, + boardId: 'gid://gitlab/Board/1', + lists, + }, data() { return { selectedId, @@ -43,7 +75,6 @@ describe('Board card layout', () => { store: createStore({ actions: { fetchLabels: jest.fn(), - setAddColumnFormVisibility: jest.fn(), ...actions, }, getters: { @@ -57,6 +88,11 @@ describe('Board card layout', () => { provide: { scopedLabelsAvailable: true, isEpicBoard: false, + issuableType: 'issue', + fullPath: 'gitlab-org/gitlab', + boardType: 'project', + isApolloBoard: false, + ...provide, }, stubs: { GlCollapsibleListbox, @@ -67,6 +103,12 @@ describe('Board card layout', () => { if (selectedId) { selectLabel(selectedId); } + + // Necessary for cache update + mockApollo.clients.defaultClient.cache.readQuery = jest + .fn() + .mockReturnValue(boardListsQueryResponse.data); + mockApollo.clients.defaultClient.cache.writeQuery = jest.fn(); }; describe('Add list button', () => { @@ -85,7 +127,7 @@ describe('Board card layout', () => { }, }); - wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + findAddNewColumnForm().vm.$emit('add-list'); await nextTick(); @@ -110,7 +152,7 @@ describe('Board card layout', () => { }, }); - wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + findAddNewColumnForm().vm.$emit('add-list'); await nextTick(); @@ -118,4 +160,59 @@ describe('Board card layout', () => { expect(createList).not.toHaveBeenCalled(); }); }); + + describe('Apollo boards', () => { + describe('when list is new', () => { + beforeEach(() => { + mountComponent({ selectedId: mockLabelList.label.id, provide: { isApolloBoard: true } }); + }); + + it('fetches labels and adds list', async () => { + findDropdown().vm.$emit('show'); + + await nextTick(); + expect(labelsQueryHandler).toHaveBeenCalled(); + + selectLabel(mockLabelList.label.id); + + findAddNewColumnForm().vm.$emit('add-list'); + + await nextTick(); + + expect(wrapper.emitted('highlight-list')).toBeUndefined(); + expect(createBoardListQueryHandler).toHaveBeenCalledWith({ + labelId: mockLabelList.label.id, + boardId: 'gid://gitlab/Board/1', + }); + }); + }); + + describe('when list already exists in board', () => { + beforeEach(() => { + mountComponent({ + lists: { + [mockLabelList.id]: mockLabelList, + }, + selectedId: mockLabelList.label.id, + provide: { isApolloBoard: true }, + }); + }); + + it('highlights existing list if trying to re-add', async () => { + findDropdown().vm.$emit('show'); + + await nextTick(); + expect(labelsQueryHandler).toHaveBeenCalled(); + + selectLabel(mockLabelList.label.id); + + findAddNewColumnForm().vm.$emit('add-list'); + + await nextTick(); + + expect(wrapper.emitted('highlight-list')).toEqual([[mockLabelList.id]]); + expect(createBoardListQueryHandler).not.toHaveBeenCalledWith(); + }); + }); + }); }); diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js index d8b93e1f3b6..825cfc9453a 100644 --- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js @@ -1,5 +1,5 @@ import { GlButton } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import Vuex from 'vuex'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; @@ -13,12 +13,16 @@ describe('BoardAddNewColumnTrigger', () => { const findBoardsCreateList = () => wrapper.findByTestId('boards-create-list'); const findTooltipText = () => getBinding(findBoardsCreateList().element, 'gl-tooltip'); + const findCreateButton = () => wrapper.findComponent(GlButton); - const mountComponent = () => { + const mountComponent = ({ isNewListShowing = false } = {}) => { wrapper = mountExtended(BoardAddNewColumnTrigger, { directives: { GlTooltip: createMockDirective('gl-tooltip'), }, + propsData: { + isNewListShowing, + }, store: createStore(), }); }; @@ -35,17 +39,19 @@ describe('BoardAddNewColumnTrigger', () => { }); it('renders an enabled button', () => { - const button = wrapper.findComponent(GlButton); + expect(findCreateButton().props('disabled')).toBe(false); + }); - expect(button.props('disabled')).toBe(false); + it('shows form on click button', () => { + findCreateButton().vm.$emit('click'); + + expect(wrapper.emitted('setAddColumnFormVisibility')).toEqual([[true]]); }); }); describe('when button is disabled', () => { - it('shows the tooltip', async () => { - wrapper.findComponent(GlButton).vm.$emit('click'); - - await nextTick(); + it('shows the tooltip', () => { + mountComponent({ isNewListShowing: true }); const tooltip = findTooltipText(); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index c8b1811cdeb..23b3b13d69c 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -25,7 +25,7 @@ describe('BoardContent', () => { const defaultState = { isShowingEpicsSwimlanes: false, - boardLists: mockLists, + boardLists: mockListsById, error: undefined, issuableType: 'issue', }; @@ -57,6 +57,8 @@ describe('BoardContent', () => { filterParams: {}, isSwimlanesOn: false, boardListsApollo: mockListsById, + listQueryVariables: {}, + addColumnFormVisible: false, ...props, }, provide: { @@ -166,7 +168,7 @@ describe('BoardContent', () => { describe('when "add column" form is visible', () => { beforeEach(() => { - createComponent({ state: { addColumnForm: { visible: true } } }); + createComponent({ props: { addColumnFormVisible: true } }); }); it('shows the "add column" form', () => { diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js index d97a1dbff47..afc7da97617 100644 --- a/spec/frontend/boards/components/board_top_bar_spec.js +++ b/spec/frontend/boards/components/board_top_bar_spec.js @@ -46,6 +46,7 @@ describe('BoardTopBar', () => { propsData: { boardId: 'gid://gitlab/Board/1', isSwimlanesOn: false, + addColumnFormVisible: false, }, provide: { swimlanesFeatureAvailable: false, diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index ec3ae27b6a1..60f906d2157 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -526,6 +526,27 @@ export const mockList = { __typename: 'BoardList', }; +export const labelsQueryResponse = { + data: { + project: { + id: 'gid://gitlab/Project/33', + labels: { + nodes: [ + { + id: 'gid://gitlab/GroupLabel/121', + title: 'To Do', + color: '#F0AD4E', + textColor: '#FFFFFF', + description: null, + descriptionHtml: null, + }, + ], + }, + __typename: 'Project', + }, + }, +}; + export const mockLabelList = { id: 'gid://gitlab/List/2', title: 'To Do', @@ -913,8 +934,8 @@ export const mockGroupLabelsResponse = { export const boardListsQueryResponse = { data: { - group: { - id: 'gid://gitlab/Group/1', + project: { + id: 'gid://gitlab/Project/1', board: { id: 'gid://gitlab/Board/1', hideBacklogList: false, @@ -922,7 +943,7 @@ export const boardListsQueryResponse = { nodes: mockLists, }, }, - __typename: 'Group', + __typename: 'Project', }, }, }; @@ -989,6 +1010,15 @@ export const updateEpicTitleResponse = { }, }; +export const createBoardListResponse = { + data: { + boardListCreate: { + list: mockLabelList, + errors: [], + }, + }, +}; + export const updateBoardListResponse = { data: { updateBoardList: { diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js index 45fc1ad04ff..9bb68c6f277 100644 --- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js +++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js @@ -1,4 +1,4 @@ -import { GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlSprintf, GlIcon, GlTruncate } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { trimText } from 'helpers/text_helper'; import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue'; @@ -44,6 +44,21 @@ describe('Stacktrace Entry', () => { expect(wrapper.findAll('.line_content.old').length).toBe(1); }); + it('should render file information if filePath exists', () => { + mountComponent({ lines }); + expect(wrapper.findComponent(FileIcon).exists()).toBe(true); + expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true); + expect(wrapper.findComponent(GlTruncate).exists()).toBe(true); + expect(wrapper.findComponent(GlTruncate).props('text')).toBe('sidekiq/util.rb'); + }); + + it('should not render file information if filePath does not exists', () => { + mountComponent({ lines, filePath: undefined }); + expect(wrapper.findComponent(FileIcon).exists()).toBe(false); + expect(wrapper.findComponent(ClipboardButton).exists()).toBe(false); + expect(wrapper.findComponent(GlTruncate).exists()).toBe(false); + }); + describe('entry caption', () => { const findFileHeaderContent = () => wrapper.find('.file-header-content').text(); diff --git a/spec/frontend/error_tracking/components/stacktrace_spec.js b/spec/frontend/error_tracking/components/stacktrace_spec.js index 29301c3e5ee..75c631617c3 100644 --- a/spec/frontend/error_tracking/components/stacktrace_spec.js +++ b/spec/frontend/error_tracking/components/stacktrace_spec.js @@ -14,6 +14,8 @@ describe('ErrorDetails', () => { [25, ' watchdog(name, \u0026block)\n'], ], lineNo: 24, + function: 'fn', + colNo: 1, }; function mountComponent(entries) { @@ -27,13 +29,33 @@ describe('ErrorDetails', () => { describe('Stacktrace', () => { it('should render single Stacktrace entry', () => { mountComponent([stackTraceEntry]); - expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(1); + const allEntries = wrapper.findAllComponents(StackTraceEntry); + expect(allEntries.length).toBe(1); + const entry = allEntries.at(0); + expect(entry.props()).toEqual({ + lines: stackTraceEntry.context, + filePath: stackTraceEntry.filename, + errorLine: stackTraceEntry.lineNo, + errorFn: stackTraceEntry.function, + errorColumn: stackTraceEntry.colNo, + expanded: true, + }); }); it('should render multiple Stacktrace entry', () => { const entriesNum = 3; mountComponent(new Array(entriesNum).fill(stackTraceEntry)); - expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(entriesNum); + const entries = wrapper.findAllComponents(StackTraceEntry); + expect(entries.length).toBe(entriesNum); + expect(entries.at(0).props('expanded')).toBe(true); + expect(entries.at(1).props('expanded')).toBe(false); + expect(entries.at(2).props('expanded')).toBe(false); + }); + + it('should use the entry abs_path if filename is missing', () => { + mountComponent([{ ...stackTraceEntry, filename: undefined, abs_path: 'abs_path' }]); + + expect(wrapper.findComponent(StackTraceEntry).props('filePath')).toBe('abs_path'); }); }); }); diff --git a/spec/graphql/types/packages/package_type_enum_spec.rb b/spec/graphql/types/packages/package_type_enum_spec.rb index fb93b1c8c8a..027ce660679 100644 --- a/spec/graphql/types/packages/package_type_enum_spec.rb +++ b/spec/graphql/types/packages/package_type_enum_spec.rb @@ -4,6 +4,6 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['PackageTypeEnum'] do it 'exposes all package types' do - expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE RPM]) + expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE RPM ML_MODEL]) end end diff --git a/spec/lib/error_tracking/stacktrace_builder_spec.rb b/spec/lib/error_tracking/stacktrace_builder_spec.rb index 57eead13fc0..b7ef2e8545a 100644 --- a/spec/lib/error_tracking/stacktrace_builder_spec.rb +++ b/spec/lib/error_tracking/stacktrace_builder_spec.rb @@ -31,7 +31,9 @@ RSpec.describe ErrorTracking::StacktraceBuilder do 'context' => expected_context, 'filename' => 'puma/thread_pool.rb', 'function' => 'block in spawn_thread', - 'colNo' => 0 + 'colNo' => 0, + 'abs_path' => + "/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/thread_pool.rb" } expect(stacktrace).to be_kind_of(Array) @@ -48,7 +50,8 @@ RSpec.describe ErrorTracking::StacktraceBuilder do 'context' => [], 'filename' => 'webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js', 'function' => 'hydrate', - 'colNo' => 0 + 'colNo' => 0, + 'abs_path' => nil } expect(stacktrace).to be_kind_of(Array) @@ -77,7 +80,9 @@ RSpec.describe ErrorTracking::StacktraceBuilder do ], 'filename' => nil, 'function' => 'main', - 'colNo' => 0 + 'colNo' => 0, + 'abs_path' => + "/Users/stanhu/github/sentry-go/example/basic/main.go" } expect(stacktrace).to be_kind_of(Array) diff --git a/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb b/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb index 8c6dcbf4b96..7c7ca8207ff 100644 --- a/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb +++ b/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb @@ -60,7 +60,7 @@ feature_category: :continuous_integration do RemoveFkToTestTmpBuildsTestTmpMetadataOnBuildsId ]) - schema_migrate_up!(only_databases: [:main]) + schema_migrate_up! fks = Gitlab::Database::PostgresForeignKey .by_referenced_table_identifier('public._test_tmp_builds') diff --git a/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb b/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb index c4f091d0d80..1834e8c6e0e 100644 --- a/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb +++ b/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' require_migration! RSpec.describe FinalizeIssuesIidScopingToNamespace, :migration, feature_category: :team_planning do - let(:batched_migrations) { table(:batched_background_migrations, database: :main) } + let(:batched_migrations) { table(:batched_background_migrations) } let!(:migration) { described_class::MIGRATION } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 29e99dbfacb..c9120262b14 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4418,7 +4418,7 @@ RSpec.describe User, feature_category: :user_profile do end end - describe '#authorized_groups' do + shared_examples '#authorized_groups shared' do let!(:user) { create(:user) } let!(:private_group) { create(:group) } let!(:child_group) { create(:group, parent: private_group) } @@ -4449,6 +4449,32 @@ RSpec.describe User, feature_category: :user_profile do end end + describe '#authorized_groups' do + context 'authorize_groups_query_without_column_cache FF enabled' do + it_behaves_like '#authorized_groups shared' do + context 'when a new column is added to namespaces table' do + before do + ApplicationRecord.connection.execute "ALTER TABLE namespaces ADD COLUMN _test_column_xyz INT NULL" + end + + # We sanity check that we don't get: + # ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: each UNION query must have the same number of columns + it 'will not raise errors' do + expect { subject.count }.not_to raise_error + end + end + end + end + + context 'authorize_groups_query_without_column_cache FF disabled' do + before do + stub_feature_flags(authorize_groups_query_without_column_cache: false) + end + + it_behaves_like '#authorized_groups shared' + end + end + describe '#membership_groups' do let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 7baec5dd551..4e746802500 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -1155,25 +1155,6 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:no_content) end - - context 'when the stored sha1 is not the same' do - let(:sent_sha1) { File.read(file_upload.path) } - let(:stored_sha1) { 'wrong sha1' } - - it 'logs an error and returns conflict' do - expect(Gitlab::ErrorTracking).to receive(:log_exception).with( - instance_of(ArgumentError), - message: 'maven package file sha1 conflict', - stored_sha1: stored_sha1, - received_sha256: Digest::SHA256.hexdigest(sent_sha1), - sha256_hexdigest_of_stored_sha1: Digest::SHA256.hexdigest(stored_sha1) - ) - - upload - - expect(response).to have_gitlab_http_status(:conflict) - end - end end context 'for md5 file' do diff --git a/spec/services/releases/links/params_spec.rb b/spec/services/releases/links/params_spec.rb new file mode 100644 index 00000000000..580bddf4fd9 --- /dev/null +++ b/spec/services/releases/links/params_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Releases::Links::Params, feature_category: :release_orchestration do + subject(:filter) { described_class.new(params) } + + let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type, unknown: '?' } } + let(:name) { 'link' } + let(:url) { 'https://example.com' } + let(:direct_asset_path) { '/path' } + let(:link_type) { 'other' } + + describe '#allowed_params' do + subject { filter.allowed_params } + + it 'returns only allowed params' do + is_expected.to eq('name' => name, 'url' => url, 'filepath' => direct_asset_path, 'link_type' => link_type) + end + + context 'when deprecated filepath is used' do + let(:params) { super().merge(direct_asset_path: nil, filepath: 'filepath') } + + it 'uses filepath value' do + is_expected.to eq('name' => name, 'url' => url, 'filepath' => 'filepath', 'link_type' => link_type) + end + end + + context 'when both direct_asset_path and filepath are provided' do + let(:params) { super().merge(filepath: 'filepath') } + + it 'uses direct_asset_path value' do + is_expected.to eq('name' => name, 'url' => url, 'filepath' => direct_asset_path, 'link_type' => link_type) + end + end + end +end diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 0084835ff8d..1b8c3388051 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -130,45 +130,19 @@ module MigrationsHelpers end end - # TODO: use Gitlab::Database::EachDatabase class (https://gitlab.com/gitlab-org/gitlab/-/issues/410154) - def migrate_databases!(only_databases: nil, version: nil) - only_databases ||= if Gitlab::Database.database_mode == Gitlab::Database::MODE_SINGLE_DATABASE - [:main] - else - %i[main ci] - end - - # unique in the context of database, host, port - configurations = Gitlab::Database.database_base_models.each_with_object({}) do |(_name, model), h| - config = model.connection_db_config - - h[config.configuration_hash.slice(:database, :host, :port)] ||= config - end - - with_reestablished_active_record_base do - configurations.each_value do |configuration| - next unless only_databases.include? configuration.name.to_sym - - ActiveRecord::Base.establish_connection(configuration) # rubocop:disable Database/EstablishConnection - - migration_context.migrate(version) # rubocop:disable Database/MultipleDatabases - end - end - end - - def schema_migrate_down!(only_databases: nil) + def schema_migrate_down! disable_migrations_output do - migrate_databases!(only_databases: only_databases, version: migration_schema_version) + migration_context.down(migration_schema_version) end reset_column_in_all_models end - def schema_migrate_up!(only_databases: nil) + def schema_migrate_up! reset_column_in_all_models disable_migrations_output do - migrate_databases!(only_databases: only_databases) + migration_context.up end reset_column_in_all_models diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index e09cca42846..7e7d8605d0b 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -214,6 +214,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| let_it_be(:package11) { create(:helm_package, project: project) } let_it_be(:package12) { create(:terraform_module_package, project: project) } let_it_be(:package13) { create(:rpm_package, project: project) } + let_it_be(:package14) { create(:ml_model_package, project: project) } Packages::Package.package_types.keys.each do |package_type| context "for package type #{package_type}" do diff --git a/yarn.lock b/yarn.lock index e2736e2e3a8..67b035463cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1110,10 +1110,10 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.2.0" -"@gitlab/svgs@3.47.0": - version "3.47.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.47.0.tgz#1a18f008aef1ecb5407688017c3bbdbc597b7ec1" - integrity sha512-xP8AyuFYRFmlxtcBYRqCnLmBgMjrACa0mUliRk/hAKUWcXoz/U4vdK69T1DhWalVi4cpUqmi4+rrIWI6fBdzew== +"@gitlab/svgs@3.49.0": + version "3.49.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.49.0.tgz#5ac366ce709c20f3a94f108556d00c3baf637e1e" + integrity sha512-2HkBtkf4X7NtTgd+1b7pnmeTRFDYoEmXduNov4yWRFB7UHy3SlGcmeH7HHWfXKwjr52iuYrmwdqSsXQUV3sbvg== "@gitlab/ui@62.12.0": version "62.12.0" |