diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-16 12:11:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-16 12:11:23 +0300 |
commit | 4e1af5260dc9187ca0637fcfcf56b450f6443192 (patch) | |
tree | d2ee12b8670f58f79595e25188ea8e41328f7c98 | |
parent | 345c883737c362ead27bc2ed6cc72e6dd365bef3 (diff) |
Add latest changes from gitlab-org/gitlab@master
46 files changed, 907 insertions, 236 deletions
diff --git a/.gitpod.yml b/.gitpod.yml index 535c60b42c8..3522ea0fca2 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,28 +1,38 @@ +# Gitpod file reference +# https://www.gitpod.io/docs/configure/workspaces/tasks + image: registry.gitlab.com/gitlab-org/gitlab-development-kit/gitpod-workspace:stable tasks: - name: GDK + # "command:" emits gitpod-start + before: | + START_UNIXTIME="$(date +%s)" + echo START_UNIXTIME="$(date +%s)" > /workspace/gitpod_start_time.sh command: | - echo START_TIME_IN_SECONDS="$(date +%s)" | tee /workspace/gitpod_start_time.sh + # send signal to other tasks that Gitpod started gp sync-done gitpod-start + echo "Waiting for other task to copy GDK.." gp sync-await gdk-copied && cd /workspace/gitlab-development-kit && gdk help - - init: | - echo "$(date) – Copying GDK" | tee -a /workspace/startup.log - cp -r $HOME/gitlab-development-kit /workspace/ + - name: GitLab + # "command:" emits gdk-copied + init: | ( set -e + echo "$(date) – Copying GDK" | tee -a /workspace/startup.log + cp -r $HOME/gitlab-development-kit /workspace/ cd /workspace/gitlab-development-kit - # Ensure GitLab directory is symlinked under the GDK + # ensure GitLab directory is symlinked under the GDK ln -nfs "$GITPOD_REPO_ROOT" /workspace/gitlab-development-kit/gitlab - mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config + mv -v /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config # ensure gdk.yml has correct instance settings - gdk config set gitlab.rails.port 443 - gdk config set gitlab.rails.https.enabled true - gdk config set webpack.host 127.0.0.1 - gdk config set webpack.static false - gdk config set webpack.live_reload false + gdk config set gitlab.rails.port 443 |& tee -a /workspace/startup.log + gdk config set gitlab.rails.https.enabled true |& tee -a /workspace/startup.log + gdk config set webpack.host 127.0.0.1 |& tee -a /workspace/startup.log + gdk config set webpack.static false |& tee -a /workspace/startup.log + gdk config set webpack.live_reload false |& tee -a /workspace/startup.log # reconfigure GDK echo "$(date) – Reconfiguring GDK" | tee -a /workspace/startup.log gdk reconfigure @@ -36,9 +46,9 @@ tasks: ) command: | ( - gp sync-await gitpod-start set -e gp sync-done gdk-copied + gp sync-await gitpod-start [[ -f /workspace/gitpod_start_time.sh ]] && source /workspace/gitpod_start_time.sh SECONDS=0 cd /workspace/gitlab-development-kit @@ -67,15 +77,14 @@ tasks: make gitlab-db-migrate fi cd /workspace/gitlab-development-kit/gitlab - # Display which branch we're on - git branch --show-current - # Install Lefthook + echo "--- on branch: $(git branch --show-current)" + echo "--- installing lefthook" bundle exec lefthook install + echo "--- resetting db/structure.sql" git checkout db/structure.sql - cd /workspace/gitlab-development-kit - # Waiting for GitLab ... + echo "--- waiting for GitLab" gp ports await 3000 - printf "Waiting for GitLab at $(gp url 3000) ..." + printf "Awaiting /-/readiness on $(gp url 3000) ..." # Check /-/readiness which returns JSON, but we're only interested in the exit code # # We use http://localhost:3000 instead of the public hostname because @@ -86,7 +95,7 @@ tasks: printf "$(date) – GitLab is up (took ~%.1f minutes)\n" "$((10*$SECONDS/60))e-1" | tee -a /workspace/startup.log gp preview $(gp url 3000) || true PREBUILD_LOG=(/workspace/.gitpod/prebuild-log-*) - [[ -f /workspace/gitpod_start_time.sh ]] && printf "Took %.1f minutes from https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitpod.yml being executed through to completion %s\n" "$((10*(($(date +%s)-${START_TIME_IN_SECONDS}))/60))e-1" "$([[ -f "$PREBUILD_LOG" ]] && echo "With Prebuilds")" + [[ -f /workspace/gitpod_start_time.sh ]] && printf "Took %.1f minutes from https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitpod.yml being executed through to completion %s\n" "$((10*(($(date +%s)-${START_UNIXTIME}))/60))e-1" "$([[ -f "$PREBUILD_LOG" ]] && echo "With Prebuilds")" ) ports: @@ -116,5 +125,5 @@ vscode: - karunamurti.haml@1.4.1 - octref.vetur@0.36.0 - dbaeumer.vscode-eslint@2.2.6 - - GitLab.gitlab-workflow@3.48.1 + - GitLab.gitlab-workflow@3.56.0 - DavidAnson.vscode-markdownlint@0.47.0 diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js index 9694bfd4e77..9b062024d03 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js @@ -4,11 +4,29 @@ import createDefaultClient from '~/lib/graphql'; Vue.use(VueApollo); +export const mergeVariables = (existing, incoming) => { + if (!incoming) return existing; + if (!existing) return incoming; + return incoming; +}; + export const apolloProvider = new VueApollo({ defaultClient: createDefaultClient( {}, { batchMax: 1, + cacheConfig: { + typePolicies: { + ContainerRepositoryDetails: { + fields: { + tags: { + keyArgs: ['id'], + merge: mergeVariables, + }, + }, + }, + }, + }, }, ), }); diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue index 8b66165a57a..b339c8c8371 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue @@ -31,6 +31,7 @@ import { import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql'; import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql'; import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql'; +import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql'; const REPOSITORY_IMPORTING_ERROR_MESSAGE = 'repository importing'; @@ -145,6 +146,13 @@ export default { query: getContainerRepositoryTagsQuery, variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE }, }, + { + query: getContainerRepositoriesDetails, + variables: { + fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, + isGroupPage: this.config.isGroupPage, + }, + }, ], }); diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js index 0204613e19c..c77a67c4287 100644 --- a/app/assets/javascripts/related_issues/index.js +++ b/app/assets/javascripts/related_issues/index.js @@ -18,7 +18,6 @@ export function initRelatedIssues(issueType = 'issue') { fullPath: el.dataset.fullPath, hasIssueWeightsFeature: parseBoolean(el.dataset.hasIssueWeightsFeature), hasIterationsFeature: parseBoolean(el.dataset.hasIterationsFeature), - projectNamespace: el.dataset.projectNamespace, }, render: (createElement) => createElement(RelatedIssuesRoot, { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue index 5c390c675fa..66e33a08a12 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue @@ -83,7 +83,7 @@ export default { <slot></slot> <div :class="{ - 'gl-flex-direction-column-reverse gl-md-flex-direction-row gl-flex-wrap gl-justify-content-end': !actions.length, + 'state-container-action-buttons gl-flex-direction-column gl-flex-wrap gl-justify-content-end': !actions.length, 'gl-md-pt-0 gl-pt-3': hasActionsSlot, }" class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue index 0b6aa104181..2db5c71be82 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue @@ -8,7 +8,7 @@ export default { canMerge: { query: readyToMergeQuery, skip() { - return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql; + return !this.mr; }, variables() { return this.mergeRequestQueryVariables; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 5bc5dd886ff..074758e33b2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -32,16 +32,6 @@ export default { </span> <template #actions> <gl-button - v-if="mr.createIssueToResolveDiscussionsPath" - :href="mr.createIssueToResolveDiscussionsPath" - class="js-create-issue gl-align-self-start gl-vertical-align-top" - size="small" - variant="confirm" - category="secondary" - > - {{ s__('mrWidget|Create issue to resolve all threads') }} - </gl-button> - <gl-button data-testid="jump-to-first" class="gl-align-self-start gl-vertical-align-top" size="small" @@ -51,6 +41,16 @@ export default { > {{ s__('mrWidget|Jump to first unresolved thread') }} </gl-button> + <gl-button + v-if="mr.createIssueToResolveDiscussionsPath" + :href="mr.createIssueToResolveDiscussionsPath" + class="js-create-issue gl-align-self-start gl-vertical-align-top" + size="small" + variant="confirm" + category="secondary" + > + {{ s__('mrWidget|Create issue to resolve all threads') }} + </gl-button> </template> </state-container> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index a29d0e38570..7e9fa24e3f5 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -499,6 +499,7 @@ export default { :work-item-type="workItemType" :fetch-by-iid="fetchByIid" :query-variables="queryVariables" + :full-path="fullPath" @error="updateError = $event" /> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js index 72a46f16bcf..0251dcc33fa 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/index.js +++ b/app/assets/javascripts/work_items/components/work_item_links/index.js @@ -17,7 +17,6 @@ export default function initWorkItemLinks() { wiHasIssueWeightsFeature, iid, wiHasIterationsFeature, - projectNamespace, } = workItemLinksRoot.dataset; // eslint-disable-next-line no-new @@ -34,7 +33,6 @@ export default function initWorkItemLinks() { fullPath: projectPath, hasIssueWeightsFeature: wiHasIssueWeightsFeature, hasIterationsFeature: wiHasIterationsFeature, - projectNamespace, }, render: (createElement) => createElement('work-item-links', { diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index c45198bd5d3..3d469b790a1 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -1,5 +1,13 @@ <script> -import { GlButton, GlIcon, GlAlert, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import { + GlButton, + GlDropdown, + GlDropdownItem, + GlIcon, + GlAlert, + GlLoadingIcon, + GlTooltipDirective, +} from '@gitlab/ui'; import { produce } from 'immer'; import { s__ } from '~/locale'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -9,7 +17,12 @@ import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_detail import { isMetaKey } from '~/lib/utils/common_utils'; import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; -import { WIDGET_ICONS, WORK_ITEM_STATUS_TEXT, WIDGET_TYPE_HIERARCHY } from '../../constants'; +import { + FORM_TYPES, + WIDGET_ICONS, + WORK_ITEM_STATUS_TEXT, + WIDGET_TYPE_HIERARCHY, +} from '../../constants'; import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; import workItemQuery from '../../graphql/work_item.query.graphql'; @@ -20,6 +33,8 @@ import WorkItemLinksForm from './work_item_links_form.vue'; export default { components: { GlButton, + GlDropdown, + GlDropdownItem, GlIcon, GlAlert, GlLoadingIcon, @@ -80,6 +95,7 @@ export default { prefetchedWorkItem: null, error: undefined, parentIssue: null, + formType: null, }; }, computed: { @@ -128,9 +144,10 @@ export default { toggle() { this.isOpen = !this.isOpen; }, - showAddForm() { + showAddForm(formType) { this.isOpen = true; this.isShownAddForm = true; + this.formType = formType; this.$nextTick(() => { this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus(); }); @@ -242,9 +259,12 @@ export default { 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.', ), addChildButtonLabel: s__('WorkItem|Add'), + addChildOptionLabel: s__('WorkItem|Existing task'), + createChildOptionLabel: s__('WorkItem|New task'), }, WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK, WORK_ITEM_STATUS_TEXT, + FORM_TYPES, }; </script> @@ -267,15 +287,26 @@ export default { {{ childrenCountLabel }} </span> </div> - <gl-button + <gl-dropdown v-if="canUpdate" - category="secondary" + right size="small" - data-testid="toggle-add-form" - @click="showAddForm" + :text="$options.i18n.addChildButtonLabel" + data-testid="toggle-form" > - {{ $options.i18n.addChildButtonLabel }} - </gl-button> + <gl-dropdown-item + data-testid="toggle-create-form" + @click="showAddForm($options.FORM_TYPES.create)" + > + {{ $options.i18n.createChildOptionLabel }} + </gl-dropdown-item> + <gl-dropdown-item + data-testid="toggle-add-form" + @click="showAddForm($options.FORM_TYPES.add)" + > + {{ $options.i18n.addChildOptionLabel }} + </gl-dropdown-item> + </gl-dropdown> <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3"> <gl-button category="tertiary" @@ -313,6 +344,7 @@ export default { :parent-confidential="confidential" :parent-iteration="issuableIteration" :parent-milestone="issuableMilestone" + :form-type="formType" @cancel="hideAddForm" @addWorkItemChild="addChild" /> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index 07a253369bb..095ea86e0d8 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -1,18 +1,21 @@ <script> -import { GlAlert, GlFormGroup, GlForm, GlFormCombobox, GlButton, GlFormInput } from '@gitlab/ui'; +import { GlAlert, GlFormGroup, GlForm, GlTokenSelector, GlButton, GlFormInput } from '@gitlab/ui'; +import { debounce } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { __, s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; +import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql'; -import { TASK_TYPE_NAME } from '../../constants'; +import { FORM_TYPES, TASK_TYPE_NAME } from '../../constants'; export default { components: { GlAlert, GlForm, - GlFormCombobox, + GlTokenSelector, GlButton, GlFormGroup, GlFormInput, @@ -45,6 +48,10 @@ export default { required: false, default: () => ({}), }, + formType: { + type: String, + required: true, + }, }, apollo: { workItemTypes: { @@ -58,13 +65,33 @@ export default { return data.workspace?.workItemTypes?.nodes; }, }, + availableWorkItems: { + query: projectWorkItemsQuery, + variables() { + return { + projectPath: this.projectPath, + searchTerm: this.search?.title || this.search, + types: ['TASK'], + in: this.search ? 'TITLE' : undefined, + }; + }, + skip() { + return !this.searchStarted; + }, + update(data) { + return data.workspace.workItems.nodes.filter((wi) => !this.childrenIds.includes(wi.id)); + }, + }, }, data() { return { + workItemTypes: [], availableWorkItems: [], search: '', + searchStarted: false, error: null, childToCreateTitle: null, + workItemsToAdd: [], }; }, computed: { @@ -92,23 +119,19 @@ export default { workItemsMvc2Enabled() { return this.glFeatures.workItemsMvc2; }, - actionsList() { - return [ - { - label: this.$options.i18n.createChildOptionLabel, - fn: () => { - this.childToCreateTitle = this.search?.title || this.search; - }, - }, - ]; + isCreateForm() { + return this.formType === FORM_TYPES.create; }, addOrCreateButtonLabel() { - return this.childToCreateTitle - ? this.$options.i18n.createChildOptionLabel - : this.$options.i18n.addTaskButtonLabel; + if (this.isCreateForm) { + return this.$options.i18n.createChildOptionLabel; + } else if (this.workItemsToAdd.length > 1) { + return this.$options.i18n.addTasksButtonLabel; + } + return this.$options.i18n.addTaskButtonLabel; }, addOrCreateMethod() { - return this.childToCreateTitle ? this.createChild : this.addChild; + return this.isCreateForm ? this.createChild : this.addChild; }, taskWorkItemType() { return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id; @@ -125,6 +148,15 @@ export default { associateMilestone() { return this.parentMilestoneId && this.workItemsMvc2Enabled; }, + isSubmitButtonDisabled() { + return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0; + }, + isLoading() { + return this.$apollo.queries.availableWorkItems.loading; + }, + }, + created() { + this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); }, methods: { getIdFromGraphQLId, @@ -132,6 +164,7 @@ export default { this.error = null; }, addChild() { + this.searchStarted = false; this.$apollo .mutate({ mutation: updateWorkItemMutation, @@ -139,7 +172,7 @@ export default { input: { id: this.issuableGid, hierarchyWidget: { - childrenIds: [this.search.id], + childrenIds: this.workItemsToAdd.map((wi) => wi.id), }, }, }, @@ -149,7 +182,7 @@ export default { [this.error] = data.workItemUpdate.errors; } else { this.unsetError(); - this.$emit('addWorkItemChild', this.search); + this.workItemsToAdd = []; } }) .catch(() => { @@ -203,10 +236,25 @@ export default { }, }); }, + setSearchKey(value) { + this.search = value; + }, + handleFocus() { + this.searchStarted = true; + }, + handleMouseOver() { + this.timeout = setTimeout(() => { + this.searchStarted = true; + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + }, + handleMouseOut() { + clearTimeout(this.timeout); + }, }, i18n: { inputLabel: __('Title'), addTaskButtonLabel: s__('WorkItem|Add task'), + addTasksButtonLabel: s__('WorkItem|Add tasks'), addChildErrorMessage: s__( 'WorkItem|Something went wrong when trying to add a child. Please try again.', ), @@ -214,7 +262,8 @@ export default { createChildErrorMessage: s__( 'WorkItem|Something went wrong when trying to create a child. Please try again.', ), - placeholder: s__('WorkItem|Add a title'), + createPlaceholder: s__('WorkItem|Add a title'), + addPlaceholder: s__('WorkItem|Search existing tasks'), fieldValidationMessage: __('Maximum of 255 characters'), }, }; @@ -223,56 +272,59 @@ export default { <template> <gl-form class="gl-bg-white gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base" - @submit.prevent="createChild" + @submit.prevent="addOrCreateMethod" > <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError"> {{ error }} </gl-alert> - <!-- Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 --> - <gl-form-combobox - v-if="false" - v-model="search" - :token-list="availableWorkItems" - match-value-to-attr="title" - class="gl-mb-4" - :label-text="$options.i18n.inputLabel" - :action-list="actionsList" - label-sr-only - autofocus - > - <template #result="{ item }"> - <div class="gl-display-flex"> - <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div> - <div>{{ item.title }}</div> - </div> - </template> - <template #action="{ item }"> - <span class="gl-text-blue-500">{{ item.label }}</span> - </template> - </gl-form-combobox> <gl-form-group + v-if="isCreateForm" :label="$options.i18n.inputLabel" :description="$options.i18n.fieldValidationMessage" > <gl-form-input ref="wiTitleInput" v-model="search" - :placeholder="$options.i18n.placeholder" + :placeholder="$options.i18n.createPlaceholder" maxlength="255" class="gl-mb-3" autofocus /> </gl-form-group> + <gl-token-selector + v-else + v-model="workItemsToAdd" + :dropdown-items="availableWorkItems" + :loading="isLoading" + :placeholder="$options.i18n.addPlaceholder" + menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!" + class="gl-mb-4" + data-testid="work-item-token-select-input" + @text-input="debouncedSearchKeyUpdate" + @focus="handleFocus" + @mouseover.native="handleMouseOver" + @mouseout.native="handleMouseOut" + > + <template #token-content="{ token }"> + {{ token.title }} + </template> + <template #dropdown-item-content="{ dropdownItem }"> + <div class="gl-display-flex"> + <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div> + <div class="gl-text-truncate">{{ dropdownItem.title }}</div> + </div> + </template> + </gl-token-selector> <gl-button category="primary" variant="confirm" size="small" type="submit" - :disabled="search.length === 0" + :disabled="isSubmitButtonDisabled" data-testid="add-child-button" class="gl-mr-2" > - {{ $options.i18n.createChildOptionLabel }} + {{ addOrCreateButtonLabel }} </gl-button> <gl-button category="secondary" size="small" @click="$emit('cancel')"> {{ s__('WorkItem|Cancel') }} diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 7737c535650..8b47c24de7d 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -102,4 +102,9 @@ export const WORK_ITEMS_TYPE_MAP = { }, }; +export const FORM_TYPES = { + create: 'create', + add: 'add', +}; + export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10; diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql index 7d38d203b84..3a23db3886a 100644 --- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql +++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql @@ -1,13 +1,16 @@ -query projectWorkItems($searchTerm: String, $projectPath: ID!, $types: [IssueType!]) { +query projectWorkItems( + $searchTerm: String + $projectPath: ID! + $types: [IssueType!] + $in: [IssuableSearchableField!] +) { workspace: project(fullPath: $projectPath) { id - workItems(search: $searchTerm, types: $types) { - edges { - node { - id - title - state - } + workItems(search: $searchTerm, types: $types, in: $in) { + nodes { + id + title + state } } } diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index f872d8c6b12..4fbcdfe2b96 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -6,13 +6,7 @@ import { createRouter } from './router'; export const initWorkItemsRoot = () => { const el = document.querySelector('#js-work-items'); - const { - fullPath, - hasIssueWeightsFeature, - issuesListPath, - projectNamespace, - hasIterationsFeature, - } = el.dataset; + const { fullPath, hasIssueWeightsFeature, issuesListPath, hasIterationsFeature } = el.dataset; return new Vue({ el, @@ -23,7 +17,6 @@ export const initWorkItemsRoot = () => { fullPath, hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), issuesListPath, - projectNamespace, hasIterationsFeature: parseBoolean(hasIterationsFeature), }, render(createElement) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d561a7d9450..c5a34ca4b31 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -46,6 +46,10 @@ } } + &.dropdown-reduced-height { + max-height: $dropdown-max-height; + } + @include media-breakpoint-down(xs) { width: 100%; } diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index 3eb897f9aff..771428b49e0 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -1210,3 +1210,15 @@ $tabs-holder-z-index: 250; .commits ol:not(:last-of-type) { margin-bottom: 0; } + +.mr-section-container { + .state-container-action-buttons { + @include media-breakpoint-up(md) { + flex-direction: row-reverse; + + .btn { + margin-left: auto; + } + } + } +} diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb index 12b48895e0c..b0dd52c9657 100644 --- a/app/models/postgresql/detached_partition.rb +++ b/app/models/postgresql/detached_partition.rb @@ -3,5 +3,9 @@ module Postgresql class DetachedPartition < ::Gitlab::Database::SharedModel scope :ready_to_drop, -> { where('drop_after < ?', Time.current) } + + def fully_qualified_table_name + "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}" + end end end diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml index 6b5b6f5df43..80f2b8b189c 100644 --- a/app/views/projects/issues/_related_issues.html.haml +++ b/app/views/projects/issues/_related_issues.html.haml @@ -5,5 +5,4 @@ has_issue_weights_feature: @project.licensed_feature_available?(:issue_weights).to_s, help_path: help_page_path('user/project/issues/related_issues'), show_categorized_issues: @project.licensed_feature_available?(:blocked_issues).to_s, - project_namespace: @project.namespace.path, has_iterations_feature: @project.licensed_feature_available?(:iterations).to_s } } diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml index a2cd967484c..72f9ec2ff16 100644 --- a/app/views/projects/issues/_work_item_links.html.haml +++ b/app/views/projects/issues/_work_item_links.html.haml @@ -1 +1 @@ -.js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_namespace: @project.namespace.path, project_path: @project.full_path, wi: work_items_index_data(@project) } } +.js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_path: @project.full_path, wi: work_items_index_data(@project) } } diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index ef3174efcc7..1246c45a529 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -14,7 +14,7 @@ - if @commits.empty? .commits-empty %h4 - There are no commits yet. + = _("There are no commits yet.") = custom_icon ('illustration_no_commits') - else .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } @@ -25,16 +25,16 @@ %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix %li.commits-tab.new-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do - Commits + = _("Commits") = gl_badge_tag @total_commit_count, { size: :sm }, { class: 'gl-tab-counter-badge' } - if @pipelines.any? %li.builds-tab = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do - Pipelines + = _("Pipelines") = gl_badge_tag @pipelines.size, { size: :sm }, { class: 'gl-tab-counter-badge' } %li.diffs-tab = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', qa_selector: 'diffs_tab'} do - Changes + = _("Changes") = gl_badge_tag @merge_request.diff_size, { size: :sm }, { class: 'gl-tab-counter-badge' } #diff-notes-app.tab-content diff --git a/data/deprecations/15-6-deprecate-runner-register-command.yml b/data/deprecations/15-6-deprecate-runner-register-command.yml index 97a3f8f4f90..b20bc4bbeec 100644 --- a/data/deprecations/15-6-deprecate-runner-register-command.yml +++ b/data/deprecations/15-6-deprecate-runner-register-command.yml @@ -8,6 +8,11 @@ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/380872 # (required) Link to the deprecation issue in GitLab body: | # (required) Do not modify this line, instead modify the lines below. - The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/) in GitLab 15.8, which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens). + The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. + GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/) in GitLab 15.8, + which introduces a new method for registering runners and eliminates the legacy + [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens). + The new method will involve passing a [runner authentication token](https://docs.gitlab.com/ee/security/token_overview.html#runner-authentication-tokens-also-called-runner-tokens) + to a new `gitlab-runner deploy` command. end_of_support_milestone: "16.0" # (optional) Use "XX.YY" format. The milestone when support for this feature will end. end_of_support_date: "2023-05-22" # (optional) The date of the milestone release when support for this feature will end. diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index 99d2bf76a49..4cf296ac1f3 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -69,12 +69,13 @@ However, GitLab sometimes changes the GraphQL API in a way that is not backward- can include removing or renaming fields, arguments, or other parts of the schema. When creating a breaking change, GitLab follows a [deprecation and removal process](#deprecation-and-removal-process). -Learn more about [breaking changes](../../development/deprecation_guidelines/index.md). +To avoid having a breaking change affect your integrations, you should +familiarize yourself with the [deprecation and removal process](#deprecation-and-removal-process) and +frequently [verify your API calls against the future breaking-change schema](#verify-against-the-future-breaking-change-schema). Fields behind a feature flag and disabled by default do not follow the deprecation and removal process, and can be removed at any time without notice. -To avoid having a breaking change affect your integrations, you should -familiarize yourself with the deprecation and removal process. +Learn more about [breaking changes](../../development/deprecation_guidelines/index.md). WARNING: GitLab makes all attempts to follow the [deprecation and removal process](#deprecation-and-removal-process). @@ -82,6 +83,18 @@ On rare occasions, GitLab might make immediate breaking changes to the GraphQL API to patch critical security or performance concerns if the deprecation process would pose significant risk. +### Verify against the future breaking-change schema + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/353642) in GitLab 15.6. + +You can make calls against the GraphQL API as if all deprecated items were already removed. +This way, you can verify API calls ahead of a [breaking-change release](#deprecation-and-removal-process) +before the items are actually removed from the schema. + +To make these calls, add a +`remove_deprecated=true` query parameter to the GitLab GraphQL API endpoint (for example, +`https://gitlab.com/api/graphql?remove_deprecated=true` for GitLab SaaS GraphQL). + ### Deprecation and removal process The deprecation and removal process for the GitLab GraphQL API aligns with the wider GitLab @@ -99,14 +112,13 @@ Items are marked as deprecated in: - The [deprecation feature removal schedule](../../update/deprecations.md), which is linked from release posts. - Introspection queries of the GraphQL API. +The deprecation message provides an alternative for the deprecated schema item, +if applicable. + NOTE: If you use the GraphQL API, we recommend you remove the deprecated schema from your GraphQL API calls as soon as possible to avoid experiencing breaking changes. -To verify your API calls against the schema without the deprecated schema items, you can add a -`?remove_deprecated=true` query parameter. You should only use this parameter for verification purposes. - -The deprecation message provides an alternative for the deprecated schema item, -if applicable. +You should [verify your API calls against the schema without the deprecated schema items](#verify-against-the-future-breaking-change-schema). #### Deprecation example diff --git a/doc/architecture/blueprints/pods/index.md b/doc/architecture/blueprints/pods/index.md index 527720d003a..3ba319d169b 100644 --- a/doc/architecture/blueprints/pods/index.md +++ b/doc/architecture/blueprints/pods/index.md @@ -319,6 +319,11 @@ The Pods architecture will impact many features requiring some of them to be rew This is the list of known affected features with the proposed solutions. - [Pods: Git Access](pods-feature-git-access.md) +- [Pods: Data Migration](pods-feature-data-migration.md) +- [Pods: Database Sequences](pods-feature-database-sequences.md) +- [Pods: GraphQL](pods-feature-graphql.md) +- [Pods: Organizations](pods-feature-organizations.md) +- [Pods: Router Endpoints Classification](pods-feature-router-endpoints-classification.md) ## Links diff --git a/doc/architecture/blueprints/pods/pods-feature-data-migration.md b/doc/architecture/blueprints/pods/pods-feature-data-migration.md new file mode 100644 index 00000000000..fad6bca45fa --- /dev/null +++ b/doc/architecture/blueprints/pods/pods-feature-data-migration.md @@ -0,0 +1,82 @@ +--- +stage: enablement +group: pods +comments: false +description: 'Pods: Data migration' +--- + +DISCLAIMER: +This page may contain information related to upcoming products, features and +functionality. It is important to note that the information presented is for +informational purposes only, so please do not rely on the information for +purchasing or planning purposes. Just like with all projects, the items +mentioned on the page are subject to change or delay, and the development, +release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + +This document is a work-in-progress and represents a very early state of the +Pods design. Significant aspects are not documented, though we expect to add +them in the future. This is one possible architecture for Pods, and we intend to +contrast this with alternatives before deciding which approach to implement. +This documentation will be kept even if we decide not to implement this so that +we can document the reasons for not choosing this approach. + +# Pods: Data migration + +It is essential for Pods architecture to provide a way to migrate data out of big Pods +into smaller ones. This describes various approaches to provide this type of split. + +## 1. Definition + +## 2. Data flow + +## 3. Proposal + +### 3.1. Split large Pods + +A single Pod can only be divided into many Pods. This is based on principle +that it is easier to create exact clone of an existing Pod in many replicas +out of which some will be made authoritative once migrated. Keeping those +replicas up-to date with Pod 0 is also much easier due to pre-existing +replication solutions that can replicate the whole systems: Geo, PostgreSQL +physical replication, etc. + +1. All data of an organization needs to not be divided across many Pods. +1. Split should be doable online. +1. New Pods cannot contain pre-existing data. +1. N Pods contain exact replica of Pod 0. +1. The data of Pod 0 is live replicated to as many Pods it needs to be split. +1. Once consensus is achieved between Pod 0 and N-Pods the organizations to be migrated away + are marked as read-only cluster-wide. +1. The `routes` is updated on for all organizations to be split to indicate an authorative + Pod holding the most recent data, like `gitlab-org` on `pod-100`. +1. The data for `gitlab-org` on Pod 0, and on other non-authoritative N-Pods are dormant + and will be removed in the future. +1. All accesses to `gitlab-org` on a given Pod are validated about `pod_id` of `routes` + to ensure that given Pod is authoritative to handle the data. + +### 3.2. Migrate organization from an existing Pod + +This is different to split, as we intend to perform logical and selective replication +of data belonging to a single organization. + +Today this type of selective replication is only implemented by Gitaly where we can migrate +Git repository from a single Gitaly node to another with minimal downtime. + +In this model we would require identifying all resources belonging to a given organization: +database rows, object storage files, Git repositories, etc. and selectively copy them over +to another (likely) existing Pod importing data into it. Ideally ensuring that we can +perform logical replication live of all changed data, but change similarly to split +which Pod is authoritative for this organization. + +1. It is hard to identify all resources belonging to organization. +1. It requires either downtime for organization or a robust system to identify + live changes made. +1. It likely will require a full database structure analysis (more robust than project import/export) + to perform selective PostgreSQL logical replication. + +## 4. Evaluation + +## 4.1. Pros + +## 4.2. Cons diff --git a/doc/architecture/blueprints/pods/pods-feature-database-sequences.md b/doc/architecture/blueprints/pods/pods-feature-database-sequences.md new file mode 100644 index 00000000000..0a8bb4d250e --- /dev/null +++ b/doc/architecture/blueprints/pods/pods-feature-database-sequences.md @@ -0,0 +1,94 @@ +--- +stage: enablement +group: pods +comments: false +description: 'Pods: Database Sequences' +--- + +DISCLAIMER: +This page may contain information related to upcoming products, features and +functionality. It is important to note that the information presented is for +informational purposes only, so please do not rely on the information for +purchasing or planning purposes. Just like with all projects, the items +mentioned on the page are subject to change or delay, and the development, +release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + +This document is a work-in-progress and represents a very early state of the +Pods design. Significant aspects are not documented, though we expect to add +them in the future. This is one possible architecture for Pods, and we intend to +contrast this with alternatives before deciding which approach to implement. +This documentation will be kept even if we decide not to implement this so that +we can document the reasons for not choosing this approach. + +# Pods: Database Sequences + +GitLab today ensures that every database row create has unique ID, allowing +to access Merge Request, CI Job or Project by a known global ID. + +Pods will use many distinct and not connected databases, each of them having +a separate IDs for most of entities. + +It might be desirable to retain globally unique IDs for all database rows +to allow migrating resources between Pods in the future. + +## 1. Definition + +## 2. Data flow + +## 3. Proposal + +This are some preliminary ideas how we can retain unique IDs across the system. + +### 3.1. UUID + +Instead of using incremental sequences use UUID (128 bit) that is stored in database. + +- This might break existing IDs and requires adding UUID column for all existing tables. +- This makes all indexes larger as it requires storing 128 bit instead of 32/64 bit in index. + +### 3.2. Use Pod index encoded in ID + +Since significant number of tables already use 64 bit ID numbers we could use MSB to encode +Pod ID effectively enabling + +- This might limit amount of Pods that can be enabled in system, as we might decide to only + allocate 1024 possible Pod numbers. +- This might make IDs to be migratable between Pods, since even if entity from Pod 1 is migrated to Pod 100 + this ID would still be unique. +- If resources are migrated the ID itself will not be enough to decode Pod number and we would need + lookup table. +- This requires updating all IDs to 32 bits. + +### 3.3. Allocate sequence ranges from central place + +Each Pod might receive its own range of the sequences as they are consumed from a centrally managed place. +Once Pod consumes all IDs assigned for a given table it would be replenished and a next range would be allocated. +Ranges would be tracked to provide a faster lookup table if a random access pattern is required. + +- This might make IDs to be migratable between Pods, since even if entity from Pod 1 is migrated to Pod 100 + this ID would still be unique. +- If resources are migrated the ID itself will not be enough to decode Pod number and we would need + much more robust lookup table as we could be breaking previously assigned sequence ranges. +- This does not require updating all IDs to 64 bits. +- This adds some performance penalty to all `INSERT` statements in Postgres or at least from Rails as we need to check for the sequence number and potentially wait for our range to be refreshed from the ID server +- The available range will need to be stored and incremented in a centralized place so that concurrent transactions cannot possibly get the same value. + +### 3.4. Define only some tables to require unique IDs + +Maybe this is acceptable only for some tables to have a globally unique IDs. It could be projects, groups +and other top-level entities. All other tables like `merge_requests` would only offer Pod-local ID, +but when referenced outside it would rather use IID (an ID that is monotonic in context of a given resource, like project). + +- This makes the ID 10000 for `merge_requests` be present on all Pods, which might be sometimes confusing + as for uniqueness of the resource. +- This might make random access by ID (if ever needed) be impossible without using composite key, like: `project_id+merge_request_id`. +- This would require us to implement a transformation/generation of new ID if we need to migrate records to another pod. This can lead to very difficult migration processes when these IDs are also used as foreign keys for other records being migrated. +- If IDs need to change when moving between pods this means that any links to records by ID would no longer work even if those links included the `project_id`. +- If we plan to allow these ids to not be unique and change the unique constraint to be based on a composite key then we'd need to update all foreign key references to be based on the composite key + +## 4. Evaluation + +## 4.1. Pros + +## 4.2. Cons diff --git a/doc/architecture/blueprints/pods/pods-feature-graphql.md b/doc/architecture/blueprints/pods/pods-feature-graphql.md new file mode 100644 index 00000000000..5f8a39c0b3f --- /dev/null +++ b/doc/architecture/blueprints/pods/pods-feature-graphql.md @@ -0,0 +1,94 @@ +--- +stage: enablement +group: pods +comments: false +description: 'Pods: GraphQL' +--- + +DISCLAIMER: +This page may contain information related to upcoming products, features and +functionality. It is important to note that the information presented is for +informational purposes only, so please do not rely on the information for +purchasing or planning purposes. Just like with all projects, the items +mentioned on the page are subject to change or delay, and the development, +release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + +This document is a work-in-progress and represents a very early state of the +Pods design. Significant aspects are not documented, though we expect to add +them in the future. This is one possible architecture for Pods, and we intend to +contrast this with alternatives before deciding which approach to implement. +This documentation will be kept even if we decide not to implement this so that +we can document the reasons for not choosing this approach. + +# Pods: GraphQL + +GitLab exensively uses GraphQL to perform efficient data query operations. +GraphQL due to it's nature is not directly routable. The way how GitLab uses +it calls the `/api/graphql` endpoint, and only query or mutation of body request +might define where the data can be accessed. + +## 1. Definition + +## 2. Data flow + +## 3. Proposal + +There are at least two main ways to implement GraphQL in Pods architecture. + +### 3.1. GraphQL routable by endpoint + +Change `/api/graphql` to `/api/organization/<organization>/graphql`. + +- This breaks all existing usages of `/api/graphql` endpoint + since the API URI is changed. + +### 3.2. GraphQL routable by body + +As part of router parse GraphQL body to find a routable entity, like `project`. + +- This still makes the GraphQL query be executed only in context of a given Pod + and not allowing the data to be merged. + +```json +# Good example +{ + project(fullPath:"gitlab-org/gitlab") { + id + description + } +} + +# Bad example, since Merge Request is not routable +{ + mergeRequest(id: 1111) { + iid + description + } +} +``` + +### 3.3. Merging GraphQL Proxy + +Implement as part of router GraphQL Proxy which can parse body +and merge results from many Pods. + +- This might make pagination hard to achieve, or we might assume that + we execute many queries of which results are merged across all Pods. + +```json +{ + project(fullPath:"gitlab-org/gitlab"){ + id, description + } + group(fullPath:"gitlab-com") { + id, description + } +} +``` + +## 4. Evaluation + +## 4.1. Pros + +## 4.2. Cons diff --git a/doc/architecture/blueprints/pods/pods-feature-organizations.md b/doc/architecture/blueprints/pods/pods-feature-organizations.md new file mode 100644 index 00000000000..a0a87458767 --- /dev/null +++ b/doc/architecture/blueprints/pods/pods-feature-organizations.md @@ -0,0 +1,58 @@ +--- +stage: enablement +group: pods +comments: false +description: 'Pods: Organizations' +--- + +DISCLAIMER: +This page may contain information related to upcoming products, features and +functionality. It is important to note that the information presented is for +informational purposes only, so please do not rely on the information for +purchasing or planning purposes. Just like with all projects, the items +mentioned on the page are subject to change or delay, and the development, +release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + +This document is a work-in-progress and represents a very early state of the +Pods design. Significant aspects are not documented, though we expect to add +them in the future. This is one possible architecture for Pods, and we intend to +contrast this with alternatives before deciding which approach to implement. +This documentation will be kept even if we decide not to implement this so that +we can document the reasons for not choosing this approach. + +# Pods: Organizations + +One of the major designs of Pods architecture is strong isolation between Groups. +Organizations as described by this blueprint provides a way to have plausible UX +for joining together many Groups that are isolated from the rest of systems. + +## 1. Definition + +Pods do require that all groups and projects of a single organization can +only be stored on a single Pod since a Pod can only access data that it holds locally +and has very limited capabilities to read information from other Pods. + +Pods with Organizations do require strong isolation between organizations. + +It will have significant implications on various user-facing features, +like Todos, dropdowns allowing to select projects, references to other issues +or projects, or any other social functions present at GitLab. Today those functions +were able to reference anything in the whole system. With the introduction of +organizations such will be forbidden. + +This problem definition aims to answer effort and implications required to add +strong isolation between organizations to the system. Including features affected +and their data processing flow. The purpose is to ensure that our solution when +implemented consistently avoids data leakage between organizations residing on +a single Pod. + +## 2. Data flow + +## 3. Proposal + +## 4. Evaluation + +## 4.1. Pros + +## 4.2. Cons diff --git a/doc/architecture/blueprints/pods/pods-feature-router-endpoints-classification.md b/doc/architecture/blueprints/pods/pods-feature-router-endpoints-classification.md new file mode 100644 index 00000000000..c672342fff9 --- /dev/null +++ b/doc/architecture/blueprints/pods/pods-feature-router-endpoints-classification.md @@ -0,0 +1,46 @@ +--- +stage: enablement +group: pods +comments: false +description: 'Pods: Router Endpoints Classification' +--- + +DISCLAIMER: +This page may contain information related to upcoming products, features and +functionality. It is important to note that the information presented is for +informational purposes only, so please do not rely on the information for +purchasing or planning purposes. Just like with all projects, the items +mentioned on the page are subject to change or delay, and the development, +release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + +This document is a work-in-progress and represents a very early state of the +Pods design. Significant aspects are not documented, though we expect to add +them in the future. This is one possible architecture for Pods, and we intend to +contrast this with alternatives before deciding which approach to implement. +This documentation will be kept even if we decide not to implement this so that +we can document the reasons for not choosing this approach. + +# Pods: Router Endpoints Classification + +Classification of all endpoints is essential to properly route request +hitting load balancer of a GitLab installation to a Pod that can serve it. + +Each Pod should be able to decode each request and classify for which Pod +it belongs to. + +GitLab currently implements houndreds of endpoints. This document tries +to describe various techniques that can be implemented to allow the Rails +to provide this information efficiently. + +## 1. Definition + +## 2. Data flow + +## 3. Proposal + +## 4. Evaluation + +## 4.1. Pros + +## 4.2. Cons diff --git a/doc/architecture/blueprints/pods/pods-feature-template.md b/doc/architecture/blueprints/pods/pods-feature-template.md index fd9813710f2..dfae21b5406 100644 --- a/doc/architecture/blueprints/pods/pods-feature-template.md +++ b/doc/architecture/blueprints/pods/pods-feature-template.md @@ -2,7 +2,7 @@ stage: enablement group: pods comments: false -description: 'Pods architecture: Problem A' +description: 'Pods: Problem A' --- This document is a work-in-progress and represents a very early state of the @@ -12,7 +12,7 @@ contrast this with alternatives before deciding which approach to implement. This documentation will be kept even if we decide not to implement this so that we can document the reasons for not choosing this approach. -# Pods: Problem A +# Pods: A > TL;DR diff --git a/doc/development/work_items.md b/doc/development/work_items.md index b71f6e86033..a417e1d1349 100644 --- a/doc/development/work_items.md +++ b/doc/development/work_items.md @@ -55,6 +55,7 @@ To avoid confusion and ensure communication is efficient, we will use the follow | legacy issue view | The existing view used to render issues and incidents | | | | issue | The existing issue model | | | | issuable | Any model currently using the issueable module (issues, epics and MRs) | _Incidents are an **issuable**_ | _Incidents are a **work item type**_ | +| widget | A UI element to present or allow interaction with specific work item data | | | Some terms have been used in the past but have since become confusing and are now discouraged. @@ -138,6 +139,20 @@ To introduce a new WIT there are two options: ### Work item type widgets +A widget is a single component that can exist on a work item. This component can be used on one or +many work item types and can be lightly customized at the point of implementation. + +A widget contains both the frontend UI (if present) and the associated logic for presenting and +managing any data used by the widget. There can be a one-to-many connection between the data model +and widgets. It means there can be multiple widgets that use or manage the same data, and they could +be present at the same time (for example, a read-only summary widget and an editable detail widget, +or two widgets showing two different filtered views of the same model). + +Widgets should be differentiated by their **purpose**. When possible, this purpose should be +abstracted to the highest reasonable level to maximize reusability. For example, the widget for +managing "tasks" was built as "child items". Rather than managing one type of child, it's abstracted +up to managing any children. + All WITs will share the same pool of predefined widgets and will be customized by which widgets are active on a specific WIT. Every attribute (column or association) will become a widget with self-encapsulated functionality regardless of the WIT it belongs to. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index e27f21d98fa..353fbf191b5 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -112,7 +112,12 @@ WARNING: This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). Review the details carefully before upgrading. -The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/) in GitLab 15.8, which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens). +The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. +GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/) in GitLab 15.8, +which introduces a new method for registering runners and eliminates the legacy +[runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens). +The new method will involve passing a [runner authentication token](https://docs.gitlab.com/ee/security/token_overview.html#runner-authentication-tokens-also-called-runner-tokens) +to a new `gitlab-runner deploy` command. </div> diff --git a/doc/user/tasks.md b/doc/user/tasks.md index 5b8e824cb4c..5c9290e57cb 100644 --- a/doc/user/tasks.md +++ b/doc/user/tasks.md @@ -50,9 +50,26 @@ Prerequisites: To create a task: 1. In the issue description, in the **Tasks** section, select **Add**. +1. Select **New task**. 1. Enter the task title. 1. Select **Create task**. +## Add existing tasks to an issue + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381868) in GitLab 15.6. + +Prerequisites: + +- You must have at least the Guest role for the project, or the project must be public. + +To add a task: + +1. In the issue description, in the **Tasks** section, select **Add**. +1. Select **Existing task**. +1. Search tasks by title. +1. Select one or multiple tasks to add to the issue. +1. Select **Add task**. + ## Edit a task Prerequisites: diff --git a/lib/api/api.rb b/lib/api/api.rb index 14ee3789366..2b69249f09c 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -219,6 +219,7 @@ module API mount ::API::Invitations mount ::API::Keys mount ::API::Lint + mount ::API::Markdown mount ::API::MergeRequestApprovals mount ::API::MergeRequestDiffs mount ::API::Metadata @@ -292,7 +293,6 @@ module API mount ::API::IssueLinks mount ::API::Issues mount ::API::Labels - mount ::API::Markdown mount ::API::MavenPackages mount ::API::Members mount ::API::MergeRequests diff --git a/lib/api/entities/markdown.rb b/lib/api/entities/markdown.rb new file mode 100644 index 00000000000..0fbaec4375e --- /dev/null +++ b/lib/api/entities/markdown.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class Markdown < Grape::Entity + expose :html, documentation: { type: 'string', example: '<p dir=\"auto\">Hello world!</p>"' } + end + end +end diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb index 1f8255fd6a4..276560f3433 100644 --- a/lib/api/markdown.rb +++ b/lib/api/markdown.rb @@ -7,13 +7,19 @@ module API feature_category :team_planning params do - requires :text, type: String, desc: "The markdown text to render" - optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown" - optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown" + requires :text, type: String, desc: "The Markdown text to render" + optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown. Default is false" + optional :project, type: String, desc: "Use project as a context when creating references using GitLab Flavored Markdown" end resource :markdown do - desc "Render markdown text" do + desc "Render an arbitrary Markdown document" do detail "This feature was introduced in GitLab 11.0." + success ::API::Entities::Markdown + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' } + ] + tags %w[markdown] end post do context = { only_path: false, current_user: current_user } @@ -29,7 +35,7 @@ module API context[:skip_project_check] = true end - { html: Banzai.render_and_post_process(params[:text], context) } + present({ html: Banzai.render_and_post_process(params[:text], context) }, with: Entities::Markdown) end end end diff --git a/lib/gitlab/database/partitioning/detached_partition_dropper.rb b/lib/gitlab/database/partitioning/detached_partition_dropper.rb index 5e32ecad4ca..58c0728b614 100644 --- a/lib/gitlab/database/partitioning/detached_partition_dropper.rb +++ b/lib/gitlab/database/partitioning/detached_partition_dropper.rb @@ -7,7 +7,7 @@ module Gitlab Gitlab::AppLogger.info(message: "Checking for previously detached partitions to drop") Postgresql::DetachedPartition.ready_to_drop.find_each do |detached_partition| - if partition_attached?(qualify_partition_name(detached_partition.table_name)) + if partition_attached?(detached_partition.fully_qualified_table_name) unmark_partition(detached_partition) else drop_partition(detached_partition) @@ -41,14 +41,14 @@ module Gitlab # Another process may have already dropped the table and deleted this entry next unless try_lock_detached_partition(detached_partition.id) - drop_detached_partition(detached_partition.table_name) + drop_detached_partition(detached_partition) detached_partition.destroy! end end def remove_foreign_keys(detached_partition) - partition_identifier = qualify_partition_name(detached_partition.table_name) + partition_identifier = detached_partition.fully_qualified_table_name # We want to load all of these into memory at once to get a consistent view to loop over, # since we'll be deleting from this list as we go @@ -65,7 +65,7 @@ module Gitlab # It is important to only drop one foreign key per transaction. # Dropping a foreign key takes an ACCESS EXCLUSIVE lock on both tables participating in the foreign key. - partition_identifier = qualify_partition_name(detached_partition.table_name) + partition_identifier = detached_partition.fully_qualified_table_name with_lock_retries do connection.transaction(requires_new: false) do next unless try_lock_detached_partition(detached_partition.id) @@ -83,16 +83,10 @@ module Gitlab end end - def drop_detached_partition(partition_name) - partition_identifier = qualify_partition_name(partition_name) + def drop_detached_partition(detached_partition) + connection.drop_table(detached_partition.fully_qualified_table_name, if_exists: true) - connection.drop_table(partition_identifier, if_exists: true) - - Gitlab::AppLogger.info(message: "Dropped previously detached partition", partition_name: partition_name) - end - - def qualify_partition_name(table_name) - "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}" + Gitlab::AppLogger.info(message: "Dropped previously detached partition", partition_name: detached_partition.table_name) end def partition_attached?(partition_identifier) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9841a48a601..05a11512583 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -46195,6 +46195,9 @@ msgstr "" msgid "WorkItem|Add task" msgstr "" +msgid "WorkItem|Add tasks" +msgstr "" + msgid "WorkItem|Add to iteration" msgstr "" @@ -46239,6 +46242,9 @@ msgstr "" msgid "WorkItem|Due date" msgstr "" +msgid "WorkItem|Existing task" +msgstr "" + msgid "WorkItem|Expand tasks" msgstr "" @@ -46260,6 +46266,9 @@ msgstr "" msgid "WorkItem|Milestone" msgstr "" +msgid "WorkItem|New task" +msgstr "" + msgid "WorkItem|No iteration" msgstr "" @@ -46287,6 +46296,9 @@ msgstr "" msgid "WorkItem|Requirements" msgstr "" +msgid "WorkItem|Search existing tasks" +msgstr "" + msgid "WorkItem|Select type" msgstr "" diff --git a/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb b/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb index 786b0592153..590120ce7b2 100644 --- a/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb +++ b/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb @@ -1,7 +1,6 @@ pull: - image: alpine:3 + image: dtzar/helm-kubectl:latest script: - - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing - helm repo add --username <%= username %> --password <%= access_token %> gitlab_qa ${CI_API_V4_URL}/projects/<%= package_project.id %>/packages/helm/stable - helm repo update - helm pull gitlab_qa/<%= package_name %> diff --git a/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb index b3e907b50f4..b1c275cd96a 100644 --- a/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb +++ b/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb @@ -1,7 +1,6 @@ deploy: - image: alpine:3 + image: dtzar/helm-kubectl:latest script: - - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing - apk add curl - helm create <%= package_name %> - cp ./Chart.yaml <%= package_name %> diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb index 2cefb95db98..4c15b7c7f99 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Package', :skip_live_env, :orchestrated, :packages, :object_storage, product_group: :package_registry do - describe 'Helm Registry', quarantine: { type: :broken, issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/382262" } do + describe 'Helm Registry' do using RSpec::Parameterized::TableSyntax include Runtime::Fixtures include Support::Helpers::MaskToken diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb index e83193da599..10a1bf7541e 100644 --- a/spec/features/work_items/work_item_children_spec.rb +++ b/spec/features/work_items/work_item_children_spec.rb @@ -48,6 +48,7 @@ RSpec.describe 'Work item children', :js do expect(page).not_to have_selector('[data-testid="add-links-form"]') click_button 'Add' + click_button 'New task' expect(page).to have_selector('[data-testid="add-links-form"]') @@ -57,9 +58,10 @@ RSpec.describe 'Work item children', :js do end end - it 'addss a child task', :aggregate_failures do + it 'adds a new child task', :aggregate_failures do page.within('[data-testid="work-item-links"]') do click_button 'Add' + click_button 'New task' expect(page).to have_button('Create task', disabled: true) fill_in 'Add a title', with: 'Task 1' @@ -77,6 +79,7 @@ RSpec.describe 'Work item children', :js do it 'removes a child task and undoing', :aggregate_failures do page.within('[data-testid="work-item-links"]') do click_button 'Add' + click_button 'New task' fill_in 'Add a title', with: 'Task 1' click_button 'Create task' wait_for_all_requests @@ -105,5 +108,29 @@ RSpec.describe 'Work item children', :js do expect(find('[data-testid="children-count"]')).to have_content('1') end end + + context 'with existing task' do + let_it_be(:task) { create(:work_item, :task, project: project) } + + it 'adds an existing child task', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + click_button 'Add' + click_button 'Existing task' + + expect(page).to have_button('Add task', disabled: true) + find('[data-testid="work-item-token-select-input"]').set(task.title) + wait_for_all_requests + click_button task.title + + expect(page).to have_button('Add task', disabled: false) + + click_button 'Add task' + + wait_for_all_requests + + expect(find('[data-testid="links-child"]')).to have_content(task.title) + end + end + end end end diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js index ee6470a9df8..310398b01cf 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js @@ -26,6 +26,7 @@ import { import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql'; import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql'; +import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue'; import Tracking from '~/tracking'; @@ -34,6 +35,7 @@ import { graphQLImageDetailsMock, graphQLDeleteImageRepositoryTagsMock, graphQLDeleteImageRepositoryTagImportingErrorMock, + graphQLProjectImageRepositoriesDetailsMock, containerRepositoryMock, graphQLEmptyImageDetailsMock, tagsMock, @@ -64,6 +66,9 @@ describe('Details Page', () => { const defaultConfig = { noContainersImage: 'noContainersImage', + projectListUrl: 'projectListUrl', + groupListUrl: 'groupListUrl', + isGroupPage: false, }; const cleanTags = tagsMock.map((t) => { @@ -81,7 +86,8 @@ describe('Details Page', () => { const mountComponent = ({ resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()), mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock), - tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)), + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())), + detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock), options, config = defaultConfig, } = {}) => { @@ -91,6 +97,7 @@ describe('Details Page', () => { [getContainerRepositoryDetailsQuery, resolver], [deleteContainerRepositoryTagsMutation, mutationResolver], [getContainerRepositoryTagsQuery, tagsResolver], + [getContainerRepositoriesDetails, detailsResolver], ]; apolloProvider = createMockApollo(requestHandlers); @@ -256,11 +263,13 @@ describe('Details Page', () => { describe('confirmDelete event', () => { let mutationResolver; let tagsResolver; + let detailsResolver; beforeEach(() => { mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock); - tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)); - mountComponent({ mutationResolver, tagsResolver }); + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())); + detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); + mountComponent({ mutationResolver, tagsResolver, detailsResolver }); return waitForApolloRequestRender(); }); @@ -280,6 +289,7 @@ describe('Details Page', () => { await waitForPromises(); expect(tagsResolver).toHaveBeenCalled(); + expect(detailsResolver).toHaveBeenCalled(); }); }); @@ -298,6 +308,7 @@ describe('Details Page', () => { await waitForPromises(); expect(tagsResolver).toHaveBeenCalled(); + expect(detailsResolver).toHaveBeenCalled(); }); }); }); @@ -359,14 +370,16 @@ describe('Details Page', () => { describe('importing repository error', () => { let mutationResolver; let tagsResolver; + let detailsResolver; beforeEach(async () => { mutationResolver = jest .fn() .mockResolvedValue(graphQLDeleteImageRepositoryTagImportingErrorMock); - tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)); + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())); + detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); - mountComponent({ mutationResolver, tagsResolver }); + mountComponent({ mutationResolver, tagsResolver, detailsResolver }); await waitForApolloRequestRender(); }); @@ -378,6 +391,7 @@ describe('Details Page', () => { await waitForPromises(); expect(tagsResolver).toHaveBeenCalled(); + expect(detailsResolver).toHaveBeenCalled(); const deleteAlert = findDeleteAlert(); expect(deleteAlert.exists()).toBe(true); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index 60866aa98b1..071d5fb715a 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -1,10 +1,11 @@ import Vue from 'vue'; -import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui'; +import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; +import { FORM_TYPES } from '~/work_items/constants'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; @@ -24,25 +25,31 @@ describe('WorkItemLinksForm', () => { const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse); + const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse); const mockParentIteration = mockIterationWidgetResponse; const createComponent = async ({ - listResponse = availableWorkItemsResponse, typesResponse = projectWorkItemTypesQueryResponse, parentConfidential = false, hasIterationsFeature = false, workItemsMvc2Enabled = false, parentIteration = null, + formType = FORM_TYPES.create, } = {}) => { wrapper = shallowMountExtended(WorkItemLinksForm, { apolloProvider: createMockApollo([ - [projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)], + [projectWorkItemsQuery, availableWorkItemsResolver], [projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)], [updateWorkItemMutation, updateMutationResolver], [createWorkItemMutation, createMutationResolver], ]), - propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential, parentIteration }, + propsData: { + issuableGid: 'gid://gitlab/WorkItem/1', + parentConfidential, + parentIteration, + formType, + }, provide: { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, @@ -56,89 +63,104 @@ describe('WorkItemLinksForm', () => { }; const findForm = () => wrapper.findComponent(GlForm); - const findCombobox = () => wrapper.findComponent(GlFormCombobox); + const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); const findInput = () => wrapper.findComponent(GlFormInput); const findAddChildButton = () => wrapper.findByTestId('add-child-button'); - beforeEach(async () => { - await createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); - it('renders form', () => { - expect(findForm().exists()).toBe(true); - }); - - it('creates child task in non confidential parent', async () => { - findInput().vm.$emit('input', 'Create task test'); + describe('creating a new work item', () => { + beforeEach(async () => { + await createComponent(); + }); - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), + it('renders create form', () => { + expect(findForm().exists()).toBe(true); + expect(findInput().exists()).toBe(true); + expect(findAddChildButton().text()).toBe('Create task'); + expect(findTokenSelector().exists()).toBe(false); }); - await waitForPromises(); - expect(createMutationResolver).toHaveBeenCalledWith({ - input: { - title: 'Create task test', - projectPath: 'project/path', - workItemTypeId: 'gid://gitlab/WorkItems::Type/3', - hierarchyWidget: { - parentId: 'gid://gitlab/WorkItem/1', + + it('creates child task in non confidential parent', async () => { + findInput().vm.$emit('input', 'Create task test'); + + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalledWith({ + input: { + title: 'Create task test', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: false, }, - confidential: false, - }, + }); }); - }); - it('creates child task in confidential parent', async () => { - await createComponent({ parentConfidential: true, workItemsMvc2Enabled: true }); + it('creates child task in confidential parent', async () => { + await createComponent({ parentConfidential: true }); - findInput().vm.$emit('input', 'Create confidential task'); + findInput().vm.$emit('input', 'Create confidential task'); - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); - await waitForPromises(); - expect(createMutationResolver).toHaveBeenCalledWith({ - input: { - title: 'Create confidential task', - projectPath: 'project/path', - workItemTypeId: 'gid://gitlab/WorkItems::Type/3', - hierarchyWidget: { - parentId: 'gid://gitlab/WorkItem/1', + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalledWith({ + input: { + title: 'Create confidential task', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: true, }, - confidential: true, - }, + }); }); }); - // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('selects and add child', async () => { - findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]); + describe('adding an existing work item', () => { + beforeEach(async () => { + await createComponent({ formType: FORM_TYPES.add }); + }); - findAddChildButton().vm.$emit('click'); - await waitForPromises(); - expect(updateMutationResolver).toHaveBeenCalled(); - }); + it('renders add form', () => { + expect(findForm().exists()).toBe(true); + expect(findTokenSelector().exists()).toBe(true); + expect(findAddChildButton().text()).toBe('Add task'); + expect(findInput().exists()).toBe(false); + }); - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('when typing in combobox', () => { - beforeEach(async () => { - findCombobox().vm.$emit('input', 'Task'); + it('searches for available work items as prop when typing in input', async () => { + findTokenSelector().vm.$emit('focus'); + findTokenSelector().vm.$emit('text-input', 'Task'); await waitForPromises(); - await jest.runOnlyPendingTimers(); - }); - it('passes available work items as prop', () => { - expect(findCombobox().exists()).toBe(true); - expect(findCombobox().props('tokenList').length).toBe(2); + expect(availableWorkItemsResolver).toHaveBeenCalled(); }); - it('passes action to create task', () => { - expect(findCombobox().props('actionList').length).toBe(1); + it('selects and adds children', async () => { + findTokenSelector().vm.$emit( + 'input', + availableWorkItemsResponse.data.workspace.workItems.nodes, + ); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + + await waitForPromises(); + + expect(findAddChildButton().text()).toBe('Add tasks'); + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(updateMutationResolver).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index 30ccd68d276..66ce2c1becf 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -8,6 +8,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; +import { FORM_TYPES } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; @@ -114,7 +115,9 @@ describe('WorkItemLinks', () => { const findToggleButton = () => wrapper.findByTestId('toggle-links'); const findLinksBody = () => wrapper.findByTestId('links-body'); const findEmptyState = () => wrapper.findByTestId('links-empty'); + const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); + const findToggleCreateFormButton = () => wrapper.findByTestId('toggle-create-form'); const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0); const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); @@ -143,11 +146,27 @@ describe('WorkItemLinks', () => { }); describe('add link form', () => { - it('displays form on click add button and hides form on cancel', async () => { + it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => { + findToggleFormDropdown().vm.$emit('click'); findToggleAddFormButton().vm.$emit('click'); await nextTick(); expect(findAddLinksForm().exists()).toBe(true); + expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.add); + + findAddLinksForm().vm.$emit('cancel'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(false); + }); + + it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => { + findToggleFormDropdown().vm.$emit('click'); + findToggleCreateFormButton().vm.$emit('click'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(true); + expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.create); findAddLinksForm().vm.$emit('cancel'); await nextTick(); @@ -200,7 +219,7 @@ describe('WorkItemLinks', () => { }); it('does not display button to toggle Add form', () => { - expect(findToggleAddFormButton().exists()).toBe(false); + expect(findToggleFormDropdown().exists()).toBe(false); }); it('does not display link menu on children', () => { @@ -290,6 +309,7 @@ describe('WorkItemLinks', () => { await createComponent({ issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)), }); + findToggleFormDropdown().vm.$emit('click'); findToggleAddFormButton().vm.$emit('click'); await nextTick(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 0099dce77b3..635a1f326f8 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -331,7 +331,8 @@ export const workItemResponseFactory = ({ export const projectWorkItemTypesQueryResponse = { data: { workspace: { - id: 'gid://gitlab/WorkItem/1', + __typename: 'Project', + id: 'gid://gitlab/Project/2', workItemTypes: { nodes: [ { id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' }, @@ -873,22 +874,20 @@ export const availableWorkItemsResponse = { __typename: 'Project', id: 'gid://gitlab/Project/2', workItems: { - edges: [ + nodes: [ { - node: { - id: 'gid://gitlab/WorkItem/458', - title: 'Task 1', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - }, + id: 'gid://gitlab/WorkItem/458', + title: 'Task 1', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + __typename: 'WorkItem', }, { - node: { - id: 'gid://gitlab/WorkItem/459', - title: 'Task 2', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - }, + id: 'gid://gitlab/WorkItem/459', + title: 'Task 2', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + __typename: 'WorkItem', }, ], }, diff --git a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb index 2ef873e8adb..336dec3a8a0 100644 --- a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb +++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb @@ -92,11 +92,11 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do context 'removing foreign keys' do it 'removes foreign keys from the table before dropping it' do - expect(dropper).to receive(:drop_detached_partition).and_wrap_original do |drop_method, partition_name| - expect(partition_name).to eq('test_partition') - expect(foreign_key_exists_by_name(partition_name, 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_falsey + expect(dropper).to receive(:drop_detached_partition).and_wrap_original do |drop_method, partition| + expect(partition.table_name).to eq('test_partition') + expect(foreign_key_exists_by_name(partition.table_name, 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_falsey - drop_method.call(partition_name) + drop_method.call(partition) end expect(foreign_key_exists_by_name('test_partition', 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_truthy |