Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-16 09:11:23 +00:00
parent 345c883737
commit 4e1af5260d
46 changed files with 918 additions and 247 deletions

View File

@ -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

View File

@ -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,
},
},
},
},
},
},
),
});

View File

@ -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,
},
},
],
});

View File

@ -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, {

View File

@ -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"

View File

@ -8,7 +8,7 @@ export default {
canMerge: {
query: readyToMergeQuery,
skip() {
return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
return !this.mr;
},
variables() {
return this.mergeRequestQueryVariables;

View File

@ -31,16 +31,6 @@ export default {
{{ s__('mrWidget|Merge blocked: all threads must be resolved.') }}
</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"
@ -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>

View File

@ -499,6 +499,7 @@ export default {
:work-item-type="workItemType"
:fetch-by-iid="fetchByIid"
:query-variables="queryVariables"
:full-path="fullPath"
@error="updateError = $event"
/>
</template>

View File

@ -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', {

View File

@ -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"
/>

View File

@ -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') }}

View File

@ -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;

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -46,6 +46,10 @@
}
}
&.dropdown-reduced-height {
max-height: $dropdown-max-height;
}
@include media-breakpoint-down(xs) {
width: 100%;
}

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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 } }

View File

@ -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) } }

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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>

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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 %>

View File

@ -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 %>

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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');
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,
},
});
});
it('creates child task in confidential parent', async () => {
await createComponent({ parentConfidential: true, workItemsMvc2Enabled: true });
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',
},
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]);
findAddChildButton().vm.$emit('click');
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('when typing in combobox', () => {
describe('creating a new work item', () => {
beforeEach(async () => {
findCombobox().vm.$emit('input', 'Task');
await createComponent();
});
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);
});
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();
await jest.runOnlyPendingTimers();
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,
},
});
});
it('passes available work items as prop', () => {
expect(findCombobox().exists()).toBe(true);
expect(findCombobox().props('tokenList').length).toBe(2);
it('creates child task in confidential parent', async () => {
await createComponent({ parentConfidential: true });
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',
},
confidential: true,
},
});
});
});
describe('adding an existing work item', () => {
beforeEach(async () => {
await createComponent({ formType: FORM_TYPES.add });
});
it('passes action to create task', () => {
expect(findCombobox().props('actionList').length).toBe(1);
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);
});
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();
expect(availableWorkItemsResolver).toHaveBeenCalled();
});
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();
});
});

View File

@ -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();

View File

@ -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',
},
],
},

View File

@ -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