Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-06 09:18:56 +00:00
parent 0a0e8803b0
commit c06178d51a
123 changed files with 1540 additions and 1241 deletions

View File

@ -0,0 +1,148 @@
<script>
import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/project-create-from-template-sm.svg';
import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg';
import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg';
import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { experiment } from '~/experimentation/utils';
import { s__ } from '~/locale';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const NEW_REPO_EXPERIMENT = 'new_repo';
const CI_CD_PANEL = 'cicd_for_external_repo';
const PANELS = [
{
key: 'blank',
name: 'blank_project',
selector: '#blank-project-pane',
title: s__('ProjectsNew|Create blank project'),
description: s__(
'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.',
),
illustration: blankProjectIllustration,
},
{
key: 'template',
name: 'create_from_template',
selector: '#create-from-template-pane',
title: s__('ProjectsNew|Create from template'),
description: s__(
'ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly.',
),
illustration: createFromTemplateIllustration,
},
{
key: 'import',
name: 'import_project',
selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'),
description: s__(
'ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
),
illustration: importProjectIllustration,
},
{
key: 'ci',
name: CI_CD_PANEL,
selector: '#ci-cd-project-pane',
title: s__('ProjectsNew|Run CI/CD for external repository'),
description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'),
illustration: ciCdProjectIllustration,
},
];
export default {
components: {
NewNamespacePage,
NewProjectPushTipPopover,
},
directives: {
SafeHtml,
},
props: {
hasErrors: {
type: Boolean,
required: false,
default: false,
},
isCiCdAvailable: {
type: Boolean,
required: false,
default: false,
},
newProjectGuidelines: {
type: String,
required: false,
default: '',
},
},
computed: {
decoratedPanels() {
const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
use: () => ({
blank: s__('ProjectsNew|Create blank project'),
import: s__('ProjectsNew|Import project'),
}),
try: () => ({
blank: s__('ProjectsNew|Create blank project/repository'),
import: s__('ProjectsNew|Import project/repository'),
}),
});
return PANELS.map(({ key, title, ...el }) => ({
...el,
title: PANEL_TITLES[key] ?? title,
}));
},
availablePanels() {
return this.isCiCdAvailable
? this.decoratedPanels
: this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
},
},
methods: {
resetProjectErrors() {
const errorsContainer = document.querySelector('.project-edit-errors');
if (errorsContainer) {
errorsContainer.innerHTML = '';
}
},
},
EXPERIMENT: NEW_REPO_EXPERIMENT,
};
</script>
<template>
<new-namespace-page
:initial-breadcrumb="s__('New project')"
:panels="availablePanels"
:jump-to-last-persisted-panel="hasErrors"
:title="s__('ProjectsNew|Create new project')"
:experiment="$options.EXPERIMENT"
persistence-key="new_project_last_active_tab"
@panel-change="resetProjectErrors"
>
<template #extra-description>
<div
v-if="newProjectGuidelines"
id="new-project-guideline"
v-safe-html="newProjectGuidelines"
></div>
</template>
<template #welcome-footer>
<div class="gl-pt-5 gl-text-center">
<p>
{{ __('You can also create a project from the command line.') }}
<a ref="clipTip" href="#" @click.prevent>
{{ __('Show command') }}
</a>
<new-project-push-tip-popover :target="() => $refs.clipTip" />
</p>
</div>
</template>
</new-namespace-page>
</template>

View File

@ -1,28 +1,44 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
import NewProjectCreationApp from './components/app.vue';
initProjectVisibilitySelector();
initProjectNew.bindEvents();
import(
/* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
)
.then((m) => {
const el = document.querySelector('.js-experiment-new-project-creation');
function initNewProjectCreation(el) {
const {
pushToCreateProjectCommand,
workingWithProjectsHelpPath,
newProjectGuidelines,
hasErrors,
isCiCdAvailable,
} = el.dataset;
if (!el) {
return;
}
const props = {
hasErrors: parseBoolean(hasErrors),
isCiCdAvailable: parseBoolean(isCiCdAvailable),
newProjectGuidelines,
};
const config = {
hasErrors: 'hasErrors' in el.dataset,
isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
newProjectGuidelines: el.dataset.newProjectGuidelines,
};
m.default(el, config);
})
.catch(() => {
createFlash(__('An error occurred while loading project creation UI'));
const provide = {
workingWithProjectsHelpPath,
pushToCreateProjectCommand,
};
return new Vue({
el,
components: {
NewProjectCreationApp,
},
provide,
render(h) {
return h(NewProjectCreationApp, { props });
},
});
}
const el = document.querySelector('.js-new-project-creation');
initNewProjectCreation(el);

View File

@ -1,5 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlIcon } from '@gitlab/ui';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql';
@ -55,6 +57,9 @@ export default {
data: { currentBranch: newBranch },
});
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
this.$emit('refetchContent');
},
},

View File

@ -1,201 +0,0 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { experiment } from '~/experimentation/utils';
import { __, s__ } from '~/locale';
import { NEW_REPO_EXPERIMENT } from '../constants';
import blankProjectIllustration from '../illustrations/blank-project.svg';
import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
import importProjectIllustration from '../illustrations/import-project.svg';
import LegacyContainer from './legacy_container.vue';
import WelcomePage from './welcome.vue';
const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo';
const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
const PANELS = [
{
key: 'blank',
name: BLANK_PANEL,
selector: '#blank-project-pane',
title: s__('ProjectsNew|Create blank project'),
description: s__(
'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.',
),
illustration: blankProjectIllustration,
},
{
key: 'template',
name: 'create_from_template',
selector: '#create-from-template-pane',
title: s__('ProjectsNew|Create from template'),
description: s__(
'Create a project pre-populated with the necessary files to get you started quickly.',
),
illustration: createFromTemplateIllustration,
},
{
key: 'import',
name: 'import_project',
selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'),
description: s__(
'Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
),
illustration: importProjectIllustration,
},
{
key: 'ci',
name: CI_CD_PANEL,
selector: '#ci-cd-project-pane',
title: s__('ProjectsNew|Run CI/CD for external repository'),
description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'),
illustration: ciCdProjectIllustration,
},
];
export default {
components: {
GlBreadcrumb,
GlIcon,
WelcomePage,
LegacyContainer,
},
directives: {
SafeHtml,
},
props: {
hasErrors: {
type: Boolean,
required: false,
default: false,
},
isCiCdAvailable: {
type: Boolean,
required: false,
default: false,
},
newProjectGuidelines: {
type: String,
required: false,
default: '',
},
},
data() {
return {
activeTab: null,
};
},
computed: {
decoratedPanels() {
const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
use: () => ({
blank: s__('ProjectsNew|Create blank project'),
import: s__('ProjectsNew|Import project'),
}),
try: () => ({
blank: s__('ProjectsNew|Create blank project/repository'),
import: s__('ProjectsNew|Import project/repository'),
}),
});
return PANELS.map(({ key, title, ...el }) => ({
...el,
title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title,
}));
},
availablePanels() {
if (this.isCiCdAvailable) {
return this.decoratedPanels;
}
return this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
},
activePanel() {
return this.decoratedPanels.find((p) => p.name === this.activeTab);
},
breadcrumbs() {
if (!this.activeTab || !this.activePanel) {
return null;
}
return [
{ text: __('New project'), href: '#' },
{ text: this.activePanel.title, href: `#${this.activeTab}` },
];
},
},
created() {
this.handleLocationHashChange();
if (this.hasErrors) {
this.activeTab = localStorage.getItem(LAST_ACTIVE_TAB_KEY) || BLANK_PANEL;
}
window.addEventListener('hashchange', () => {
this.handleLocationHashChange();
this.resetProjectErrors();
});
this.$root.$on('clicked::link', (e) => {
window.location = e.target.href;
});
},
methods: {
resetProjectErrors() {
const errorsContainer = document.querySelector('.project-edit-errors');
if (errorsContainer) {
errorsContainer.innerHTML = '';
}
},
handleLocationHashChange() {
this.activeTab = window.location.hash.substring(1) || null;
if (this.activeTab) {
localStorage.setItem(LAST_ACTIVE_TAB_KEY, this.activeTab);
}
},
},
PANELS,
};
</script>
<template>
<welcome-page v-if="activeTab === null" :panels="availablePanels" />
<div v-else class="row">
<div class="col-lg-3">
<div class="gl-text-white" v-html="activePanel.illustration"></div>
<h4>{{ activePanel.title }}</h4>
<p>{{ activePanel.description }}</p>
<div
v-if="newProjectGuidelines"
id="new-project-guideline"
v-safe-html="newProjectGuidelines"
></div>
</div>
<div class="col-lg-9">
<gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
<template #separator>
<gl-icon name="chevron-right" :size="8" />
</template>
</gl-breadcrumb>
<template v-for="panel in $options.PANELS">
<legacy-container
v-if="activeTab === panel.name"
:key="panel.name"
class="gl-mt-3"
:selector="panel.selector"
/>
</template>
</div>
</div>
</template>

View File

@ -1,66 +0,0 @@
<script>
/* eslint-disable vue/no-v-html */
import Tracking from '~/tracking';
import { NEW_REPO_EXPERIMENT } from '../constants';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
export default {
components: {
NewProjectPushTipPopover,
},
mixins: [trackingMixin],
props: {
panels: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div class="container">
<div class="blank-state-welcome">
<h2 class="blank-state-welcome-title gl-mt-5! gl-mb-3!">
{{ s__('ProjectsNew|Create new project') }}
</h2>
<p div class="blank-state-text">&nbsp;</p>
</div>
<div class="row blank-state-row">
<a
v-for="panel in panels"
:key="panel.name"
:href="`#${panel.name}`"
:data-qa-selector="`${panel.name}_link`"
class="blank-state blank-state-link experiment-new-project-page-blank-state"
@click="track('click_tab', { label: panel.name })"
>
<div class="blank-state-icon gl-text-white" v-html="panel.illustration"></div>
<div class="blank-state-body gl-pl-4!">
<h3 class="blank-state-title experiment-new-project-page-blank-state-title">
{{ panel.title }}
</h3>
<p class="blank-state-text">
{{ panel.description }}
</p>
</div>
</a>
</div>
<div class="blank-state-welcome">
<p>
{{ __('You can also create a project from the command line.') }}
<a
ref="clipTip"
href="#"
click.prevent
class="push-new-project-tip"
rel="noopener noreferrer"
>
{{ __('Show command') }}
</a>
<new-project-push-tip-popover :target="() => $refs.clipTip" />
</p>
</div>
</div>
</template>

View File

@ -1 +0,0 @@
export const NEW_REPO_EXPERIMENT = 'new_repo';

View File

@ -1,9 +0,0 @@
<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.1912 8.19118H77.6176C78.2755 8.19118 78.8088 8.72448 78.8088 9.38235V69.6176C78.8088 70.2755 78.2755 70.8088 77.6176 70.8088H66.1912V8.19118Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.38235"/>
<path d="M22.0517 19.2723L22.0094 10.1001C22.004 8.92546 22.8555 7.92221 24.0153 7.73664L63.3613 1.44139C64.8087 1.2098 66.12 2.32794 66.12 3.79382V75.8717C66.12 77.3323 64.8177 78.449 63.3742 78.2262L24.3037 72.1952C23.1461 72.0165 22.2902 71.023 22.2848 69.8517L22.2428 60.7554" stroke="#DBDBDB" stroke-width="2.38235"/>
<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.38235"/>
<circle cx="23" cy="40" r="17" fill="#6E49CB"/>
<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/>
<path d="M22.3125 48V33.3659" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/>
<path d="M15 40.3049H30" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1010 B

View File

@ -1,23 +0,0 @@
<svg width="169" height="78" viewBox="0 0 169 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M115.571 41.5714L147.714 41.5714C158.365 41.5714 167 32.9369 167 22.2857C167 11.6345 158.365 3 147.714 3C137.063 3 128.429 11.6345 128.429 22.2857C128.429 27.3128 130.352 31.8907 133.503 35.3235" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path d="M115.107 41.5714H125.786C133.084 41.5714 139 47.4877 139 54.7857C139 62.0838 133.084 68 125.786 68C118.488 68 112.571 62.0838 112.571 54.7857C112.571 53.039 112.91 51.3715 113.526 49.8453" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path d="M87.5486 37H76.3943C75.6243 37 75 36.3746 75 35.6032C75 34.8318 75.6243 34.2064 76.3943 34.2064H87.5486C88.3187 34.2064 88.9429 34.8318 88.9429 35.6032C88.9429 36.3746 88.3187 37 87.5486 37Z" fill="#FC6D26"/>
<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="#FC6D26"/>
<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="white" fill-opacity="0.6"/>
<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="#6E49CB"/>
<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="white" fill-opacity="0.8"/>
<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="#6E49CB"/>
<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="white" fill-opacity="0.8"/>
<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="#6E49CB"/>
<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="white" fill-opacity="0.4"/>
<path d="M146.262 24.2349L143.048 21.0153C142.767 20.7338 142.282 20.7323 141.983 21.0313L140.394 22.6236C140.1 22.9181 140.088 23.4002 140.378 23.6903L145.344 28.6651C145.841 29.1637 146.666 29.1795 147.166 28.6793L147.866 27.9779L155.864 19.9653C156.171 19.658 156.167 19.1776 155.868 18.8786L154.279 17.2863C153.985 16.9918 153.495 16.9891 153.194 17.2903L146.262 24.2349Z" fill="#FC6D26"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M125.682 56.7113L123.087 59.3221C122.858 59.5529 122.547 59.6825 122.223 59.6824C121.898 59.6824 121.587 59.5526 121.358 59.3218C121.129 59.091 121 58.7779 121 58.4515C121 58.1251 121.129 57.8121 121.358 57.5813L123.087 55.8412L121.358 54.1011C121.129 53.8703 121 53.5573 121 53.2309C121 52.9045 121.129 52.5915 121.358 52.3606C121.587 52.1298 121.898 52.0001 122.223 52C122.547 51.9999 122.858 52.1296 123.087 52.3603L125.682 54.9711C125.911 55.2019 126.04 55.5149 126.04 55.8412C126.04 56.1675 125.911 56.4805 125.682 56.7113ZM131.796 56.7113L129.202 59.3221C129.088 59.4364 128.954 59.527 128.805 59.5888C128.657 59.6506 128.498 59.6824 128.337 59.6824C128.177 59.6824 128.018 59.6505 127.869 59.5886C127.721 59.5268 127.586 59.4361 127.472 59.3218C127.359 59.2075 127.269 59.0718 127.207 58.9225C127.146 58.7732 127.114 58.6131 127.114 58.4515C127.114 58.2899 127.146 58.1299 127.208 57.9806C127.269 57.8313 127.359 57.6956 127.473 57.5813L129.202 55.8412L127.473 54.1011C127.359 53.9868 127.269 53.8512 127.208 53.7018C127.146 53.5525 127.114 53.3925 127.114 53.2309C127.114 53.0693 127.146 52.9092 127.207 52.7599C127.269 52.6106 127.359 52.4749 127.472 52.3606C127.586 52.2463 127.721 52.1556 127.869 52.0938C128.018 52.0319 128.177 52 128.337 52C128.498 52 128.657 52.0318 128.805 52.0936C128.954 52.1554 129.088 52.246 129.202 52.3603L131.796 54.9711C132.026 55.2019 132.154 55.5149 132.154 55.8412C132.154 56.1675 132.026 56.4805 131.796 56.7113Z" fill="#6E49CB"/>
<path d="M2 26C2 28.415 14.4361 30.3727 29.7769 30.3727C33.7709 30.3727 37.568 30.24 41 30.0011" stroke="#DBDBDB" stroke-width="1.28173"/>
<path d="M2 50C2 52.415 14.4361 54.3727 29.7769 54.3727C35.6133 54.3727 41.0293 54.0893 45.5 53.6052" stroke="#DBDBDB" stroke-width="1.28173"/>
<path d="M57.5537 5V22M2 5V68.6673C2 73.1731 20.9696 75.5204 29.7769 75.5204C38.5842 75.5204 57.5537 73.1731 57.5537 68.6673V57" stroke="#DBDBDB" stroke-width="2.56346" stroke-linejoin="round"/>
<ellipse cx="29.7769" cy="5.64391" rx="27.7769" ry="3.64391" stroke="#DBDBDB" stroke-width="2.56346"/>
<ellipse cx="55.4286" cy="39.46" rx="17.4286" ry="17.46" stroke="#6E49CB" stroke-width="2.56346"/>
<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="#6E49CB"/>
<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="white" fill-opacity="0.9"/>
<path d="M61.763 38.5893C62.5797 39.0892 62.5797 40.2756 61.763 40.7756L52.951 46.1704C52.0969 46.6933 51 46.0787 51 45.0773L51 34.2875C51 33.2861 52.0969 32.6715 52.951 33.1944L61.763 38.5893Z" fill="#6E49CB"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,13 +0,0 @@
<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M68.1765 8.17647H79.6471C80.2968 8.17647 80.8235 8.70319 80.8235 9.35294V69.6471C80.8235 70.2968 80.2968 70.8235 79.6471 70.8235H68.1765V8.17647Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.35294"/>
<path d="M24.0504 19L24.0093 10.0746C24.0039 8.9145 24.8449 7.92363 25.9905 7.74035L65.393 1.43595C66.8226 1.20721 68.1176 2.31155 68.1176 3.75934V75.903C68.1176 77.3456 66.8314 78.4485 65.4057 78.2284L26.2788 72.1887C25.1356 72.0122 24.2902 71.0309 24.2849 69.8742L24.244 61" stroke="#DBDBDB" stroke-width="2.35294"/>
<path d="M60.0194 11.1796L30.0195 15.2198C29.4357 15.2984 29 15.7966 29 16.3857V19.1235C29 19.8153 29.594 20.3578 30.283 20.2951L60.283 17.5679C60.889 17.5128 61.3529 17.0047 61.3529 16.3962V12.3455C61.3529 11.6334 60.7252 11.0845 60.0194 11.1796Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M51.1704 29.1021L41.8902 29.8481C41.0202 29.918 40.5266 30.8776 40.9756 31.626L42.6523 34.4205C42.8676 34.7793 43.2573 34.9968 43.6758 34.9916L51.2794 34.8968C51.9233 34.8888 52.4412 34.3645 52.4412 33.7205V30.2748C52.4412 29.5879 51.8551 29.0471 51.1704 29.1021Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M61.2104 70.6341V40.1765C61.2104 39.5267 60.6837 39 60.0339 39H44.9909C44.4469 39 43.9738 39.373 43.8469 39.9019L41.118 51.2721C41.0819 51.4226 41.0148 51.5672 40.923 51.6918C37.1778 56.7763 34.7228 57.4741 29.7135 59.6826C29.2815 59.873 29.0064 60.3064 29.0162 60.7783L29.1309 66.295C29.1428 66.8693 29.5679 67.3511 30.1362 67.4345L59.8631 71.7981C60.5732 71.9024 61.2104 71.3519 61.2104 70.6341Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M43.5694 24L36 24.5" stroke="#DBDBDB" stroke-width="1.17647" stroke-linecap="round"/>
<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.35294"/>
<circle cx="23" cy="40" r="17" fill="#6E49CB"/>
<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/>
<path d="M22.3125 48V33" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/>
<path d="M15 41.3148H30" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,38 +0,0 @@
<svg width="169" height="84" viewBox="0 0 169 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M153.5 74.5714H165.684C166.411 74.5714 167 73.9822 167 73.2554V8.74461C167 8.01779 166.411 7.42859 165.684 7.42859H153.5" stroke="#DBDBDB" stroke-width="2.63203"/>
<path d="M107.94 57L108.014 72.9062C108.017 73.5536 108.49 74.1026 109.13 74.2008L151.913 80.7674C152.71 80.8897 153.429 80.273 153.429 79.4666V2.54193C153.429 1.73264 152.705 1.11511 151.906 1.24226L108.829 8.09543C108.187 8.19744 107.716 8.7519 107.719 9.4012L107.771 20.5" stroke="#DBDBDB" stroke-width="2.63203"/>
<path d="M133.539 52.5313L122.91 51.9925M137.311 52.7225L148.969 53.3135" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M132.224 43.9783L124 43.6955M135.998 44.1081L147.665 44.5092" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M148.238 12.3644L131.189 14.604M117.282 16.4529L126.416 15.2311" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M149.032 36.8519L131.839 37.0342M125 37.0852L127.024 37.0852" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M112.038 66.3444L120.582 67.4102M148.266 70.8634L134.595 69.1581M125.025 67.9644L129.468 68.5186" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M114.352 23.3947L116.215 23.2387M129.258 22.147L119.433 22.9693M137.388 21.4665L145.18 20.8143" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M135.832 29.2067L125.981 29.5888M138.724 28.9864L146.537 28.6833" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M115.114 59.5557L128.942 60.8796M133.782 61.3429L145.19 62.4351" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M53.4286 42.4286H21.2857C10.6345 42.4286 2.00002 33.7941 2.00002 23.1429C2.00002 12.4917 10.6345 3.85718 21.2857 3.85718C31.9369 3.85718 40.5714 12.4917 40.5714 23.1429C40.5714 28.17 38.648 32.7479 35.4969 36.1807" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path d="M53.0361 42.4286H42.3571C35.0591 42.4286 29.1428 48.3448 29.1428 55.6429C29.1428 62.9409 35.0591 68.8572 42.3571 68.8572C49.6552 68.8572 55.5714 62.9409 55.5714 55.6429C55.5714 53.8962 55.2325 52.2287 54.6169 50.7025" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4286 51.7144C38.4286 50.9254 39.0682 50.2858 39.8572 50.2858H44.1429C44.829 50.2858 45.4022 50.7695 45.5399 51.4146L47.7105 52.6677C48.3938 53.0622 48.6279 53.9359 48.2334 54.6192C47.3183 56.2042 45.5714 59.2248 45.4609 59.4191C45.1836 59.9063 44.7237 60.2858 44.1429 60.2858H39.8572C39.0682 60.2858 38.4286 59.6462 38.4286 58.8572V51.7144ZM39.8572 51.7144H44.1429V58.8572H39.8572L39.8572 51.7144ZM45.5714 56.3727L46.9962 53.9049L45.5714 53.0823V56.3727Z" fill="#FC6D26"/>
<path d="M25.5984 15.2331C25.8026 14.471 25.3503 13.6877 24.5882 13.4835C23.8261 13.2793 23.0428 13.7315 22.8386 14.4936L18.4017 31.0524C18.1975 31.8145 18.6497 32.5978 19.4118 32.802C20.1739 33.0062 20.9573 32.5539 21.1615 31.7918L25.5984 15.2331Z" fill="#6E49CB"/>
<path d="M17.2958 17.8469C17.8537 18.4048 17.8537 19.3093 17.2958 19.8672L14.0203 23.1428L17.2958 26.4183C17.8537 26.9762 17.8537 27.8807 17.2958 28.4386C16.738 28.9965 15.8334 28.9965 15.2755 28.4386L10.9898 24.1529C10.4319 23.595 10.4319 22.6905 10.9898 22.1326L15.2755 17.8469C15.8334 17.289 16.738 17.289 17.2958 17.8469Z" fill="#6E49CB"/>
<path d="M26.7041 17.8469C26.1462 18.4048 26.1462 19.3093 26.7041 19.8672L29.9797 23.1428L26.7041 26.4183C26.1462 26.9762 26.1462 27.8807 26.7041 28.4386C27.262 28.9965 28.1665 28.9965 28.7244 28.4386L33.0101 24.1529C33.568 23.595 33.568 22.6905 33.0101 22.1326L28.7244 17.8469C28.1665 17.289 27.262 17.289 26.7041 17.8469Z" fill="#6E49CB"/>
<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="#FC6D26"/>
<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="white" fill-opacity="0.6"/>
<path d="M70.5713 35.2857L83.4285 35.2857C84.2175 35.2857 84.8571 35.9253 84.8571 36.7143C84.8571 37.5032 84.2175 38.1428 83.4285 38.1428L70.5713 38.1428C69.7824 38.1428 69.1428 37.5032 69.1428 36.7143C69.1428 35.9253 69.7824 35.2857 70.5713 35.2857Z" fill="#FC6D26"/>
<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="#6E49CB"/>
<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="white" fill-opacity="0.8"/>
<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="#6E49CB"/>
<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="white" fill-opacity="0.6"/>
<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="#6E49CB"/>
<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="white" fill-opacity="0.8"/>
<circle cx="107.714" cy="38.8571" r="17.8571" stroke="#6E49CB" stroke-width="2.63203"/>
<circle cx="107.714" cy="38.8573" r="13.5714" fill="#6E49CB"/>
<circle cx="107.714" cy="38.8573" r="13.5714" fill="white" fill-opacity="0.9"/>
<path d="M111.431 35.0867L115.367 39.0232L111.431 42.9597C111.016 43.3744 110.344 43.3744 109.929 42.9597C109.515 42.545 109.515 41.8727 109.929 41.458L111.302 40.0851H101.123C100.537 40.0851 100.061 39.6097 100.061 39.0232C100.061 38.4367 100.537 37.9613 101.123 37.9613H111.302L109.929 36.5884C109.515 36.1737 109.515 35.5014 109.929 35.0867C110.344 34.672 111.016 34.672 111.431 35.0867Z" fill="#6E49CB"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="169" height="84" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,20 +0,0 @@
import Vue from 'vue';
import NewProjectCreationApp from './components/app.vue';
export default function initNewProjectCreation(el, props) {
const { pushToCreateProjectCommand, workingWithProjectsHelpPath } = el.dataset;
return new Vue({
el,
components: {
NewProjectCreationApp,
},
provide: {
workingWithProjectsHelpPath,
pushToCreateProjectCommand,
},
render(h) {
return h(NewProjectCreationApp, { props });
},
});
}

View File

@ -175,8 +175,10 @@ export default {
this.updateAssignees([this.currentUser.username]);
},
saveAssignees() {
this.isDirty = false;
this.updateAssignees(this.selected.map(({ username }) => username));
if (this.isDirty) {
this.isDirty = false;
this.updateAssignees(this.selected.map(({ username }) => username));
}
this.$el.dispatchEvent(hideDropdownEvent);
},
collapseWidget() {

View File

@ -95,7 +95,7 @@ export default {
<gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
<div v-if="showParticipantLabel" class="title hide-collapsed">
<div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2">
<gl-loading-icon v-if="loading" :inline="true" />
{{ participantLabel }}
</div>
@ -105,10 +105,10 @@ export default {
:key="participant.id"
class="participants-author"
>
<a :href="participant.web_url" class="author-link">
<a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image
:lazy="true"
:img-src="participant.avatar_url"
:img-src="participant.avatar_url || participant.avatarUrl"
:size="24"
:tooltip-text="participant.name"
css-classes="avatar-inline"

View File

@ -0,0 +1,68 @@
<script>
import { __ } from '~/locale';
import { participantsQueries } from '~/sidebar/constants';
import Participants from './participants.vue';
export default {
i18n: {
fetchingError: __('An error occurred while fetching participants'),
},
components: {
Participants,
},
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
participants: [],
};
},
apollo: {
participants: {
query() {
return participantsQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return data.workspace?.issuable?.participants.nodes || [];
},
error(error) {
this.$emit('fetch-error', {
message: this.$options.i18n.fetchingError,
error,
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.participants.loading;
},
},
};
</script>
<template>
<participants
:loading="isLoading"
:participants="participants"
:number-of-less-participants="7"
/>
</template>

View File

@ -1,6 +1,7 @@
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
@ -46,6 +47,9 @@ export const participantsQueries = {
[IssuableType.MergeRequest]: {
query: getMergeRequestParticipants,
},
[IssuableType.Epic]: {
query: epicParticipantsQuery,
},
};
export const confidentialityQueries = {

View File

@ -0,0 +1,18 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query epicParticipants($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
participants {
nodes {
...User
...UserAvailability
}
}
}
}
}

View File

@ -0,0 +1,71 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
import Tracking from '~/tracking';
export default {
directives: {
SafeHtml,
},
props: {
title: {
type: String,
required: true,
},
panels: {
type: Array,
required: true,
},
experiment: {
type: String,
required: false,
default: null,
},
},
created() {
const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: this.experiment });
const trackingInstance = new Vue({
...trackingMixin,
render() {
return null;
},
});
this.track = trackingInstance.track;
},
};
</script>
<template>
<div class="container">
<h2 class="gl-my-7 gl-font-size-h1 gl-text-center">
{{ title }}
</h2>
<div>
<div
v-for="panel in panels"
:key="panel.name"
class="new-namespace-panel-wrapper gl-display-inline-block gl-px-3 gl-mb-5"
>
<a
:href="`#${panel.name}`"
:data-qa-selector="`${panel.name}_link`"
class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!"
@click="track('click_tab', { label: panel.name })"
>
<div
v-safe-html="panel.illustration"
class="new-namespace-panel-illustration gl-text-white gl-display-flex gl-flex-shrink-0 gl-justify-content-center"
></div>
<div class="gl-pl-4">
<h3 class="gl-font-size-h2 gl-reset-color">
{{ panel.title }}
</h3>
<p class="gl-text-gray-900">
{{ panel.description }}
</p>
</div>
</a>
</div>
</div>
<slot name="footer"></slot>
</div>
</template>

View File

@ -0,0 +1,135 @@
<script>
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import LegacyContainer from './components/legacy_container.vue';
import WelcomePage from './components/welcome.vue';
export default {
components: {
GlBreadcrumb,
GlIcon,
WelcomePage,
LegacyContainer,
},
directives: {
SafeHtml,
},
props: {
title: {
type: String,
required: true,
},
initialBreadcrumb: {
type: String,
required: true,
},
panels: {
type: Array,
required: true,
},
jumpToLastPersistedPanel: {
type: Boolean,
required: false,
default: false,
},
persistenceKey: {
type: String,
required: true,
},
experiment: {
type: String,
required: false,
default: null,
},
},
data() {
return {
activePanelName: null,
};
},
computed: {
activePanel() {
return this.panels.find((p) => p.name === this.activePanelName);
},
details() {
return this.activePanel.details || this.activePanel.description;
},
hasTextDetails() {
return typeof this.details === 'string';
},
breadcrumbs() {
if (!this.activePanel) {
return null;
}
return [
{ text: this.initialBreadcrumb, href: '#' },
{ text: this.activePanel.title, href: `#${this.activePanel.name}` },
];
},
},
created() {
this.handleLocationHashChange();
if (this.jumpToLastPersistedPanel) {
this.activePanelName = localStorage.getItem(this.persistenceKey) || this.panels[0].name;
}
window.addEventListener('hashchange', () => {
this.handleLocationHashChange();
this.$emit('panel-change');
});
this.$root.$on('clicked::link', (e) => {
window.location = e.target.href;
});
},
methods: {
handleLocationHashChange() {
this.activePanelName = window.location.hash.substring(1) || null;
if (this.activePanelName) {
localStorage.setItem(this.persistenceKey, this.activePanelName);
}
},
},
};
</script>
<template>
<welcome-page
v-if="activePanelName === null"
:panels="panels"
:title="title"
:experiment="experiment"
>
<template #footer>
<slot name="welcome-footer"> </slot>
</template>
</welcome-page>
<div v-else class="row">
<div class="col-lg-3">
<div v-safe-html="activePanel.illustration" class="gl-text-white"></div>
<h4>{{ activePanel.title }}</h4>
<p v-if="hasTextDetails">{{ details }}</p>
<component :is="details" v-else />
<slot name="extra-description"></slot>
</div>
<div class="col-lg-9">
<gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
<template #separator>
<gl-icon name="chevron-right" :size="8" />
</template>
</gl-breadcrumb>
<legacy-container :key="activePanel.name" class="gl-mt-3" :selector="activePanel.selector" />
</div>
</div>
</template>

View File

@ -1,11 +1,13 @@
<script>
import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
import { dateInWords, isValidDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlBadge,
GlIcon,
GlLink,
GlButton,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@ -16,11 +18,37 @@ export default {
required: true,
},
},
computed: {
releaseDate() {
const { published_at } = this.feature;
const date = new Date(published_at);
if (!isValidDate(date) || date.getTime() === 0) {
return '';
}
return dateInWords(date);
},
},
};
</script>
<template>
<div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<div class="gl-py-6 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<div
class="whats-new-item-image gl-bg-size-cover"
:style="`background-image: url(${feature.image_url});`"
>
<span class="gl-sr-only">{{ feature.title }}</span>
</div>
</gl-link>
<gl-link
:href="feature.url"
target="_blank"
@ -29,39 +57,28 @@ export default {
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5>
<h5 class="gl-font-lg gl-mb-1" data-test-id="feature-title">{{ feature.title }}</h5>
</gl-link>
<div v-if="releaseDate" class="gl-mb-3" data-testid="release-date">{{ releaseDate }}</div>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="packageName in feature.packages"
:key="packageName"
size="sm"
class="whats-new-item-badge gl-mr-2 gl-py-1!"
size="md"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ packageName }}
</gl-badge>
</div>
<gl-link
<div v-safe-html="feature.body" class="gl-pt-3 gl-line-height-20"></div>
<gl-button
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/>
</gl-link>
<div v-safe-html="feature.body" class="gl-pt-3"></div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
{{ __('Learn more') }} <gl-icon name="arrow-right" />
</gl-button>
</div>
</template>

View File

@ -54,6 +54,7 @@
.whats-new-item-image {
border-color: $gray-50;
height: 250px;
}
.whats-new-modal-backdrop {

View File

@ -45,7 +45,6 @@
@import 'framework/toggle';
@import 'framework/typography';
@import 'framework/zen';
@import 'framework/blank';
@import 'framework/wells';
@import 'framework/page_header';
@import 'framework/page_title';

View File

@ -1,136 +0,0 @@
.blank-state-parent-container {
.section-container {
padding: 10px;
}
.section-body {
width: 100%;
height: 100%;
padding-bottom: 25px;
border-radius: $border-radius-default;
}
}
.blank-state-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.blank-state-welcome {
text-align: center;
padding: $gl-padding 0 ($gl-padding * 2);
.blank-state-welcome-title {
font-size: 24px;
}
.blank-state-text {
margin-bottom: 0;
}
}
.blank-state-link {
color: $gl-text-color;
margin-bottom: 15px;
&:hover {
background-color: $gray-light;
text-decoration: none;
color: $gl-text-color;
}
}
.blank-state-center {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
}
.blank-state {
display: flex;
align-items: center;
padding: 20px 50px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
min-height: 240px;
margin-bottom: $gl-padding;
width: calc(50% - #{$gl-padding-8});
@include media-breakpoint-down(sm) {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 50px 20px;
.column-small & {
width: 100%;
}
}
}
.blank-state,
.blank-state-center {
.blank-state-icon {
svg {
display: block;
margin: auto;
}
}
.blank-state-title {
margin-top: 0;
font-size: 18px;
}
.blank-state-body {
@include media-breakpoint-down(sm) {
text-align: center;
margin-top: 20px;
}
@include media-breakpoint-up(sm) {
padding-left: 20px;
}
}
}
@include media-breakpoint-up(lg) {
.column-large {
flex: 2;
}
.column-small {
flex: 1;
margin-bottom: 15px;
.blank-state {
max-width: 400px;
flex-wrap: wrap;
margin-left: 15px;
}
.blank-state-icon {
margin-bottom: 30px;
}
}
}
.experiment-new-project-page-blank-state {
@include media-breakpoint-down(md) {
flex-direction: column;
justify-content: center;
text-align: center;
}
.blank-state-icon {
min-width: 215px;
}
}
$experiment-new-project-indigo-700: #41419f;
.experiment-new-project-page-blank-state-title {
color: $experiment-new-project-indigo-700;
}

View File

@ -0,0 +1,28 @@
@import 'mixins_and_variables_and_functions';
$new-namespace-panel-illustration-width: 215px;
$new-namespace-panel-height: 240px;
.new-namespace-panel-illustration {
width: $new-namespace-panel-illustration-width;
}
.new-namespace-panel-wrapper {
@include media-breakpoint-down(md) {
width: 100%;
}
width: 50%;
}
.new-namespace-panel {
&:hover {
background-color: $gray-10;
}
color: $purple-700;
min-height: $new-namespace-panel-height;
text-align: center;
@include media-breakpoint-up(lg) {
text-align: left;
}
}

View File

@ -217,7 +217,6 @@
.title {
color: $gl-text-color;
margin-bottom: $gl-padding-4;
line-height: $gl-line-height-20;
.avatar {

View File

@ -85,7 +85,7 @@ class ProjectsController < Projects::ApplicationController
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
render 'new', locals: { active_tab: active_new_project_tab }
render 'new'
end
end

View File

@ -123,9 +123,7 @@ module GroupsHelper
@has_group_title = true
full_title = []
ancestors = group.ancestors.with_route
ancestors.reverse_each.with_index do |parent, index|
sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index|
if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else
@ -294,11 +292,20 @@ module GroupsHelper
end
def oldest_consecutively_locked_ancestor(group)
group.ancestors.find do |group|
sorted_ancestors(group).find do |group|
!group.has_parent? || !group.parent.share_with_group_lock?
end
end
# Ancestors sorted by hierarchy depth in bottom-top order.
def sorted_ancestors(group)
if group.root_ancestor.use_traversal_ids?
group.ancestors(hierarchy_order: :asc)
else
group.ancestors
end
end
def default_help
s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.")
end

View File

@ -114,7 +114,7 @@ module RepositoryStorageMovable
private
def container_repository_writable
add_error(_('is read only')) if container&.repository_read_only?
add_error(_('is read-only')) if container&.repository_read_only?
end
def error_key

View File

@ -839,7 +839,12 @@ class Group < Namespace
end
def uncached_ci_variables_for(ref, project, environment: nil)
list_of_ids = [self] + ancestors
list_of_ids = if root_ancestor.use_traversal_ids?
[self] + ancestors(hierarchy_order: :asc)
else
[self] + ancestors
end
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)

View File

@ -123,7 +123,7 @@ class Issue < ApplicationRecord
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
scope :with_api_entity_associations, -> {
preload(:timelogs, :closed_by, :assignees, :author, :notes, :labels,
preload(:timelogs, :closed_by, :assignees, :author, :labels,
milestone: { project: [:route, { namespace: :route }] },
project: [:route, { namespace: :route }])
}

View File

@ -58,11 +58,18 @@ module Namespaces
end
def self_and_descendants
if use_traversal_ids?
lineage(self)
else
super
end
return super unless use_traversal_ids?
lineage(top: self)
end
def ancestors(hierarchy_order: nil)
return super() unless use_traversal_ids?
return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
return self.class.none if parent_id.blank?
lineage(bottom: parent, hierarchy_order: hierarchy_order)
end
private
@ -84,11 +91,29 @@ module Namespaces
end
# Search this namespace's lineage. Bound inclusively by top node.
def lineage(top)
raise UnboundedSearch, 'Must bound search by a top' unless top
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
without_sti_condition
.traversal_ids_contains("{#{top.id}}")
skope = without_sti_condition
if top
skope = skope.traversal_ids_contains("{#{top.id}}")
end
if bottom
skope = skope.where(id: bottom.traversal_ids[0..-1])
end
# The original `with_depth` attribute in ObjectHierarchy increments as you
# walk away from the "base" namespace. This direction changes depending on
# if you are walking up the ancestors or down the descendants.
if hierarchy_order
depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
.order(depth: hierarchy_order)
end
skope
end
end
end

View File

@ -268,7 +268,7 @@ class Service < ApplicationRecord
private_class_method :instance_level_integration
def self.create_from_active_default_integrations(scope, association, with_templates: false)
group_ids = scope.ancestors.select(:id)
group_ids = sorted_ancestors(scope).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
from_union([
@ -459,6 +459,15 @@ class Service < ApplicationRecord
private
# Ancestors sorted by hierarchy depth in bottom-top order.
def self.sorted_ancestors(scope)
if scope.root_ancestor.use_traversal_ids?
Namespace.from(scope.ancestors(hierarchy_order: :asc))
else
scope.ancestors
end
end
def validate_is_instance_or_template
errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level?
end

View File

@ -54,7 +54,7 @@ module Clusters
##
# If we haven't created a provider record yet,
# we restrict ourselves to read only access so
# we restrict ourselves to read-only access so
# that we can safely expose credentials to the
# frontend (to be used when populating the
# creation form).

View File

@ -2,4 +2,4 @@
.text-warning.center.prepend-top-20
%p
= sprite_icon('warning-solid')
= _('Archived project! Repository and other project resources are read only')
= _('Archived project! Repository and other project resources are read-only')

View File

@ -2,80 +2,38 @@
- @hide_top_links = true
- page_title _('New Project')
- header_title _("Projects"), dashboard_projects_path
- active_tab = local_assigns.fetch(:active_tab, 'blank')
- add_page_specific_style 'page_bundles/new_namespace'
.project-edit-container.gl-mt-5
.project-edit-errors
= render 'projects/errors'
.js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any?, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } }
.js-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?).to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } }
.row{ 'v-cloak': true }
.col-lg-3.profile-settings-sidebar
%h4.gl-mt-0
= _('New project')
%p
- among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "project-features"), target: '_blank'
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
= render_if_exists 'projects/new_ci_cd_banner_external_repo'
%p
- pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/index", anchor: "getting-started"), target: '_blank'
= _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
.md
= brand_new_project_guidelines
%p
%strong= _("Tip:")
= _("You can also create a project from the command line.")
#blank-project-pane.tab-pane.active
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
.col-lg-9.js-toggle-container
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', experiment_track_label: 'blank_project' }, role: 'tab' }
%span.d-none.d-sm-block= s_('ProjectsNew|Blank project')
%span.d-block.d-sm-none= s_('ProjectsNew|Blank')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', experiment_track_label: 'create_from_template' }, role: 'tab' }
%span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template')
%span.d-block.d-sm-none= s_('ProjectsNew|Template')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', experiment_track_label: 'import_project' }, role: 'tab' }
%span.d-none.d-sm-block= s_('ProjectsNew|Import project')
%span.d-block.d-sm-none= s_('ProjectsNew|Import')
= render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
.tab-content.gitlab-tab-content
.tab-pane.js-toggle-container{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
.card.card-slim.m-4.p-4
#create-from-template-pane.tab-pane
.gl-card.gl-my-5
.gl-card-body
%div
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
= render 'project_templates', f: f, project: @project
= render 'project_templates', f: f, project: @project
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- if import_sources_enabled?
= render 'import_project_pane', active_tab: active_tab
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
%p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
#import-project-pane.tab-pane.js-toggle-container
- if import_sources_enabled?
= render 'import_project_pane'
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
%p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
= render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab
.save-project-loader.d-none
.center
%h2
.gl-spinner.gl-spinner-md.align-text-bottom
= s_('ProjectsNew|Creating project & repository.')
%p
= s_('ProjectsNew|Please wait a moment, this page will automatically refresh when ready.')
= render_if_exists 'projects/new_ci_cd_only_project_pane'

View File

@ -14,7 +14,7 @@
method: :post, class: "gl-button btn btn-confirm"
- else
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') }
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
%p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "gl-button btn btn-warning"

View File

@ -0,0 +1,5 @@
---
title: Improve performance of project issues API
merge_request: 60981
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Remove subscribed field from todos, related issues, and epic issues API
merge_request: 60981
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Make new project ui the only option
merge_request: 59452
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Store slice multiplier and max slices running for reindex in DB
merge_request: 60861
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Linear traversal query for Namespace#ancestors
merge_request: 57137
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Polish the "What's new" UI
merge_request: 60804
author: Kev @KevSlashNull
type: changed

View File

@ -0,0 +1,5 @@
---
title: Merge branch 'mo-update-artifact-documentation' into 'master'
merge_request: 61084
author:
type: other

View File

@ -207,6 +207,7 @@ module Gitlab
config.assets.precompile << "page_bundles/merge_conflicts.css"
config.assets.precompile << "page_bundles/merge_requests.css"
config.assets.precompile << "page_bundles/milestone.css"
config.assets.precompile << "page_bundles/new_namespace.css"
config.assets.precompile << "page_bundles/oncall_schedules.css"
config.assets.precompile << "page_bundles/pipeline.css"
config.assets.precompile << "page_bundles/pipeline_schedules.css"

View File

@ -0,0 +1,8 @@
---
name: use_traversal_ids_for_ancestors
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57137
rollout_issue_url:
milestone: '13.12'
type: development
group: group::access
default_enabled: false

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class AddSliceMultiplierAndMaxSlicesToElasticReindexingTask < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DEFAULT_MAX_TOTAL_SLICES_RUNNING = 60
DEFAULT_SLICE_MULTIPLIER = 2
def change
add_column :elastic_reindexing_tasks, :max_slices_running, :integer,
limit: 2,
default: DEFAULT_MAX_TOTAL_SLICES_RUNNING,
null: false
add_column :elastic_reindexing_tasks, :slice_multiplier, :integer,
limit: 2,
default: DEFAULT_SLICE_MULTIPLIER,
null: false
end
end

View File

@ -0,0 +1 @@
08f4cd1f8f7ddc336d0edee7581b0cb59e0cdc7b5f3cbeb1ccdcd7a1c52d366f

View File

@ -12447,6 +12447,8 @@ CREATE TABLE elastic_reindexing_tasks (
error_message text,
documents_count_target integer,
delete_original_index_at timestamp with time zone,
max_slices_running smallint DEFAULT 60 NOT NULL,
slice_multiplier smallint DEFAULT 2 NOT NULL,
CONSTRAINT check_04151aca42 CHECK ((char_length(index_name_from) <= 255)),
CONSTRAINT check_7f64acda8e CHECK ((char_length(error_message) <= 255)),
CONSTRAINT check_85ebff7124 CHECK ((char_length(index_name_to) <= 255)),

View File

@ -267,9 +267,9 @@ dashboard in your browser.
Failures that happen during a backfill are scheduled to be retried at the end
of the backfill.
## Remove Geo node
## Remove Geo site
For more information on removing a Geo node, see [Removing **secondary** Geo nodes](replication/remove_geo_node.md).
For more information on removing a Geo node, see [Removing **secondary** Geo nodes](replication/remove_geo_site.md).
## Disable Geo

View File

@ -287,7 +287,7 @@ Please note that disabling a **secondary** node stops the synchronization proces
Please note that if `git_data_dirs` is customized on the **primary** node for multiple
repository shards you must duplicate the same configuration on each **secondary** node.
Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md).
Point your users to the [Using a Geo Site guide](usage.md).
Currently, this is what is synced:

View File

@ -4,5 +4,5 @@ redirect_to: '../../geo/replication/remove_geo_site.md'
This document was moved to [another location](../../geo/replication/remove_geo_site.md).
<!-- This redirect file can be deleted after 2022-04-01 -->
<!-- This redirect file can be deleted after 2021-06-01 -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -4,5 +4,5 @@ redirect_to: '../../geo/replication/usage.md'
This document was moved to [another location](../../geo/replication/usage.md).
<!-- This redirect file can be deleted after 2022-04-01 -->
<!-- This redirect file can be deleted after 2022-06-01 -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -25,7 +25,7 @@ If you installed GitLab using the Omnibus packages (highly recommended):
1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** nodes.
1. [Configure GitLab](../replication/configuration.md) to set the **primary** and **secondary** nodes.
1. Optional: [Configure a secondary LDAP server](../../auth/ldap/index.md) for the **secondary** node. See [notes on LDAP](../index.md#ldap).
1. [Follow the "Using a Geo Server" guide](../replication/using_a_geo_server.md).
1. Follow the [Using a Geo Site](../replication/usage.md) guide.
## Post-installation documentation

View File

@ -108,9 +108,9 @@ The availability objectives for Gitaly clusters are:
Gitaly Cluster supports:
- [Strong consistency](praefect.md#strong-consistency) of the secondary replicas.
- [Automatic failover](praefect.md#automatic-failover-and-leader-election) from the primary to the secondary.
- [Automatic failover](praefect.md#automatic-failover-and-primary-election-strategies) from the primary to the secondary.
- Reporting of possible data loss if replication queue is non-empty.
- Marking repositories as [read only](praefect.md#read-only-mode) if data loss is detected to prevent data inconsistencies.
- Marking repositories as [read-only](praefect.md#read-only-mode) if data loss is detected to prevent data inconsistencies.
Follow the [Gitaly Cluster epic](https://gitlab.com/groups/gitlab-org/-/epics/1489)
for improvements including
@ -248,10 +248,10 @@ Gitaly Cluster and [Geo](../geo/index.md) both provide redundancy. However the r
The following table outlines the major differences between Gitaly Cluster and Geo:
| Tool | Nodes | Locations | Latency tolerance | Failover | Consistency | Provides redundancy for |
|:---------------|:---------|:----------|:-------------------|:----------------------------------------------------------------|:-----------------------------------------|:------------------------|
| Gitaly Cluster | Multiple | Single | Approximately 1 ms | [Automatic](praefect.md#automatic-failover-and-leader-election) | [Strong](praefect.md#strong-consistency) | Data storage in Git |
| Geo | Multiple | Multiple | Up to one minute | [Manual](../geo/disaster_recovery/index.md) | Eventual | Entire GitLab instance |
| Tool | Nodes | Locations | Latency tolerance | Failover | Consistency | Provides redundancy for |
|:---------------|:---------|:----------|:-------------------|:----------------------------------------------------------------------------|:-----------------------------------------|:------------------------|
| Gitaly Cluster | Multiple | Single | Approximately 1 ms | [Automatic](praefect.md#automatic-failover-and-primary-election-strategies) | [Strong](praefect.md#strong-consistency) | Data storage in Git |
| Geo | Multiple | Multiple | Up to one minute | [Manual](../geo/disaster_recovery/index.md) | Eventual | Entire GitLab instance |
For more information, see:

View File

@ -1079,32 +1079,33 @@ You can configure:
current assignments: gitaly-1, gitaly-2
```
## Automatic failover and leader election
## Automatic failover and primary election strategies
Praefect regularly checks the health of each backend Gitaly node. This
information can be used to automatically failover to a new primary node if the
current primary node is found to be unhealthy.
Praefect regularly checks the health of each Gitaly node. This is used to automatically fail over
to a newly-elected primary Gitaly node if the current primary node is found to be unhealthy.
### Election strategies
We recommend using [repository-specific primary nodes](#repository-specific-primary-nodes),
which is [planned to be the only available election strategy](https://gitlab.com/gitlab-org/gitaly/-/issues/3574)
We recommend using [repository-specific primary nodes](#repository-specific-primary-nodes). This is
[planned to be the only available election strategy](https://gitlab.com/gitlab-org/gitaly/-/issues/3574)
from GitLab 14.0.
In the future, we are likely to implement support for:
### Repository-specific primary nodes
- A [Consul](../consul.md) strategy.
- A cloud-native strategy.
> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/3492) in GitLab 13.12.
#### Repository-specific primary nodes
Gitaly Cluster supports electing repository-specific primary Gitaly nodes. Repository-specific
Gitaly primary nodes are enabled in `/etc/gitlab/gitlab.rb` by setting
`praefect['failover_election_strategy'] = 'per_repository'`.
Praefect's earlier election strategies elected a primary for each virtual storage, which was used as the
primary for each repository in the virtual storage. This model prevented horizontal scaling of a virtual
storage. The primary Gitaly node needed a replica of each repository and thus became the bottleneck.
Praefect's [deprecated election strategies](#deprecated-election-strategies):
The `per_repository` election strategy solves this problem by electing a primary separately for each repository.
Combined with [configurable replication factors](#configure-replication-factor), you can horizontally
scale storage capacity and distribute write load across Gitaly nodes.
- Elected a primary Gitaly node for each virtual storage, which was used as the primary node for
each repository in the virtual storage.
- Prevented horizontal scaling of a virtual storage. The primary Gitaly node needed a replica of
each repository and thus became the bottleneck.
The `per_repository` election strategy solves this problem by electing a primary Gitaly node separately for each
repository. Combined with [configurable replication factors](#configure-replication-factor), you can
horizontally scale storage capacity and distribute write load across Gitaly nodes.
Primary elections are run when:
@ -1128,16 +1129,13 @@ If there are no healthy secondary nodes for a repository:
- The unhealthy primary node is demoted and the repository is left without a primary node.
- Operations that require a primary node fail until a primary is successfully elected.
Repository-specific primaries are enabled in `/etc/gitlab/gitlab.rb` by setting
`praefect['failover_election_strategy'] = 'per_repository'`.
#### Migrate to repository-specific primary Gitaly nodes
##### Migrate to repository-specific primary nodes
New Gitaly clusters can start using the `per_repository` election strategy immediately.
New Gitaly Clusters can start using the `per_repository` election strategy immediately.
To migrate existing clusters:
1. Praefect didn't historically keep database records of every repository stored on the cluster. When
1. Praefect nodes didn't historically keep database records of every repository stored on the cluster. When
the `per_repository` election strategy is configured, Praefect expects to have database records of
each repository. A [background migration](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/2749) is
included in GitLab 13.6 and later to create any missing database records for repositories. Before migrating
@ -1161,35 +1159,39 @@ To migrate existing clusters:
The migration is ran when Praefect starts up. If the migration is unsuccessful, you can restart
a Praefect node to reattempt it. The migration only runs with `sql` election strategy configured.
1. Running two different election strategies side by side can cause a split brain, where different Praefects
consider repositories to have different primaries. To avoid this, all Praefects should be shut down prior
to changing the election strategy.
1. Running two different election strategies side by side can cause a split brain, where different
Praefect nodes consider repositories to have different primaries. To avoid this, shut down
all Praefect nodes before changing the election strategy.
This can be done by running `gitlab-ctl stop praefect` on the Praefect nodes.
Do this by running `gitlab-ctl stop praefect` on the Praefect nodes.
1. On the Praefect nodes, configure the election strategy in `/etc/gitlab/gitlab.rb` with
`praefect['failover_election_strategy'] = 'per_repository'`.
1. Finally, run `gitlab-ctl reconfigure` to reconfigure and restart the Praefects.
1. Finally, run `gitlab-ctl reconfigure` to reconfigure and restart the Praefect nodes.
#### Deprecated election strategies
### Deprecated election strategies
WARNING:
The below election strategies are deprecated and are scheduled for removal in GitLab 14.0.
Migrate to [repository-specific primary nodes](#repository-specific-primary-nodes).
- **PostgreSQL:** Enabled by default until GitLab 14.0, and equivalent to:
`praefect['failover_election_strategy'] = 'sql'`. This configuration
option allows multiple Praefect nodes to coordinate via the
PostgreSQL database to elect a primary Gitaly node. This configuration
causes Praefect nodes to elect a new primary, monitor its health,
and elect a new primary if the current one has not been reachable in
10 seconds by a majority of the Praefect nodes.
`praefect['failover_election_strategy'] = 'sql'`.
This configuration option:
- Allows multiple Praefect nodes to coordinate via the PostgreSQL database to elect a primary
Gitaly node.
- Causes Praefect nodes to elect a new primary Gitaly node, monitor its health, and elect a new primary
Gitaly node if the current one is not reached within 10 seconds by a majority of the Praefect
nodes.
- **Memory:** Enabled by setting `praefect['failover_election_strategy'] = 'local'`
in `/etc/gitlab/gitlab.rb` on the Praefect node. If a sufficient number of health
checks fail for the current primary backend Gitaly node, and new primary will
be elected. **Do not use with multiple Praefect nodes!** Using with multiple
Praefect nodes is likely to result in a split brain.
in `/etc/gitlab/gitlab.rb` on the Praefect node.
If a sufficient number of health checks fail for the current primary Gitaly node, a new primary is
elected. **Do not use with multiple Praefect nodes!** Using with multiple Praefect nodes is
likely to result in a split brain.
## Primary Node Failure
@ -1438,7 +1440,7 @@ GitLab repositories can be associated with projects, groups, and snippets. Each
have a separate API to schedule the respective repositories to move. To move all repositories
on a GitLab instance, each of these types must be scheduled to move for each storage.
Each repository is made read only for the duration of the move. The repository is not writable
Each repository is made read-only for the duration of the move. The repository is not writable
until the move has completed.
After creating and configuring Gitaly Cluster:

View File

@ -947,7 +947,7 @@ To enable the read-only mode:
sudo gitlab-ctl reconfigure
```
This command sets the Container Registry into the read only mode.
This command sets the Container Registry into the read-only mode.
1. Next, trigger one of the garbage collect commands:

View File

@ -120,7 +120,7 @@ Few notes on the service itself:
- The service runs under a system account, by default `gitlab-consul`.
- If you are using a different username, you will have to specify it. We
will refer to it with `CONSUL_USERNAME`,
- There will be a database user created with read only access to the repmgr
- There will be a database user created with read-only access to the repmgr
database
- Passwords will be stored in the following locations:
- `/etc/gitlab/gitlab.rb`: hashed

View File

@ -107,7 +107,6 @@ Example response:
"award_emoji": "http://localhost:3001/api/v4/projects/8/issues/6/award_emoji",
"project": "http://localhost:3001/api/v4/projects/8"
},
"subscribed": true,
"epic_issue_id": 2
}
]

View File

@ -51,7 +51,6 @@ Parameters:
"description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z",
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
"due_date": null,
"web_url": "http://example.com/example/example/issues/14",

View File

@ -1,8 +0,0 @@
---
redirect_to: 'dora4_project_analytics.md'
---
This document was moved to [another location](dora4_project_analytics.md).
<!-- This redirect file can be deleted after <2021-04-25>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -102,7 +102,6 @@ Example Response:
},
"merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7",
@ -176,7 +175,6 @@ Example Response:
},
"merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7",

View File

@ -1,8 +0,0 @@
---
redirect_to: '../README.md#contributed-examples'
---
This document was moved to [another location](../README.md#contributed-examples).
<!-- This redirect file can be deleted after 2021-04-18. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../README.md#contributed-examples'
---
This document was moved to [another location](../README.md#contributed-examples).
<!-- This redirect file can be deleted after 2021-04-18. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../README.md#contributed-examples'
---
This document was moved to [another location](../README.md#contributed-examples).
<!-- This redirect file can be deleted after 2021-04-19. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: 'README.md#contributed-examples'
---
This document was moved to [another location](README.md#contributed-examples).
<!-- This redirect file can be deleted after 2021-04-18. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../README.md'
---
This example is no longer available. [View other examples](../README.md).
<!-- This redirect file can be deleted after <2021-04-05>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: 'README.md'
---
This documentation page was removed. For information about variables, see [GitLab CI/CD environment variables](README.md)
<!-- This redirect file can be deleted after 2021-04-14. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../pipeline_editor/index.md#visualize-ci-configuration'
---
This document was moved to [another location](../pipeline_editor/index.md#visualize-ci-configuration).
<!-- This redirect file can be deleted after 2021-04-13. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../snowplow/index.md'
---
This document was moved to [another location](../snowplow/index.md).
<!-- This redirect file can be deleted after April 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../usage_ping/index.md'
---
This document was moved to [another location](../usage_ping/index.md).
<!-- This redirect file can be deleted after April 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -4692,7 +4692,7 @@ Tiers: `free`, `premium`, `ultimate`
Projects with repository mirroring enabled
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181920_projects_mirrored_with_pipelines_enabled.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216181920_projects_mirrored_with_pipelines_enabled.yml)
Group: `group::continuous integration`
@ -10532,6 +10532,30 @@ Status: `implemented`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.epics_usage.g_project_management_users_removing_epic_emoji_monthly`
Counts of MAU removing emoji on epic
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210505071850_g_project_management_users_removing_epic_emoji_monthly.yml)
Group: `group::product planning`
Status: `implemented`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.epics_usage.g_project_management_users_removing_epic_emoji_weekly`
Counts of WAU removing emoji on epic
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210505071932_g_project_management_users_removing_epic_emoji_weekly.yml)
Group: `group::product planning`
Status: `implemented`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_confidential_monthly`
Count of MAU making epics confidential
@ -16360,7 +16384,7 @@ Tiers: `free`
Count creator_id from projects with repository mirroring enabled.
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181934_projects_mirrored_with_pipelines_enabled.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216181934_projects_mirrored_with_pipelines_enabled.yml)
Group: `group::continuous integration`
@ -18280,7 +18304,7 @@ Tiers: `free`
Count creator_id from projects with repository mirroring enabled.
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216181943_projects_mirrored_with_pipelines_enabled.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210216181943_projects_mirrored_with_pipelines_enabled.yml)
Group: `group::continuous integration`

View File

@ -1,8 +0,0 @@
---
redirect_to: 'paging.md'
---
This document was moved to [another location](paging.md).
<!-- This redirect file can be deleted after 2021-04-21 -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../../user/project/clusters/protect/web_application_firewall/index.md'
---
This document was moved to [another location](../../user/project/clusters/protect/web_application_firewall/index.md).
<!-- This redirect file can be deleted after <2021-04-01>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: '../../user/project/clusters/protect/web_application_firewall/quick_start_guide.md'
---
This document was moved to [another location](../../user/project/clusters/protect/web_application_firewall/quick_start_guide.md).
<!-- This redirect file can be deleted after <2021-04-01>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: 'protect/index.md'
---
This document was moved to [another location](protect/index.md).
<!-- This redirect file can be deleted after <2021-04-01>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -15,7 +15,7 @@ Although branching strategies usually work well enough for source code and
plain text because different versions can be merged together, they do not work
for binary files.
When file locking is setup, lockable files are **read only** by default.
When file locking is setup, lockable files are **read-only** by default.
When a file is locked, only the user who locked the file may modify it. This
user is said to "hold the lock" or have "taken the lock", since only one user

View File

@ -41,7 +41,8 @@ For a list of words that can't be used as project names see
To create a new blank project on the **New project** page:
1. On the **Blank project** tab, provide the following information:
1. Click **Create blank project**
1. Provide the following information:
- The name of your project in the **Project name** field. You can't use
special characters, but you can use spaces, hyphens, underscores, or even
emoji. When adding the name, the **Project slug** auto populates.
@ -86,7 +87,8 @@ Built-in templates are project templates that are:
To use a built-in template on the **New project** page:
1. On the **Create from template** tab, select the **Built-in** tab.
1. Click **Create from template**
1. Select the **Built-in** tab.
1. From the list of available built-in templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.
@ -99,7 +101,8 @@ GitLab is developing Enterprise templates to help you streamline audit managemen
To create a new project with an Enterprise template, on the **New project** page:
1. On the **Create from template** tab, select the **Built-in** tab.
1. Click **Create from template**
1. Select the **Built-in** tab.
1. From the list of available built-in Enterprise templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.
@ -123,11 +126,12 @@ quickly starting projects.
Custom projects are available at the [instance-level](../../user/admin_area/custom_project_templates.md)
from the **Instance** tab, or at the [group-level](../../user/group/custom_project_templates.md)
from the **Group** tab, under the **Create from template** tab.
from the **Group** tab, on the **Create from template** page.
To use a custom project template on the **New project** page:
1. On the **Create from template** tab, select the **Instance** tab or the **Group** tab.
1. Click **Create from template**
1. Select the **Instance** tab or the **Group** tab.
1. From the list of available custom templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.

View File

@ -21,12 +21,12 @@ module API
related_issues = source_issue.related_issues(current_user) do |issues|
issues.with_api_entity_associations.preload_awardable
end
related_issues.each { |issue| issue.lazy_subscription(current_user, user_project) } # preload subscriptions
present related_issues,
with: Entities::RelatedIssue,
current_user: current_user,
project: user_project
project: user_project,
include_subscribed: false
end
desc 'Relate issues' do

View File

@ -79,7 +79,7 @@ module API
next unless collection
targets = collection.map(&:target)
options[type] = { issuable_metadata: Gitlab::IssuableMetadata.new(current_user, targets).data }
options[type] = { issuable_metadata: Gitlab::IssuableMetadata.new(current_user, targets).data, include_subscribed: false }
end
end
end

View File

@ -56,6 +56,6 @@ apply:
- terraform apply -input=false $PLAN
dependencies:
- plan
when: manual
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual

View File

@ -11,7 +11,7 @@ module Gitlab
# This query will read each element in the index matching the project_id filter.
# If for a project_id has 100_000 issues, all 100_000 elements will be read.
#
# A loose index scan will read only one entry from the index for each project_id to reduce the number of disk reads.
# A loose index scan will only read one entry from the index for each project_id to reduce the number of disk reads.
#
# Usage:
#

View File

@ -63,6 +63,12 @@
aggregation: daily
feature_flag: track_epics_activity
- name: g_project_management_users_removing_epic_emoji
category: epics_usage
redis_slot: project_management
aggregation: daily
feature_flag: track_epics_activity
# start date events
- name: g_project_management_users_setting_epic_start_date_as_fixed

View File

@ -1463,9 +1463,6 @@ msgstr ""
msgid "A project containing issues for each audit inquiry in the HIPAA Audit Protocol published by the U.S. Department of Health & Human Services"
msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
msgid "A projects repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"
msgstr ""
@ -3184,9 +3181,6 @@ msgstr ""
msgid "All epics"
msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
msgid "All groups and projects"
msgstr ""
@ -3502,6 +3496,9 @@ msgstr ""
msgid "An error occurred while fetching markdown preview"
msgstr ""
msgid "An error occurred while fetching participants"
msgstr ""
msgid "An error occurred while fetching participants."
msgstr ""
@ -3595,9 +3592,6 @@ msgstr ""
msgid "An error occurred while loading merge requests."
msgstr ""
msgid "An error occurred while loading project creation UI"
msgstr ""
msgid "An error occurred while loading the access tokens form, please try again."
msgstr ""
@ -4170,16 +4164,13 @@ msgstr ""
msgid "Archived in this version"
msgstr ""
msgid "Archived project! Repository and other project resources are read only"
msgstr ""
msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Archived projects"
msgstr ""
msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}"
msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Are you ABSOLUTELY SURE you wish to delete this project?"
@ -5592,9 +5583,6 @@ msgstr ""
msgid "CI/CD configuration file"
msgstr ""
msgid "CI/CD for external repo"
msgstr ""
msgid "CICDAnalytics|%{percent}%{percentSymbol}"
msgstr ""
@ -9250,9 +9238,6 @@ msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
msgid "Create a project pre-populated with the necessary files to get you started quickly."
msgstr ""
msgid "Create an account using:"
msgstr ""
@ -12989,9 +12974,6 @@ msgstr ""
msgid "Errors:"
msgstr ""
msgid "Escalation Policies"
msgstr ""
msgid "Escalation policies"
msgstr ""
@ -17356,9 +17338,6 @@ msgstr ""
msgid "Inform users without uploaded SSH keys that they can't push over SSH until one is added"
msgstr ""
msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}."
msgstr ""
msgid "Infrastructure"
msgstr ""
@ -20925,9 +20904,6 @@ msgstr ""
msgid "Middleman project with Static Site Editor support"
msgstr ""
msgid "Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab."
msgstr ""
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@ -23404,9 +23380,6 @@ msgstr ""
msgid "Pages Domain"
msgstr ""
msgid "Pages getting started guide"
msgstr ""
msgid "Pagination|Go to first page"
msgstr ""
@ -25828,12 +25801,6 @@ msgstr ""
msgid "ProjectsNew|Allows you to immediately clone this projects repository. Skip this if you plan to push up an existing repository."
msgstr ""
msgid "ProjectsNew|Blank"
msgstr ""
msgid "ProjectsNew|Blank project"
msgstr ""
msgid "ProjectsNew|Connect your external repository to GitLab CI/CD."
msgstr ""
@ -25846,6 +25813,9 @@ msgstr ""
msgid "ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things."
msgstr ""
msgid "ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly."
msgstr ""
msgid "ProjectsNew|Create blank project"
msgstr ""
@ -25858,9 +25828,6 @@ msgstr ""
msgid "ProjectsNew|Create new project"
msgstr ""
msgid "ProjectsNew|Creating project & repository."
msgstr ""
msgid "ProjectsNew|Description format"
msgstr ""
@ -25876,10 +25843,10 @@ msgstr ""
msgid "ProjectsNew|Initialize repository with a README"
msgstr ""
msgid "ProjectsNew|No import options available"
msgid "ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab."
msgstr ""
msgid "ProjectsNew|Please wait a moment, this page will automatically refresh when ready."
msgid "ProjectsNew|No import options available"
msgstr ""
msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}"
@ -25888,9 +25855,6 @@ msgstr ""
msgid "ProjectsNew|Run CI/CD for external repository"
msgstr ""
msgid "ProjectsNew|Template"
msgstr ""
msgid "ProjectsNew|Visibility Level"
msgstr ""
@ -31160,6 +31124,9 @@ msgstr ""
msgid "SuperSonics|Type"
msgstr ""
msgid "SuperSonics|Upload a legacy license"
msgstr ""
msgid "SuperSonics|Valid From"
msgstr ""
@ -33489,9 +33456,6 @@ msgstr[1] ""
msgid "Time|s"
msgstr ""
msgid "Tip:"
msgstr ""
msgid "Tip: Hover over a job to see the jobs it depends on to run."
msgstr ""
@ -37447,9 +37411,6 @@ msgstr ""
msgid "already shared with this group"
msgstr ""
msgid "among other things"
msgstr ""
msgid "and"
msgstr ""
@ -38103,7 +38064,7 @@ msgstr ""
msgid "is not valid. The iteration group has to match the iteration cadence group."
msgstr ""
msgid "is read only"
msgid "is read-only"
msgstr ""
msgid "is too long (%{current_value}). The maximum size is %{max_size}."

View File

@ -260,7 +260,6 @@ module QA
module Project
autoload :New, 'qa/page/project/new'
autoload :NewExperiment, 'qa/page/project/new_experiment'
autoload :Show, 'qa/page/project/show'
autoload :Activity, 'qa/page/project/activity'
autoload :Menu, 'qa/page/project/menu'

View File

@ -6,11 +6,7 @@ module QA
module_function
def go_to_create_project_from_template
if Page::Project::NewExperiment.perform(&:shown?)
Page::Project::NewExperiment.perform(&:click_create_from_template_link)
else
Page::Project::New.perform(&:click_create_from_template_tab)
end
Page::Project::New.perform(&:click_create_from_template_link)
end
end
end

View File

@ -8,11 +8,6 @@ module QA
include Page::Component::Select2
include Page::Component::VisibilitySetting
view 'app/views/projects/new.html.haml' do
element :project_create_from_template_tab
element :import_project_tab, "Import project" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
element :project_namespace_select
@ -29,6 +24,19 @@ module QA
element :template_option_row
end
view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do
element :blank_project_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
element :create_from_template_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
end
def click_blank_project_link
click_element :blank_project_link
end
def click_create_from_template_link
click_element :create_from_template_link
end
def choose_test_namespace
choose_namespace(Runtime::Namespace.path)
end
@ -60,6 +68,10 @@ module QA
click_element(:project_create_from_template_tab)
end
def set_visibility(visibility)
choose visibility.capitalize
end
def click_github_link
click_link 'GitHub'
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
module QA
module Page
module Project
class NewExperiment < Page::Base
view 'app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue' do
element :blank_project_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
element :create_from_template_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
end
def shown?
has_element? :blank_project_link
end
def click_blank_project_link
click_element :blank_project_link
end
def click_create_from_template_link
click_element :create_from_template_link
end
end
end
end
end

View File

@ -83,7 +83,7 @@ module QA
end
end
Page::Project::NewExperiment.perform(&:click_blank_project_link) if Page::Project::NewExperiment.perform(&:shown?)
Page::Project::New.perform(&:click_blank_project_link)
Page::Project::New.perform do |new_page|
new_page.choose_test_namespace

View File

@ -37,7 +37,7 @@ RSpec.describe 'Admin Appearance' do
expect_custom_sign_in_appearance(appearance)
end
it 'preview new project page appearance' do
it 'preview new project page appearance', :js do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
@ -86,10 +86,11 @@ RSpec.describe 'Admin Appearance' do
expect_custom_sign_in_appearance(appearance)
end
it 'custom new project page' do
it 'custom new project page', :js do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit new_project_path
find('[data-qa-selector="blank_project_link"]').click
expect_custom_new_project_appearance(appearance)
end

View File

@ -25,8 +25,8 @@ RSpec.describe 'New project', :js do
expect(page).to have_no_selector('a', text: 'New project/repository')
end
expect(page).to have_selector('.blank-state-title', text: 'Create blank project')
expect(page).to have_no_selector('.blank-state-title', text: 'Create blank project/repository')
expect(page).to have_selector('h3', text: 'Create blank project')
expect(page).to have_no_selector('h3', text: 'Create blank project/repository')
end
it 'when in candidate renders "project/repository"' do
@ -40,7 +40,7 @@ RSpec.describe 'New project', :js do
expect(page).to have_selector('a', text: 'New project/repository')
end
expect(page).to have_selector('.blank-state-title', text: 'Create blank project/repository')
expect(page).to have_selector('h3', text: 'Create blank project/repository')
end
context 'with combined_menu feature disabled' do
@ -240,7 +240,7 @@ RSpec.describe 'New project', :js do
find('[data-qa-selector="import_project_link"]').click
first('.js-import-git-toggle-button').click
page.within '.toggle-import-form' do
page.within '#import-project-pane' do
expect(page).not_to have_css('input#project_initialize_with_readme')
expect(page).not_to have_content('Initialize repository with a README')
end

View File

@ -90,7 +90,7 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(GlToggle).props('value')).toBe(true);
});
it('should alert users the flag is read only', () => {
it('should alert users the flag is read-only', () => {
expect(findAlert().text()).toContain('GitLab is moving to a new way of managing feature flags');
});

View File

@ -281,7 +281,7 @@ describe('feature flag form', () => {
});
});
it('renders read only name', () => {
it('renders read-only name', () => {
expect(wrapper.find('.js-scope-all').exists()).toEqual(true);
});
});

View File

@ -0,0 +1,77 @@
import { shallowMount } from '@vue/test-utils';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/pages/projects/new/components/app.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
describe('Experimental new project creation app', () => {
let wrapper;
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
const createComponent = (propsData) => {
wrapper = shallowMount(App, { propsData });
};
afterEach(() => {
wrapper.destroy();
});
describe('new_repo experiment', () => {
it('passes new_repo experiment', () => {
createComponent();
expect(findNewNamespacePage().props().experiment).toBe('new_repo');
});
describe('when in the candidate variant', () => {
assignGitlabExperiment('new_repo', 'candidate');
it('has "repository" in the panel title', () => {
createComponent();
expect(findNewNamespacePage().props().panels[0].title).toBe(
'Create blank project/repository',
);
});
});
describe('when in the control variant', () => {
assignGitlabExperiment('new_repo', 'control');
it('has "project" in the panel title', () => {
createComponent();
expect(findNewNamespacePage().props().panels[0].title).toBe('Create blank project');
});
});
});
it('passes custom new project guideline text to underlying component', () => {
const DEMO_GUIDELINES = 'Demo guidelines';
const guidelineSelector = '#new-project-guideline';
createComponent({
newProjectGuidelines: DEMO_GUIDELINES,
});
expect(wrapper.find(guidelineSelector).text()).toBe(DEMO_GUIDELINES);
});
it.each`
isCiCdAvailable | outcome
${false} | ${'do not show CI/CD panel'}
${true} | ${'show CI/CD panel'}
`('$outcome when isCiCdAvailable is $isCiCdAvailable', ({ isCiCdAvailable }) => {
createComponent({
isCiCdAvailable,
});
expect(
Boolean(
wrapper
.findComponent(NewNamespacePage)
.props()
.panels.find((p) => p.name === 'cicd_for_external_repo'),
),
).toBe(isCiCdAvailable);
});
});

View File

@ -1,6 +1,6 @@
import { GlPopover, GlFormInputGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue';
import NewProjectPushTipPopover from '~/pages/projects/new/components/new_project_push_tip_popover.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('New project push tip popover', () => {

View File

@ -123,11 +123,28 @@ describe('Pipeline editor branch switcher', () => {
describe('when switching branches', () => {
beforeEach(async () => {
jest.spyOn(window.history, 'pushState').mockImplementation(() => {});
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
createComponentWithApollo();
await waitForPromises();
});
it('updates session history when selecting a different branch', async () => {
const branch = findDropdownItems().at(1);
await branch.vm.$emit('click');
expect(window.history.pushState).toHaveBeenCalled();
expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`);
});
it('does not update session history when selecting current branch', async () => {
const branch = findDropdownItems().at(0);
await branch.vm.$emit('click');
expect(branch.text()).toBe(mockDefaultBranch);
expect(window.history.pushState).not.toHaveBeenCalled();
});
it('emits the refetchContent event when selecting a different branch', async () => {
const branch = findDropdownItems().at(1);

View File

@ -1,144 +0,0 @@
import { GlBreadcrumb } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/projects/experiment_new_project_creation/components/app.vue';
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
describe('Experimental new project creation app', () => {
let wrapper;
const createComponent = (propsData) => {
wrapper = shallowMount(App, { propsData });
};
afterEach(() => {
wrapper.destroy();
window.location.hash = '';
wrapper = null;
});
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findPanel = (panelName) =>
findWelcomePage()
.props()
.panels.find((p) => p.name === panelName);
const findPanelHeader = () => wrapper.find('h4');
describe('new_repo experiment', () => {
describe('when in the candidate variant', () => {
assignGitlabExperiment('new_repo', 'candidate');
it('has "repository" in the panel title', () => {
createComponent();
expect(findPanel('blank_project').title).toBe('Create blank project/repository');
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
createComponent();
});
it('renders "project/repository"', () => {
expect(findPanelHeader().text()).toBe('Create blank project/repository');
});
});
});
describe('when in the control variant', () => {
assignGitlabExperiment('new_repo', 'control');
it('has "project" in the panel title', () => {
createComponent();
expect(findPanel('blank_project').title).toBe('Create blank project');
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
createComponent();
});
it('renders "project"', () => {
expect(findPanelHeader().text()).toBe('Create blank project');
});
});
});
});
describe('with empty hash', () => {
beforeEach(() => {
createComponent();
});
it('renders welcome page', () => {
expect(wrapper.find(WelcomePage).exists()).toBe(true);
});
it('does not render breadcrumbs', () => {
expect(wrapper.find(GlBreadcrumb).exists()).toBe(false);
});
});
it('renders blank project container if there are errors', () => {
createComponent({ hasErrors: true });
expect(wrapper.find(WelcomePage).exists()).toBe(false);
expect(wrapper.find(LegacyContainer).exists()).toBe(true);
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
createComponent();
});
it('renders relevant container', () => {
expect(wrapper.find(WelcomePage).exists()).toBe(false);
expect(wrapper.find(LegacyContainer).exists()).toBe(true);
});
it('renders breadcrumbs', () => {
expect(wrapper.find(GlBreadcrumb).exists()).toBe(true);
});
});
describe('display custom new project guideline text', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
});
it('does not render new project guideline if undefined', () => {
createComponent();
expect(wrapper.find('div#new-project-guideline').exists()).toBe(false);
});
it('render new project guideline if defined', () => {
const guidelineSelector = 'div#new-project-guideline';
createComponent({
newProjectGuidelines: '<h4>Internal Guidelines</h4><p>lorem ipsum</p>',
});
expect(wrapper.find(guidelineSelector).exists()).toBe(true);
expect(wrapper.find(guidelineSelector).html()).toContain('<h4>Internal Guidelines</h4>');
expect(wrapper.find(guidelineSelector).html()).toContain('<p>lorem ipsum</p>');
});
});
it('renders relevant container when hash changes', () => {
createComponent();
expect(wrapper.find(WelcomePage).exists()).toBe(true);
window.location.hash = '#blank_project';
const ev = document.createEvent('HTMLEvents');
ev.initEvent('hashchange', false, false);
window.dispatchEvent(ev);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(WelcomePage).exists()).toBe(false);
expect(wrapper.find(LegacyContainer).exists()).toBe(true);
});
});
});

View File

@ -208,6 +208,22 @@ describe('Sidebar assignees widget', () => {
]);
});
it('does not trigger mutation or fire event when editing and exiting without making changes', async () => {
createComponent();
await waitForPromises();
findEditableItem().vm.$emit('open');
await waitForPromises();
findEditableItem().vm.$emit('close');
expect(findEditableItem().props('isDirty')).toBe(false);
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledTimes(0);
expect(wrapper.emitted('assignees-updated')).toBe(undefined);
});
describe('when expanded', () => {
beforeEach(async () => {
createComponent();

View File

@ -0,0 +1,89 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Participants from '~/sidebar/components/participants/participants.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
import { epicParticipantsResponse } from '../../mock_data';
Vue.use(VueApollo);
describe('Sidebar Participants Widget', () => {
let wrapper;
let fakeApollo;
const findParticipants = () => wrapper.findComponent(Participants);
const createComponent = ({
participantsQueryHandler = jest.fn().mockResolvedValue(epicParticipantsResponse()),
} = {}) => {
fakeApollo = createMockApollo([[epicParticipantsQuery, participantsQueryHandler]]);
wrapper = shallowMount(SidebarParticipantsWidget, {
apolloProvider: fakeApollo,
propsData: {
fullPath: 'group',
iid: '1',
issuableType: 'epic',
},
stubs: {
Participants,
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to child component when query is loading', () => {
createComponent();
expect(findParticipants().props('loading')).toBe(true);
});
describe('when participants are loaded', () => {
beforeEach(() => {
createComponent({
participantsQueryHandler: jest.fn().mockResolvedValue(epicParticipantsResponse()),
});
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findParticipants().props('loading')).toBe(false);
});
it('passes participants to child component', () => {
expect(findParticipants().props('participants')).toEqual(
epicParticipantsResponse().data.workspace.issuable.participants.nodes,
);
});
});
describe('when error occurs', () => {
it('emits error event with correct parameters', async () => {
const mockError = new Error('mayday');
createComponent({
participantsQueryHandler: jest.fn().mockRejectedValue(mockError),
});
await waitForPromises();
const [
[
{
message,
error: { networkError },
},
],
] = wrapper.emitted('fetch-error');
expect(message).toBe(wrapper.vm.$options.i18n.fetchingError);
expect(networkError).toEqual(mockError);
});
});
});

View File

@ -262,6 +262,31 @@ export const issuableStartDateResponse = (startDate = null) => ({
},
});
export const epicParticipantsResponse = () => ({
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
participants: {
nodes: [
{
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
name: 'Jacki Kub',
username: 'francina.skiles',
webUrl: '/franc',
status: null,
},
],
},
},
},
},
});
export const issueReferenceResponse = (reference) => ({
data: {
workspace: {

Some files were not shown because too many files have changed in this diff Show More