Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
345c883737
commit
4e1af5260d
49
.gitpod.yml
49
.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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default {
|
|||
canMerge: {
|
||||
query: readyToMergeQuery,
|
||||
skip() {
|
||||
return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
|
||||
return !this.mr;
|
||||
},
|
||||
variables() {
|
||||
return this.mergeRequestQueryVariables;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -499,6 +499,7 @@ export default {
|
|||
:work-item-type="workItemType"
|
||||
:fetch-by-iid="fetchByIid"
|
||||
:query-variables="queryVariables"
|
||||
:full-path="fullPath"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.dropdown-reduced-height {
|
||||
max-height: $dropdown-max-height;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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) } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue