Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
829e846dd5
commit
ed00b1a6a3
|
@ -1 +1 @@
|
|||
1833948feca92eab5791a4358f862b9b9d6b680d
|
||||
4bb38a198255d9b898763eedfd64508b72af7b3b
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'DeleteButton',
|
||||
components: {
|
||||
GlDeprecatedButton,
|
||||
GlButton,
|
||||
GlModal,
|
||||
},
|
||||
directives: {
|
||||
|
@ -25,7 +26,12 @@ export default {
|
|||
buttonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
default: 'info',
|
||||
},
|
||||
buttonSize: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'medium',
|
||||
},
|
||||
hasSelectedDesigns: {
|
||||
type: Boolean,
|
||||
|
@ -38,27 +44,38 @@ export default {
|
|||
modalId: uniqueId('design-deletion-confirmation-'),
|
||||
};
|
||||
},
|
||||
modal: {
|
||||
title: s__('DesignManagement|Delete designs confirmation'),
|
||||
actionPrimary: {
|
||||
text: s__('Delete'),
|
||||
attributes: { variant: 'danger' },
|
||||
},
|
||||
actionCancel: {
|
||||
text: s__('Cancel'),
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-display-flex gl-align-items-center gl-h-full">
|
||||
<gl-modal
|
||||
:modal-id="modalId"
|
||||
:title="s__('DesignManagement|Delete designs confirmation')"
|
||||
:ok-title="s__('DesignManagement|Delete')"
|
||||
ok-variant="danger"
|
||||
:title="$options.modal.title"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
@ok="$emit('deleteSelectedDesigns')"
|
||||
>
|
||||
<p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
|
||||
</gl-modal>
|
||||
<gl-deprecated-button
|
||||
<gl-button
|
||||
v-gl-modal-directive="modalId"
|
||||
:variant="buttonVariant"
|
||||
:disabled="isDeleting || !hasSelectedDesigns"
|
||||
:size="buttonSize"
|
||||
:class="buttonClass"
|
||||
:disabled="isDeleting || !hasSelectedDesigns"
|
||||
>
|
||||
<slot></slot>
|
||||
</gl-deprecated-button>
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -13,13 +13,14 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
iid: {
|
||||
type: String,
|
||||
required: true,
|
||||
from: 'issueIid',
|
||||
defaut: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -60,7 +60,7 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
if (this.isNoteLinked) {
|
||||
this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
||||
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -80,7 +80,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
|
||||
<timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
|
||||
<user-avatar-link
|
||||
:link-href="author.webUrl"
|
||||
:img-src="author.avatarUrl"
|
||||
|
|
|
@ -127,7 +127,7 @@ export default {
|
|||
params: { id: filename },
|
||||
query: $route.query,
|
||||
}"
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item"
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
>
|
||||
<div class="card-body p-0 d-flex-center overflow-hidden position-relative">
|
||||
<div v-if="icon.name" class="design-event position-absolute">
|
||||
|
|
|
@ -6,7 +6,6 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
|
|||
import Pagination from './pagination.vue';
|
||||
import DeleteButton from '../delete_button.vue';
|
||||
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
|
||||
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
|
||||
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
|
||||
|
||||
export default {
|
||||
|
@ -55,19 +54,17 @@ export default {
|
|||
permissions: {
|
||||
createDesign: false,
|
||||
},
|
||||
projectPath: '',
|
||||
issueIid: null,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
manual: true,
|
||||
result({ data: { projectPath, issueIid } }) {
|
||||
this.projectPath = projectPath;
|
||||
this.issueIid = issueIid;
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
default: '',
|
||||
},
|
||||
issueIid: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
permissions: {
|
||||
query: permissionsQuery,
|
||||
variables() {
|
||||
|
@ -102,6 +99,7 @@ export default {
|
|||
query: $route.query,
|
||||
}"
|
||||
:aria-label="s__('DesignManagement|Go back to designs')"
|
||||
data-testid="close-design"
|
||||
class="mr-3 text-plain d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<icon :size="18" name="close" />
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedButton,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
|
@ -30,7 +30,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<gl-deprecated-button
|
||||
<gl-button
|
||||
v-gl-tooltip.hover
|
||||
:title="
|
||||
s__(
|
||||
|
@ -38,12 +38,13 @@ export default {
|
|||
)
|
||||
"
|
||||
:disabled="isSaving"
|
||||
variant="success"
|
||||
variant="default"
|
||||
size="small"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
{{ s__('DesignManagement|Upload designs') }}
|
||||
<gl-loading-icon v-if="isSaving" inline class="ml-1" />
|
||||
</gl-deprecated-button>
|
||||
</gl-button>
|
||||
|
||||
<input
|
||||
ref="fileUpload"
|
||||
|
|
|
@ -12,6 +12,12 @@ export default {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
hasDesigns: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragCounter: 0,
|
||||
|
@ -22,6 +28,12 @@ export default {
|
|||
dragging() {
|
||||
return this.dragCounter !== 0;
|
||||
},
|
||||
iconStyles() {
|
||||
return {
|
||||
size: this.hasDesigns ? 24 : 16,
|
||||
class: this.hasDesigns ? 'gl-mb-2' : 'gl-mr-3 gl-text-gray-700',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isValidUpload(files) {
|
||||
|
@ -76,25 +88,21 @@ export default {
|
|||
>
|
||||
<slot>
|
||||
<button
|
||||
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
|
||||
class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
<div class="d-flex-center flex-column text-center">
|
||||
<gl-icon name="doc-new" :size="48" class="mb-4" />
|
||||
<p>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__(
|
||||
'%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #lineOne="{ content }"
|
||||
><span class="d-block">{{ content }}</span>
|
||||
</template>
|
||||
|
||||
<div
|
||||
:class="{ 'gl-flex-direction-column': hasDesigns }"
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center"
|
||||
data-testid="dropzone-area"
|
||||
>
|
||||
<gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" />
|
||||
<p class="gl-mb-0">
|
||||
<gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} designs to attach')">
|
||||
<template #link="{ content }">
|
||||
<gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
|
||||
<gl-link @click.stop="openFileUpload">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
@ -117,7 +125,7 @@ export default {
|
|||
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
|
||||
>
|
||||
<div v-show="!isDragDataValid" class="mw-50 text-center">
|
||||
<h3>{{ __('Oh no!') }}</h3>
|
||||
<h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3>
|
||||
<span>{{
|
||||
__(
|
||||
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
|
||||
|
@ -125,7 +133,7 @@ export default {
|
|||
}}</span>
|
||||
</div>
|
||||
<div v-show="isDragDataValid" class="mw-50 text-center">
|
||||
<h3>{{ __('Incoming!') }}</h3>
|
||||
<h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3>
|
||||
<span>{{ __('Drop your designs to start your upload.') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
|
||||
import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import allVersionsMixin from '../../mixins/all_versions';
|
||||
import { findVersionId } from '../../utils/design_management_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedDropdown,
|
||||
GlDeprecatedDropdownItem,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
},
|
||||
mixins: [allVersionsMixin],
|
||||
computed: {
|
||||
|
@ -50,8 +50,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-deprecated-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
|
||||
<gl-deprecated-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
|
||||
<gl-new-dropdown :text="dropdownText" size="small" class="design-version-dropdown">
|
||||
<gl-new-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
|
||||
<router-link
|
||||
class="d-flex js-version-link"
|
||||
:to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
|
||||
|
@ -71,6 +71,6 @@ export default {
|
|||
class="fa fa-check pull-right"
|
||||
></i>
|
||||
</router-link>
|
||||
</gl-deprecated-dropdown-item>
|
||||
</gl-deprecated-dropdown>
|
||||
</gl-new-dropdown-item>
|
||||
</gl-new-dropdown>
|
||||
</template>
|
||||
|
|
|
@ -1,32 +1,15 @@
|
|||
// This application is being moved, please do not touch this files
|
||||
// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
import apolloProvider from './graphql';
|
||||
import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
|
||||
import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-design-management');
|
||||
const badge = document.querySelector('.js-designs-count');
|
||||
const el = document.querySelector('.js-design-management-new');
|
||||
const { issueIid, projectPath, issuePath } = el.dataset;
|
||||
const router = createRouter(issuePath);
|
||||
|
||||
$('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
|
||||
if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
|
||||
router.push({ name: DESIGNS_ROUTE_NAME });
|
||||
} else if (id === 'discussion') {
|
||||
router.push({ name: ROOT_ROUTE_NAME });
|
||||
}
|
||||
});
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
data: {
|
||||
projectPath,
|
||||
issueIid,
|
||||
activeDiscussion: {
|
||||
__typename: 'ActiveDiscussion',
|
||||
id: null,
|
||||
|
@ -35,25 +18,14 @@ export default () => {
|
|||
},
|
||||
});
|
||||
|
||||
apolloProvider.clients.defaultClient
|
||||
.watchQuery({
|
||||
query: getDesignListQuery,
|
||||
variables: {
|
||||
fullPath: projectPath,
|
||||
iid: issueIid,
|
||||
atVersion: null,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (badge) {
|
||||
badge.textContent = data.project.issue.designCollection.designs.edges.length;
|
||||
}
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
router,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
projectPath,
|
||||
issueIid,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
},
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
|
||||
import appDataQuery from '../graphql/queries/app_data.query.graphql';
|
||||
import { findVersionId } from '../utils/design_management_utils';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
manual: true,
|
||||
result({ data: { projectPath, issueIid } }) {
|
||||
this.projectPath = projectPath;
|
||||
this.issueIid = issueIid;
|
||||
},
|
||||
},
|
||||
allVersions: {
|
||||
query: getDesignListQuery,
|
||||
variables() {
|
||||
|
@ -24,6 +15,14 @@ export default {
|
|||
update: data => data.project.issue.designCollection.versions.edges,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
default: '',
|
||||
},
|
||||
issueIid: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasValidVersion() {
|
||||
return (
|
||||
|
@ -55,8 +54,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
allVersions: [],
|
||||
projectPath: '',
|
||||
issueIid: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,7 +12,6 @@ import DesignPresentation from '../../components/design_presentation.vue';
|
|||
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
|
||||
import DesignSidebar from '../../components/design_sidebar.vue';
|
||||
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
|
||||
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
|
||||
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
|
||||
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
|
||||
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
|
||||
|
@ -62,22 +61,12 @@ export default {
|
|||
design: {},
|
||||
comment: '',
|
||||
annotationCoordinates: null,
|
||||
projectPath: '',
|
||||
errorMessage: '',
|
||||
issueIid: '',
|
||||
scale: 1,
|
||||
resolvedDiscussionsExpanded: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
manual: true,
|
||||
result({ data: { projectPath, issueIid } }) {
|
||||
this.projectPath = projectPath;
|
||||
this.issueIid = issueIid;
|
||||
},
|
||||
},
|
||||
design: {
|
||||
query: getDesignQuery,
|
||||
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import UploadButton from '../components/upload/button.vue';
|
||||
|
@ -33,7 +33,7 @@ export default {
|
|||
components: {
|
||||
GlLoadingIcon,
|
||||
GlAlert,
|
||||
GlDeprecatedButton,
|
||||
GlButton,
|
||||
UploadButton,
|
||||
Design,
|
||||
DesignDestroyer,
|
||||
|
@ -96,9 +96,20 @@ export default {
|
|||
? s__('DesignManagement|Deselect all')
|
||||
: s__('DesignManagement|Select all');
|
||||
},
|
||||
isDesignListEmpty() {
|
||||
return !this.isSaving && !this.hasDesigns;
|
||||
},
|
||||
designDropzoneWrapperClass() {
|
||||
return this.isDesignListEmpty
|
||||
? 'col-12'
|
||||
: 'gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.toggleOnPasteListener(this.$route.name);
|
||||
if (this.$route.path === '/designs') {
|
||||
this.$el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetFilesToBeSaved() {
|
||||
|
@ -238,51 +249,54 @@ export default {
|
|||
this.onUploadDesign([newFile]);
|
||||
}
|
||||
},
|
||||
toggleOnPasteListener(route) {
|
||||
if (route === DESIGNS_ROUTE_NAME) {
|
||||
document.addEventListener('paste', this.onDesignPaste);
|
||||
} else {
|
||||
document.removeEventListener('paste', this.onDesignPaste);
|
||||
}
|
||||
toggleOnPasteListener() {
|
||||
document.addEventListener('paste', this.onDesignPaste);
|
||||
},
|
||||
toggleOffPasteListener() {
|
||||
document.removeEventListener('paste', this.onDesignPaste);
|
||||
},
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.toggleOnPasteListener(to.name);
|
||||
this.selectedDesigns = [];
|
||||
next();
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
this.toggleOnPasteListener(to.name);
|
||||
next();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
data-testid="designs-root"
|
||||
class="gl-mt-5"
|
||||
@mouseenter="toggleOnPasteListener"
|
||||
@mouseleave="toggleOffPasteListener"
|
||||
>
|
||||
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
|
||||
<div class="d-flex justify-content-between align-items-center w-100">
|
||||
<design-version-dropdown />
|
||||
<div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
|
||||
<gl-deprecated-button
|
||||
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full">
|
||||
<div>
|
||||
<span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
|
||||
<design-version-dropdown />
|
||||
</div>
|
||||
<div v-show="hasDesigns" class="qa-selector-toolbar gl-display-flex gl-align-items-center">
|
||||
<gl-button
|
||||
v-if="isLatestVersion"
|
||||
variant="link"
|
||||
class="mr-2 js-select-all"
|
||||
size="small"
|
||||
class="gl-mr-2 js-select-all"
|
||||
@click="toggleDesignsSelection"
|
||||
>{{ selectAllButtonText }}</gl-deprecated-button
|
||||
>
|
||||
>{{ selectAllButtonText }}
|
||||
</gl-button>
|
||||
<design-destroyer
|
||||
#default="{ mutate, loading }"
|
||||
:filenames="selectedDesigns"
|
||||
:project-path="projectPath"
|
||||
:iid="issueIid"
|
||||
@done="onDesignDelete"
|
||||
@error="onDesignDeleteError"
|
||||
>
|
||||
<delete-button
|
||||
v-if="isLatestVersion"
|
||||
:is-deleting="loading"
|
||||
button-class="btn-danger btn-inverted mr-2"
|
||||
button-variant="danger"
|
||||
button-class="gl-mr-4"
|
||||
button-size="small"
|
||||
:has-selected-designs="hasSelectedDesigns"
|
||||
@deleteSelectedDesigns="mutate()"
|
||||
>
|
||||
|
@ -300,11 +314,17 @@ export default {
|
|||
{{ __('An error occurred while loading designs. Please try again.') }}
|
||||
</gl-alert>
|
||||
<ol v-else class="list-unstyled row">
|
||||
<li class="col-md-6 col-lg-4 mb-3">
|
||||
<design-dropzone class="design-list-item" @change="onUploadDesign" />
|
||||
<li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
|
||||
<design-dropzone
|
||||
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
|
||||
:has-designs="hasDesigns"
|
||||
@change="onUploadDesign"
|
||||
/>
|
||||
</li>
|
||||
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
|
||||
<design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
|
||||
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-3 gl-mb-3">
|
||||
<design-dropzone
|
||||
:has-designs="hasDesigns"
|
||||
@change="onExistingDesignDropzoneChange($event, design.filename)"
|
||||
><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
|
||||
/></design-dropzone>
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export const ROOT_ROUTE_NAME = 'root';
|
||||
export const DESIGNS_ROUTE_NAME = 'designs';
|
||||
export const DESIGN_ROUTE_NAME = 'design';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import routes from './routes';
|
||||
|
@ -16,9 +15,7 @@ export default function createRouter(base) {
|
|||
});
|
||||
const pageEl = getPageLayoutElement();
|
||||
|
||||
router.beforeEach(({ meta: { el }, name }, _, next) => {
|
||||
$(`#${el}`).tab('show');
|
||||
|
||||
router.beforeEach(({ name }, _, next) => {
|
||||
// apply a fullscreen layout style in Design View (a.k.a design detail)
|
||||
if (pageEl) {
|
||||
if (name === DESIGN_ROUTE_NAME) {
|
||||
|
|
|
@ -1,44 +1,29 @@
|
|||
import Home from '../pages/index.vue';
|
||||
import DesignDetail from '../pages/design/index.vue';
|
||||
import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
|
||||
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: ROOT_ROUTE_NAME,
|
||||
name: DESIGNS_ROUTE_NAME,
|
||||
path: '/',
|
||||
component: Home,
|
||||
meta: {
|
||||
el: 'discussion',
|
||||
},
|
||||
alias: '/designs',
|
||||
},
|
||||
{
|
||||
name: DESIGNS_ROUTE_NAME,
|
||||
path: '/designs',
|
||||
component: Home,
|
||||
meta: {
|
||||
el: 'designs',
|
||||
},
|
||||
children: [
|
||||
name: DESIGN_ROUTE_NAME,
|
||||
path: '/designs/:id',
|
||||
component: DesignDetail,
|
||||
beforeEnter(
|
||||
{
|
||||
name: DESIGN_ROUTE_NAME,
|
||||
path: ':id',
|
||||
component: DesignDetail,
|
||||
meta: {
|
||||
el: 'designs',
|
||||
},
|
||||
beforeEnter(
|
||||
{
|
||||
params: { id },
|
||||
},
|
||||
from,
|
||||
next,
|
||||
) {
|
||||
if (typeof id === 'string') {
|
||||
next();
|
||||
}
|
||||
},
|
||||
props: ({ params: { id } }) => ({ id }),
|
||||
params: { id },
|
||||
},
|
||||
],
|
||||
_,
|
||||
next,
|
||||
) {
|
||||
if (typeof id === 'string') {
|
||||
next();
|
||||
}
|
||||
},
|
||||
props: ({ params: { id } }) => ({ id }),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'DeleteButton',
|
||||
components: {
|
||||
GlButton,
|
||||
GlDeprecatedButton,
|
||||
GlModal,
|
||||
},
|
||||
directives: {
|
||||
|
@ -26,12 +25,7 @@ export default {
|
|||
buttonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'info',
|
||||
},
|
||||
buttonSize: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'medium',
|
||||
default: '',
|
||||
},
|
||||
hasSelectedDesigns: {
|
||||
type: Boolean,
|
||||
|
@ -44,38 +38,27 @@ export default {
|
|||
modalId: uniqueId('design-deletion-confirmation-'),
|
||||
};
|
||||
},
|
||||
modal: {
|
||||
title: s__('DesignManagement|Delete designs confirmation'),
|
||||
actionPrimary: {
|
||||
text: s__('Delete'),
|
||||
attributes: { variant: 'danger' },
|
||||
},
|
||||
actionCancel: {
|
||||
text: s__('Cancel'),
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center gl-h-full">
|
||||
<div>
|
||||
<gl-modal
|
||||
:modal-id="modalId"
|
||||
:title="$options.modal.title"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
:title="s__('DesignManagement|Delete designs confirmation')"
|
||||
:ok-title="s__('DesignManagement|Delete')"
|
||||
ok-variant="danger"
|
||||
@ok="$emit('deleteSelectedDesigns')"
|
||||
>
|
||||
<p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
|
||||
</gl-modal>
|
||||
<gl-button
|
||||
<gl-deprecated-button
|
||||
v-gl-modal-directive="modalId"
|
||||
:variant="buttonVariant"
|
||||
:size="buttonSize"
|
||||
:class="buttonClass"
|
||||
:disabled="isDeleting || !hasSelectedDesigns"
|
||||
:class="buttonClass"
|
||||
>
|
||||
<slot></slot>
|
||||
</gl-button>
|
||||
</gl-deprecated-button>
|
||||
</div>
|
||||
</template>
|
|
@ -13,14 +13,13 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
default: '',
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iid: {
|
||||
from: 'issueIid',
|
||||
defaut: '',
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
|
@ -60,7 +60,7 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
if (this.isNoteLinked) {
|
||||
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
||||
this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -80,7 +80,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
|
||||
<timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
|
||||
<user-avatar-link
|
||||
:link-href="author.webUrl"
|
||||
:img-src="author.avatarUrl"
|
|
@ -127,7 +127,7 @@ export default {
|
|||
params: { id: filename },
|
||||
query: $route.query,
|
||||
}"
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item"
|
||||
>
|
||||
<div class="card-body p-0 d-flex-center overflow-hidden position-relative">
|
||||
<div v-if="icon.name" class="design-event position-absolute">
|
|
@ -6,6 +6,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
|
|||
import Pagination from './pagination.vue';
|
||||
import DeleteButton from '../delete_button.vue';
|
||||
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
|
||||
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
|
||||
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
|
||||
|
||||
export default {
|
||||
|
@ -54,17 +55,19 @@ export default {
|
|||
permissions: {
|
||||
createDesign: false,
|
||||
},
|
||||
projectPath: '',
|
||||
issueIid: null,
|
||||
};
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
default: '',
|
||||
},
|
||||
issueIid: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
manual: true,
|
||||
result({ data: { projectPath, issueIid } }) {
|
||||
this.projectPath = projectPath;
|
||||
this.issueIid = issueIid;
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
query: permissionsQuery,
|
||||
variables() {
|
||||
|
@ -99,7 +102,6 @@ export default {
|
|||
query: $route.query,
|
||||
}"
|
||||
:aria-label="s__('DesignManagement|Go back to designs')"
|
||||
data-testid="close-design"
|
||||
class="mr-3 text-plain d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<icon :size="18" name="close" />
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlDeprecatedButton,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
|
@ -30,7 +30,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<gl-button
|
||||
<gl-deprecated-button
|
||||
v-gl-tooltip.hover
|
||||
:title="
|
||||
s__(
|
||||
|
@ -38,13 +38,12 @@ export default {
|
|||
)
|
||||
"
|
||||
:disabled="isSaving"
|
||||
variant="default"
|
||||
size="small"
|
||||
variant="success"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
{{ s__('DesignManagement|Upload designs') }}
|
||||
<gl-loading-icon v-if="isSaving" inline class="ml-1" />
|
||||
</gl-button>
|
||||
</gl-deprecated-button>
|
||||
|
||||
<input
|
||||
ref="fileUpload"
|
|
@ -12,12 +12,6 @@ export default {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
hasDesigns: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragCounter: 0,
|
||||
|
@ -28,12 +22,6 @@ export default {
|
|||
dragging() {
|
||||
return this.dragCounter !== 0;
|
||||
},
|
||||
iconStyles() {
|
||||
return {
|
||||
size: this.hasDesigns ? 24 : 16,
|
||||
class: this.hasDesigns ? 'gl-mb-2' : 'gl-mr-3 gl-text-gray-700',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isValidUpload(files) {
|
||||
|
@ -88,21 +76,25 @@ export default {
|
|||
>
|
||||
<slot>
|
||||
<button
|
||||
class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
|
||||
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
<div
|
||||
:class="{ 'gl-flex-direction-column': hasDesigns }"
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center"
|
||||
data-testid="dropzone-area"
|
||||
>
|
||||
<gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" />
|
||||
<p class="gl-mb-0">
|
||||
<gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} designs to attach')">
|
||||
<div class="d-flex-center flex-column text-center">
|
||||
<gl-icon name="doc-new" :size="48" class="mb-4" />
|
||||
<p>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__(
|
||||
'%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #lineOne="{ content }"
|
||||
><span class="d-block">{{ content }}</span>
|
||||
</template>
|
||||
|
||||
<template #link="{ content }">
|
||||
<gl-link @click.stop="openFileUpload">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
<gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
@ -125,7 +117,7 @@ export default {
|
|||
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
|
||||
>
|
||||
<div v-show="!isDragDataValid" class="mw-50 text-center">
|
||||
<h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3>
|
||||
<h3>{{ __('Oh no!') }}</h3>
|
||||
<span>{{
|
||||
__(
|
||||
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
|
||||
|
@ -133,7 +125,7 @@ export default {
|
|||
}}</span>
|
||||
</div>
|
||||
<div v-show="isDragDataValid" class="mw-50 text-center">
|
||||
<h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3>
|
||||
<h3>{{ __('Incoming!') }}</h3>
|
||||
<span>{{ __('Drop your designs to start your upload.') }}</span>
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import allVersionsMixin from '../../mixins/all_versions';
|
||||
import { findVersionId } from '../../utils/design_management_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
},
|
||||
mixins: [allVersionsMixin],
|
||||
computed: {
|
||||
|
@ -50,8 +50,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-new-dropdown :text="dropdownText" size="small" class="design-version-dropdown">
|
||||
<gl-new-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
|
||||
<gl-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
|
||||
<gl-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
|
||||
<router-link
|
||||
class="d-flex js-version-link"
|
||||
:to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
|
||||
|
@ -71,6 +71,6 @@ export default {
|
|||
class="fa fa-check pull-right"
|
||||
></i>
|
||||
</router-link>
|
||||
</gl-new-dropdown-item>
|
||||
</gl-new-dropdown>
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -0,0 +1,61 @@
|
|||
// This application is being moved, please do not touch this files
|
||||
// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
import apolloProvider from './graphql';
|
||||
import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
|
||||
import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-design-management');
|
||||
const badge = document.querySelector('.js-designs-count');
|
||||
const { issueIid, projectPath, issuePath } = el.dataset;
|
||||
const router = createRouter(issuePath);
|
||||
|
||||
$('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
|
||||
if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
|
||||
router.push({ name: DESIGNS_ROUTE_NAME });
|
||||
} else if (id === 'discussion') {
|
||||
router.push({ name: ROOT_ROUTE_NAME });
|
||||
}
|
||||
});
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
data: {
|
||||
projectPath,
|
||||
issueIid,
|
||||
activeDiscussion: {
|
||||
__typename: 'ActiveDiscussion',
|
||||
id: null,
|
||||
source: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
apolloProvider.clients.defaultClient
|
||||
.watchQuery({
|
||||
query: getDesignListQuery,
|
||||
variables: {
|
||||
fullPath: projectPath,
|
||||
iid: issueIid,
|
||||
atVersion: null,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (badge) {
|
||||
badge.textContent = data.project.issue.designCollection.designs.edges.length;
|
||||
}
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
router,
|
||||
apolloProvider,
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,8 +1,17 @@
|
|||
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
|
||||
import appDataQuery from '../graphql/queries/app_data.query.graphql';
|
||||
import { findVersionId } from '../utils/design_management_utils';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
manual: true,
|
||||
result({ data: { projectPath, issueIid } }) {
|
||||
this.projectPath = projectPath;
|
||||
this.issueIid = issueIid;
|
||||
},
|
||||
},
|
||||
allVersions: {
|
||||
query: getDesignListQuery,
|
||||
variables() {
|
||||
|
@ -15,14 +24,6 @@ export default {
|
|||
update: data => data.project.issue.designCollection.versions.edges,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
default: '',
|
||||
},
|
||||
issueIid: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasValidVersion() {
|
||||
return (
|
||||
|
@ -54,6 +55,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
allVersions: [],
|
||||
projectPath: '',
|
||||
issueIid: null,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -12,6 +12,7 @@ import DesignPresentation from '../../components/design_presentation.vue';
|
|||
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
|
||||
import DesignSidebar from '../../components/design_sidebar.vue';
|
||||
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
|
||||
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
|
||||
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
|
||||
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
|
||||
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
|
||||
|
@ -61,12 +62,22 @@ export default {
|
|||
design: {},
|
||||
comment: '',
|
||||
annotationCoordinates: null,
|
||||
projectPath: '',
|
||||
errorMessage: '',
|
||||
issueIid: '',
|
||||
scale: 1,
|
||||
resolvedDiscussionsExpanded: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
appData: {
|
||||
query: appDataQuery,
|
||||
manual: true,
|
||||
result({ data: { projectPath, issueIid } }) {
|
||||
this.projectPath = projectPath;
|
||||
this.issueIid = issueIid;
|
||||
},
|
||||
},
|
||||
design: {
|
||||
query: getDesignQuery,
|
||||
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import UploadButton from '../components/upload/button.vue';
|
||||
|
@ -33,7 +33,7 @@ export default {
|
|||
components: {
|
||||
GlLoadingIcon,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlDeprecatedButton,
|
||||
UploadButton,
|
||||
Design,
|
||||
DesignDestroyer,
|
||||
|
@ -96,20 +96,9 @@ export default {
|
|||
? s__('DesignManagement|Deselect all')
|
||||
: s__('DesignManagement|Select all');
|
||||
},
|
||||
isDesignListEmpty() {
|
||||
return !this.isSaving && !this.hasDesigns;
|
||||
},
|
||||
designDropzoneWrapperClass() {
|
||||
return this.isDesignListEmpty
|
||||
? 'col-12'
|
||||
: 'gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.toggleOnPasteListener(this.$route.name);
|
||||
if (this.$route.path === '/designs') {
|
||||
this.$el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetFilesToBeSaved() {
|
||||
|
@ -249,54 +238,51 @@ export default {
|
|||
this.onUploadDesign([newFile]);
|
||||
}
|
||||
},
|
||||
toggleOnPasteListener() {
|
||||
document.addEventListener('paste', this.onDesignPaste);
|
||||
},
|
||||
toggleOffPasteListener() {
|
||||
document.removeEventListener('paste', this.onDesignPaste);
|
||||
toggleOnPasteListener(route) {
|
||||
if (route === DESIGNS_ROUTE_NAME) {
|
||||
document.addEventListener('paste', this.onDesignPaste);
|
||||
} else {
|
||||
document.removeEventListener('paste', this.onDesignPaste);
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.toggleOnPasteListener(to.name);
|
||||
this.selectedDesigns = [];
|
||||
next();
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
this.toggleOnPasteListener(to.name);
|
||||
next();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-testid="designs-root"
|
||||
class="gl-mt-5"
|
||||
@mouseenter="toggleOnPasteListener"
|
||||
@mouseleave="toggleOffPasteListener"
|
||||
>
|
||||
<div>
|
||||
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
|
||||
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full">
|
||||
<div>
|
||||
<span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
|
||||
<design-version-dropdown />
|
||||
</div>
|
||||
<div v-show="hasDesigns" class="qa-selector-toolbar gl-display-flex gl-align-items-center">
|
||||
<gl-button
|
||||
<div class="d-flex justify-content-between align-items-center w-100">
|
||||
<design-version-dropdown />
|
||||
<div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
|
||||
<gl-deprecated-button
|
||||
v-if="isLatestVersion"
|
||||
variant="link"
|
||||
size="small"
|
||||
class="gl-mr-2 js-select-all"
|
||||
class="mr-2 js-select-all"
|
||||
@click="toggleDesignsSelection"
|
||||
>{{ selectAllButtonText }}
|
||||
</gl-button>
|
||||
>{{ selectAllButtonText }}</gl-deprecated-button
|
||||
>
|
||||
<design-destroyer
|
||||
#default="{ mutate, loading }"
|
||||
:filenames="selectedDesigns"
|
||||
:project-path="projectPath"
|
||||
:iid="issueIid"
|
||||
@done="onDesignDelete"
|
||||
@error="onDesignDeleteError"
|
||||
>
|
||||
<delete-button
|
||||
v-if="isLatestVersion"
|
||||
:is-deleting="loading"
|
||||
button-variant="danger"
|
||||
button-class="gl-mr-4"
|
||||
button-size="small"
|
||||
button-class="btn-danger btn-inverted mr-2"
|
||||
:has-selected-designs="hasSelectedDesigns"
|
||||
@deleteSelectedDesigns="mutate()"
|
||||
>
|
||||
|
@ -314,17 +300,11 @@ export default {
|
|||
{{ __('An error occurred while loading designs. Please try again.') }}
|
||||
</gl-alert>
|
||||
<ol v-else class="list-unstyled row">
|
||||
<li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
|
||||
<design-dropzone
|
||||
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
|
||||
:has-designs="hasDesigns"
|
||||
@change="onUploadDesign"
|
||||
/>
|
||||
<li class="col-md-6 col-lg-4 mb-3">
|
||||
<design-dropzone class="design-list-item" @change="onUploadDesign" />
|
||||
</li>
|
||||
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-3 gl-mb-3">
|
||||
<design-dropzone
|
||||
:has-designs="hasDesigns"
|
||||
@change="onExistingDesignDropzoneChange($event, design.filename)"
|
||||
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
|
||||
<design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
|
||||
><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
|
||||
/></design-dropzone>
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export const ROOT_ROUTE_NAME = 'root';
|
||||
export const DESIGNS_ROUTE_NAME = 'designs';
|
||||
export const DESIGN_ROUTE_NAME = 'design';
|
|
@ -1,8 +1,9 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import routes from './routes';
|
||||
import { DESIGN_ROUTE_NAME } from './constants';
|
||||
import { getPageLayoutElement } from '~/design_management_new/utils/design_management_utils';
|
||||
import { getPageLayoutElement } from '~/design_management_legacy/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
@ -15,7 +16,9 @@ export default function createRouter(base) {
|
|||
});
|
||||
const pageEl = getPageLayoutElement();
|
||||
|
||||
router.beforeEach(({ name }, _, next) => {
|
||||
router.beforeEach(({ meta: { el }, name }, _, next) => {
|
||||
$(`#${el}`).tab('show');
|
||||
|
||||
// apply a fullscreen layout style in Design View (a.k.a design detail)
|
||||
if (pageEl) {
|
||||
if (name === DESIGN_ROUTE_NAME) {
|
|
@ -0,0 +1,44 @@
|
|||
import Home from '../pages/index.vue';
|
||||
import DesignDetail from '../pages/design/index.vue';
|
||||
import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: ROOT_ROUTE_NAME,
|
||||
path: '/',
|
||||
component: Home,
|
||||
meta: {
|
||||
el: 'discussion',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DESIGNS_ROUTE_NAME,
|
||||
path: '/designs',
|
||||
component: Home,
|
||||
meta: {
|
||||
el: 'designs',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: DESIGN_ROUTE_NAME,
|
||||
path: ':id',
|
||||
component: DesignDetail,
|
||||
meta: {
|
||||
el: 'designs',
|
||||
},
|
||||
beforeEnter(
|
||||
{
|
||||
params: { id },
|
||||
},
|
||||
from,
|
||||
next,
|
||||
) {
|
||||
if (typeof id === 'string') {
|
||||
next();
|
||||
}
|
||||
},
|
||||
props: ({ params: { id } }) => ({ id }),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -1,33 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
import apolloProvider from './graphql';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-design-management-new');
|
||||
const { issueIid, projectPath, issuePath } = el.dataset;
|
||||
const router = createRouter(issuePath);
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
data: {
|
||||
activeDiscussion: {
|
||||
__typename: 'ActiveDiscussion',
|
||||
id: null,
|
||||
source: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
router,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
projectPath,
|
||||
issueIid,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
import Home from '../pages/index.vue';
|
||||
import DesignDetail from '../pages/design/index.vue';
|
||||
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: DESIGNS_ROUTE_NAME,
|
||||
path: '/',
|
||||
component: Home,
|
||||
alias: '/designs',
|
||||
},
|
||||
{
|
||||
name: DESIGN_ROUTE_NAME,
|
||||
path: '/designs/:id',
|
||||
component: DesignDetail,
|
||||
beforeEnter(
|
||||
{
|
||||
params: { id },
|
||||
},
|
||||
_,
|
||||
next,
|
||||
) {
|
||||
if (typeof id === 'string') {
|
||||
next();
|
||||
}
|
||||
},
|
||||
props: ({ params: { id } }) => ({ id }),
|
||||
},
|
||||
];
|
|
@ -2,6 +2,7 @@
|
|||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { escape } from 'lodash';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import { hasDiff } from '~/helpers/diffs_helper';
|
||||
|
@ -16,6 +17,7 @@ export default {
|
|||
DiffContent,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
|
@ -89,8 +91,25 @@ export default {
|
|||
|
||||
this.setFileCollapsed({ filePath: this.file.file_path, collapsed: newVal });
|
||||
},
|
||||
'file.file_hash': {
|
||||
handler: function watchFileHash() {
|
||||
if (
|
||||
this.glFeatures.autoExpandCollapsedDiffs &&
|
||||
this.viewDiffsFileByFile &&
|
||||
this.file.viewer.collapsed
|
||||
) {
|
||||
this.isCollapsed = false;
|
||||
this.handleLoadCollapsedDiff();
|
||||
} else {
|
||||
this.isCollapsed = this.file.viewer.collapsed || false;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
'file.viewer.collapsed': function setIsCollapsed(newVal) {
|
||||
this.isCollapsed = newVal;
|
||||
if (!this.viewDiffsFileByFile && !this.glFeatures.autoExpandCollapsedDiffs) {
|
||||
this.isCollapsed = newVal;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
|
|
@ -8,12 +8,14 @@ import {
|
|||
GlAvatar,
|
||||
GlTooltipDirective,
|
||||
GlButton,
|
||||
GlSearchBoxByType,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
|
||||
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
|
||||
import { I18N } from '../constants';
|
||||
import { I18N, INCIDENT_SEARCH_DELAY } from '../constants';
|
||||
|
||||
const tdClass =
|
||||
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
|
||||
|
@ -52,6 +54,7 @@ export default {
|
|||
GlAvatar,
|
||||
GlButton,
|
||||
TimeAgoTooltip,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -62,6 +65,7 @@ export default {
|
|||
query: getIncidents,
|
||||
variables() {
|
||||
return {
|
||||
searchTerm: this.searchTerm,
|
||||
projectPath: this.projectPath,
|
||||
labelNames: ['incident'],
|
||||
};
|
||||
|
@ -77,11 +81,12 @@ export default {
|
|||
errored: false,
|
||||
isErrorAlertDismissed: false,
|
||||
redirecting: false,
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showErrorMsg() {
|
||||
return this.errored && !this.isErrorAlertDismissed;
|
||||
return this.errored && !this.isErrorAlertDismissed && !this.searchTerm;
|
||||
},
|
||||
loading() {
|
||||
return this.$apollo.queries.incidents.loading;
|
||||
|
@ -98,6 +103,13 @@ export default {
|
|||
return mergeUrlParams({ issuable_template: this.incidentTemplateName }, this.newIssuePath);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchTerm: debounce(function debounceSearch(input) {
|
||||
if (input !== this.searchTerm) {
|
||||
this.searchTerm = input;
|
||||
}
|
||||
}, INCIDENT_SEARCH_DELAY),
|
||||
},
|
||||
methods: {
|
||||
hasAssignees(assignees) {
|
||||
return Boolean(assignees.nodes?.length);
|
||||
|
@ -116,7 +128,7 @@ export default {
|
|||
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
class="gl-mt-3 create-incident-button"
|
||||
class="gl-mt-3 gl-mb-3 create-incident-button"
|
||||
data-testid="createIncidentBtn"
|
||||
:loading="redirecting"
|
||||
:disabled="redirecting"
|
||||
|
@ -129,6 +141,14 @@ export default {
|
|||
</gl-button>
|
||||
</div>
|
||||
|
||||
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
|
||||
<gl-search-box-by-type
|
||||
v-model.trim="searchTerm"
|
||||
class="gl-bg-white"
|
||||
:placeholder="$options.i18n.searchPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h4 class="gl-display-block d-md-none my-3">
|
||||
{{ s__('IncidentManagement|Incidents') }}
|
||||
</h4>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const I18N = {
|
||||
errorMsg: s__('IncidentManagement|There was an error displaying the incidents.'),
|
||||
noIncidents: s__('IncidentManagement|No incidents to display.'),
|
||||
unassigned: s__('IncidentManagement|Unassigned'),
|
||||
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
|
||||
searchPlaceholder: __('Search or filter results...'),
|
||||
};
|
||||
|
||||
export const INCIDENT_SEARCH_DELAY = 300;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
query getIncidents($projectPath: ID!, $labelNames: [String], $state: IssuableState) {
|
||||
query getIncidents(
|
||||
$searchTerm: String
|
||||
$projectPath: ID!
|
||||
$labelNames: [String]
|
||||
$state: IssuableState
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
issues(state: $state, labelName: $labelNames) {
|
||||
issues(search: $searchTerm, state: $state, labelName: $labelNames) {
|
||||
nodes {
|
||||
iid
|
||||
title
|
||||
|
|
|
@ -26,6 +26,7 @@ export default {
|
|||
emptyListHelpUrl: state => state.config.emptyListHelpUrl,
|
||||
comingSoon: state => state.config.comingSoon,
|
||||
filterQuery: state => state.filterQuery,
|
||||
selectedType: state => state.selectedType,
|
||||
}),
|
||||
tabsToRender() {
|
||||
return PACKAGE_REGISTRY_TABS;
|
||||
|
@ -42,11 +43,11 @@ export default {
|
|||
onPackageDeleteRequest(item) {
|
||||
return this.requestDeletePackage(item);
|
||||
},
|
||||
tabChanged(e) {
|
||||
const selectedType = PACKAGE_REGISTRY_TABS[e];
|
||||
tabChanged(index) {
|
||||
const selected = PACKAGE_REGISTRY_TABS[index];
|
||||
|
||||
if (selectedType) {
|
||||
this.setSelectedType(selectedType);
|
||||
if (selected !== this.selectedType) {
|
||||
this.setSelectedType(selected);
|
||||
this.requestPackagesList();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -73,10 +73,15 @@ export const PACKAGE_REGISTRY_TABS = [
|
|||
title: __('All'),
|
||||
type: null,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|Composer'),
|
||||
type: PackageType.COMPOSER,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|Conan'),
|
||||
type: PackageType.CONAN,
|
||||
},
|
||||
|
||||
{
|
||||
title: s__('PackageRegistry|Maven'),
|
||||
type: PackageType.MAVEN,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { PACKAGE_REGISTRY_TABS } from '../constants';
|
||||
|
||||
export default () => ({
|
||||
/**
|
||||
* Determine if the component is loading data from the API
|
||||
|
@ -48,4 +50,8 @@ export default () => ({
|
|||
* The search query that is used to filter packages by name
|
||||
*/
|
||||
filterQuery: '',
|
||||
/**
|
||||
* The selected TAB of the package types tabs
|
||||
*/
|
||||
selectedType: PACKAGE_REGISTRY_TABS[0],
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ export const PackageType = {
|
|||
NPM: 'npm',
|
||||
NUGET: 'nuget',
|
||||
PYPI: 'pypi',
|
||||
COMPOSER: 'composer',
|
||||
};
|
||||
|
||||
export const TrackingActions = {
|
||||
|
|
|
@ -19,6 +19,8 @@ export const getPackageTypeLabel = packageType => {
|
|||
return s__('PackageType|NuGet');
|
||||
case PackageType.PYPI:
|
||||
return s__('PackageType|PyPi');
|
||||
case PackageType.COMPOSER:
|
||||
return s__('PackageType|Composer');
|
||||
|
||||
default:
|
||||
return null;
|
||||
|
|
|
@ -16,13 +16,13 @@ export default function() {
|
|||
initSentryErrorStackTraceApp();
|
||||
initRelatedMergeRequestsApp();
|
||||
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
||||
// This will be removed when we remove the `design_management_moved` feature flag
|
||||
// See https://gitlab.com/gitlab-org/gitlab/-/issues/223197
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management_legacy')
|
||||
.then(module => module.default())
|
||||
.catch(() => {});
|
||||
|
||||
// This will be removed when we remove the `design_management_moved` feature flag
|
||||
// See https://gitlab.com/gitlab-org/gitlab/-/issues/223197
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management_new')
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
||||
.then(module => module.default())
|
||||
.catch(() => {});
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
</div>
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-black-normal gl-font-weight-bold"
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-body gl-font-weight-bold"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<slot name="left-primary"></slot>
|
||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
|||
>
|
||||
<template #left-primary>
|
||||
<router-link
|
||||
class="gl-text-black-normal gl-font-weight-bold"
|
||||
class="gl-text-body gl-font-weight-bold"
|
||||
data-testid="detailsLink"
|
||||
:to="{ name: 'details', params: { id: encodedItem } }"
|
||||
>
|
||||
|
|
|
@ -50,9 +50,7 @@ export default {
|
|||
data-qa-selector="clone_button"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="blob in blobs" :key="blob.path">
|
||||
<snippet-blob :snippet="snippet" :blob="blob" />
|
||||
</div>
|
||||
<snippet-blob v-for="blob in blobs" :key="blob.path" :snippet="snippet" :blob="blob" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -25,8 +25,9 @@ export default {
|
|||
rich: this.activeViewerType === RICH_BLOB_VIEWER,
|
||||
};
|
||||
},
|
||||
update: data =>
|
||||
data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData,
|
||||
update(data) {
|
||||
return this.onContentUpdate(data);
|
||||
},
|
||||
result() {
|
||||
if (this.activeViewerType === RICH_BLOB_VIEWER) {
|
||||
this.blob.richViewer.renderError = null;
|
||||
|
@ -76,6 +77,12 @@ export default {
|
|||
this.$apollo.queries.blobContent.skip = false;
|
||||
this.$apollo.queries.blobContent.refetch();
|
||||
},
|
||||
onContentUpdate(data) {
|
||||
const { path: blobPath } = this.blob;
|
||||
const { blobs } = data.snippets.edges[0].node;
|
||||
const updatedBlobData = blobs.find(blob => blob.path === blobPath);
|
||||
return updatedBlobData.richData || updatedBlobData.plainData;
|
||||
},
|
||||
},
|
||||
BLOB_RENDER_EVENT_LOAD,
|
||||
BLOB_RENDER_EVENT_SHOW_SOURCE,
|
||||
|
|
|
@ -3,7 +3,8 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
|
|||
edges {
|
||||
node {
|
||||
id
|
||||
blob {
|
||||
blobs {
|
||||
path
|
||||
richData @include(if: $rich)
|
||||
plainData @skip(if: $rich)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@
|
|||
|
||||
.snippet-file-content {
|
||||
border-radius: 3px;
|
||||
|
||||
+ .snippet-file-content {
|
||||
@include gl-mt-5;
|
||||
}
|
||||
}
|
||||
|
||||
.snippet-header {
|
||||
|
|
|
@ -36,6 +36,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:multiline_comments, @project)
|
||||
push_frontend_feature_flag(:file_identifier_hash)
|
||||
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project)
|
||||
end
|
||||
|
||||
before_action do
|
||||
|
|
|
@ -108,7 +108,7 @@ module Issuable
|
|||
end
|
||||
|
||||
def milestone_changes_tracking_enabled?
|
||||
::Feature.enabled?(:track_resource_milestone_change_events, issuable.project)
|
||||
::Feature.enabled?(:track_resource_milestone_change_events, issuable.project, default_enabled: true)
|
||||
end
|
||||
|
||||
def create_due_date_note
|
||||
|
|
|
@ -341,7 +341,7 @@ module SystemNotes
|
|||
|
||||
def state_change_tracking_enabled?
|
||||
noteable.respond_to?(:resource_state_events) &&
|
||||
::Feature.enabled?(:track_resource_state_change_events, noteable.project)
|
||||
::Feature.enabled?(:track_resource_state_change_events, noteable.project, default_enabled: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Unify Prometheus metric initialization by always using inline transaction metrics
|
||||
merge_request: 32980
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add composer tab and package type to package list
|
||||
merge_request: 37928
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track milestone and state changes in issues / MRs using resource events
|
||||
merge_request: 36936
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add search bar for incidents
|
||||
merge_request: 37885
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix dark mode container registry text
|
||||
merge_request: 37940
|
||||
author:
|
||||
type: fixed
|
|
@ -199,7 +199,7 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
|
|||
val = super
|
||||
|
||||
if current_transaction = ::Gitlab::Metrics::Transaction.current
|
||||
current_transaction.increment(:new_redis_connections, 1)
|
||||
current_transaction.increment(:gitlab_transaction_new_redis_connections_total, 1)
|
||||
end
|
||||
|
||||
val
|
||||
|
|
|
@ -1,86 +1,49 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Reference architecture: up to 1,000 users
|
||||
|
||||
This page describes GitLab reference architecture for up to 1,000 users.
|
||||
For a full list of reference architectures, see
|
||||
This page describes GitLab reference architecture for up to 1,000 users. For a
|
||||
full list of reference architectures, see
|
||||
[Available reference architectures](index.md#available-reference-architectures).
|
||||
|
||||
If you need to serve up to 1,000 users and you don't have strict availability
|
||||
requirements, a single-node solution with
|
||||
[frequent backups](index.md#automated-backups-core-only) is appropriate for
|
||||
many organizations .
|
||||
|
||||
> - **Supported users (approximate):** 1,000
|
||||
> - **High Availability:** False
|
||||
> - **High Availability:** No
|
||||
|
||||
| Users | Configuration([8](#footnotes)) | GCP | AWS | Azure |
|
||||
|-------------|------------------------------------|----------------|---------------------|------------------------|
|
||||
| up to 500 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | F4s v2 |
|
||||
| up to 1000 | 8 vCPU, 7.2GB Memory | `n1-highcpu-8` | `c5.2xlarge` | F8s v2 |
|
||||
| Users | Configuration | GCP | AWS | Azure |
|
||||
|--------------|-------------------------|----------------|-----------------|----------------|
|
||||
| Up to 500 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Up to 1,000 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
|
||||
In addition to the above, we recommend having at least
|
||||
2GB of swap on your server, even if you currently have
|
||||
enough available RAM. Having swap will help reduce the chance of errors occurring
|
||||
if your available memory changes. We also recommend
|
||||
configuring the kernel's swappiness setting
|
||||
to a low value like `10` to make the most of your RAM while still having the swap
|
||||
available when needed.
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
CPU platform. On different hardware you may find that adjustments, either lower
|
||||
or higher, are required for your CPU or node counts. For more information, see
|
||||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
For situations where you need to serve up to 1,000 users, a single-node
|
||||
solution with [frequent backups](index.md#automated-backups-core-only) is appropriate
|
||||
for many organizations. With automatic backup of the GitLab repositories,
|
||||
configuration, and the database, if you don't have strict availability
|
||||
requirements, this is the ideal solution.
|
||||
In addition to the stated configurations, we recommend having at least 2GB of
|
||||
swap on your server, even if you currently have enough available memory. Having
|
||||
swap will help reduce the chance of errors occurring if your available memory
|
||||
changes. We also recommend configuring the kernel's swappiness setting to a
|
||||
lower value (such as `10`) to make the most of your memory, while still having
|
||||
the swap available when needed.
|
||||
|
||||
## Setup instructions
|
||||
|
||||
- For this default reference architecture, use the standard [installation instructions](../../install/README.md) to install GitLab.
|
||||
For this default reference architecture, to install GitLab use the standard
|
||||
[installation instructions](../../install/README.md).
|
||||
|
||||
NOTE: **Note:**
|
||||
You can also optionally configure GitLab to use an
|
||||
[external PostgreSQL service](../postgresql/external.md) or an
|
||||
[external object storage service](../high_availability/object_storage.md) for
|
||||
added performance and reliability at a reduced complexity cost.
|
||||
|
||||
## Footnotes
|
||||
|
||||
1. In our architectures we run each GitLab Rails node using the Puma webserver
|
||||
and have its number of workers set to 90% of available CPUs along with four threads. For
|
||||
nodes that are running Rails with other components the worker value should be reduced
|
||||
accordingly where we've found 50% achieves a good balance but this is dependent
|
||||
on workload.
|
||||
|
||||
1. Gitaly node requirements are dependent on customer data, specifically the number of
|
||||
projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
|
||||
and at least four nodes should be used when supporting 50,000 or more users.
|
||||
We also recommend that each Gitaly node should store no more than 5TB of data
|
||||
and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
|
||||
set to 20% of available CPUs. Additional nodes should be considered in conjunction
|
||||
with a review of expected data size and spread based on the recommendations above.
|
||||
|
||||
1. Recommended Redis setup differs depending on the size of the architecture.
|
||||
For smaller architectures (less than 3,000 users) a single instance should suffice.
|
||||
For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
|
||||
classes and that Redis Sentinel is hosted alongside Consul.
|
||||
For larger architectures (10,000 users or more) we suggest running a separate
|
||||
[Redis Cluster](../redis/replication_and_failover.md#running-multiple-redis-clusters) for the Cache class
|
||||
and another for the Queues and Shared State classes respectively. We also recommend
|
||||
that you run the Redis Sentinel clusters separately for each Redis Cluster.
|
||||
|
||||
1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
|
||||
over NFS where possible, due to better performance and availability.
|
||||
|
||||
1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
|
||||
object storage but this isn't typically recommended for performance reasons. Note however it is required for
|
||||
[GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/196).
|
||||
|
||||
1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
|
||||
as the load balancer. Although other load balancers with similar feature sets
|
||||
could also be used, those load balancers have not been validated.
|
||||
|
||||
1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
|
||||
HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
|
||||
as these components have heavy I/O. These IOPS values are recommended only as a starter
|
||||
as with time they may be adjusted higher or lower depending on the scale of your
|
||||
environment's workload. If you're running the environment on a Cloud provider
|
||||
you may need to refer to their documentation on how configure IOPS correctly.
|
||||
|
||||
1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
CPU platform on GCP. On different hardware you may find that adjustments, either lower
|
||||
or higher, are required for your CPU or Node counts accordingly. For more information, a
|
||||
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
|
||||
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
---
|
||||
reading_time: true
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Reference architecture: up to 2,000 users
|
||||
|
@ -9,19 +12,19 @@ For a full list of reference architectures, see
|
|||
[Available reference architectures](index.md#available-reference-architectures).
|
||||
|
||||
> - **Supported users (approximate):** 2,000
|
||||
> - **High Availability:** False
|
||||
> - **High Availability:** No
|
||||
> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
|
||||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|------------------------------------------|--------|-------------------------|-----------------|----------------|-----------|
|
||||
| Load balancer | 1 | 2 vCPU, 1.8GB memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
|
||||
| PostgreSQL | 1 | 2 vCPU, 7.5GB memory | `n1-standard-2` | `m5.large` | `D2s v3` |
|
||||
| Redis | 1 | 1 vCPU, 3.75GB memory | `n1-standard-1` | `m5.large` | `D2s v3` |
|
||||
| Gitaly | 1 | 4 vCPU, 15GB memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
|
||||
| GitLab Rails | 2 | 8 vCPU, 7.2GB memory | `n1-highcpu-8` | `c5.2xlarge` | `F8s v2` |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8GB memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6GB memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|------------------------------------------|--------|-------------------------|----------------|--------------|---------|
|
||||
| Load balancer | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 1 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Redis | 1 | 1 vCPU, 3.75GB memory | n1-standard-1 | m5.large | D2s v3 |
|
||||
| Gitaly | 1 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 2 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue