Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-28 12:09:49 +00:00
parent 829e846dd5
commit ed00b1a6a3
209 changed files with 2063 additions and 1757 deletions

View File

@ -1 +1 @@
1833948feca92eab5791a4358f862b9b9d6b680d
4bb38a198255d9b898763eedfd64508b72af7b3b

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,2 @@
export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ export const PackageType = {
NPM: 'npm',
NUGET: 'nuget',
PYPI: 'pypi',
COMPOSER: 'composer',
};
export const TrackingActions = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,8 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
edges {
node {
id
blob {
blobs {
path
richData @include(if: $rich)
plainData @skip(if: $rich)
}

View File

@ -32,6 +32,10 @@
.snippet-file-content {
border-radius: 3px;
+ .snippet-file-content {
@include gl-mt-5;
}
}
.snippet-header {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
title: Unify Prometheus metric initialization by always using inline transaction metrics
merge_request: 32980
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add composer tab and package type to package list
merge_request: 37928
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Track milestone and state changes in issues / MRs using resource events
merge_request: 36936
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add search bar for incidents
merge_request: 37885
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix dark mode container registry text
merge_request: 37940
author:
type: fixed

View File

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

View File

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

View File

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