提交 4720b569 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 cefe554b
...@@ -48,8 +48,10 @@ rules: ...@@ -48,8 +48,10 @@ rules:
no-jquery/no-serialize: error no-jquery/no-serialize: error
promise/always-return: off promise/always-return: off
promise/no-callback-in-promise: off promise/no-callback-in-promise: off
# Make update to eslint@6 smoother:
prefer-object-spread: off
overrides: overrides:
files: - files:
- '**/spec/**/*' - '**/spec/**/*'
rules: rules:
"@gitlab/i18n/no-non-i18n-strings": off "@gitlab/i18n/no-non-i18n-strings": off
/* eslint-disable max-classes-per-file */
import $ from 'jquery'; import $ from 'jquery';
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
......
/* eslint-disable one-var, consistent-return */ /* eslint-disable max-classes-per-file, one-var, consistent-return */
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
......
<script> <script>
import _ from 'underscore'; import { escape as esc, isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale'; import { sprintf, __ } from '../../locale';
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
'%{startLink}%{name}%{endLink}', '%{startLink}%{name}%{endLink}',
{ {
startLink: `<a href="${this.deploymentStatus.environment.environment_path}" class="js-environment-link">`, startLink: `<a href="${this.deploymentStatus.environment.environment_path}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name), name: esc(this.deploymentStatus.environment.name),
endLink: '</a>', endLink: '</a>',
}, },
false, false,
...@@ -58,10 +58,10 @@ export default { ...@@ -58,10 +58,10 @@ export default {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
}, },
hasEnvironment() { hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment); return !isEmpty(this.deploymentStatus.environment);
}, },
lastDeploymentPath() { lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable) return !isEmpty(this.lastDeployment.deployable)
? this.lastDeployment.deployable.build_path ? this.lastDeployment.deployable.build_path
: ''; : '';
}, },
...@@ -74,8 +74,8 @@ export default { ...@@ -74,8 +74,8 @@ export default {
} }
const { name, path } = this.deploymentCluster; const { name, path } = this.deploymentCluster;
const escapedName = _.escape(name); const escapedName = esc(name);
const escapedPath = _.escape(path); const escapedPath = esc(path);
if (!escapedPath) { if (!escapedPath) {
return escapedName; return escapedName;
......
<script> <script>
import _ from 'underscore'; import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -21,7 +21,7 @@ export default { ...@@ -21,7 +21,7 @@ export default {
}, },
computed: { computed: {
isErasedByUser() { isErasedByUser() {
return !_.isEmpty(this.user); return !isEmpty(this.user);
}, },
}, },
}; };
......
<script> <script>
import _ from 'underscore'; import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
...@@ -125,7 +125,7 @@ export default { ...@@ -125,7 +125,7 @@ export default {
// Once the job log is loaded, // Once the job log is loaded,
// fetch the stages for the dropdown on the sidebar // fetch the stages for the dropdown on the sidebar
job(newVal, oldVal) { job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { if (isEmpty(oldVal) && !isEmpty(newVal.pipeline)) {
const stages = this.job.pipeline.details.stages || []; const stages = this.job.pipeline.details.stages || [];
const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage); const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage);
...@@ -145,7 +145,7 @@ export default { ...@@ -145,7 +145,7 @@ export default {
}, },
}, },
created() { created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100); this.throttled = throttle(this.toggleScrollButtons, 100);
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll); window.addEventListener('scroll', this.updateScroll);
......
<script> <script>
import _ from 'underscore'; import { uniqueId } from 'lodash';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
...@@ -19,7 +19,9 @@ export default { ...@@ -19,7 +19,9 @@ export default {
validator(value) { validator(value) {
return ( return (
value === null || value === null ||
(_.has(value, 'path') && _.has(value, 'method') && _.has(value, 'button_title')) (Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'button_title'))
); );
}, },
}, },
...@@ -78,7 +80,7 @@ export default { ...@@ -78,7 +80,7 @@ export default {
const newVariable = { const newVariable = {
key: this.key, key: this.key,
secret_value: this.secretValue, secret_value: this.secretValue,
id: _.uniqueId(), id: uniqueId(),
}; };
this.variables.push(newVariable); this.variables.push(newVariable);
......
<script> <script>
import _ from 'underscore'; import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton } from '@gitlab/ui'; import { GlLink, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
...@@ -84,10 +84,10 @@ export default { ...@@ -84,10 +84,10 @@ export default {
); );
}, },
hasArtifact() { hasArtifact() {
return !_.isEmpty(this.job.artifact); return !isEmpty(this.job.artifact);
}, },
hasTriggers() { hasTriggers() {
return !_.isEmpty(this.job.trigger); return !isEmpty(this.job.trigger);
}, },
hasStages() { hasStages() {
return ( return (
......
<script> <script>
import _ from 'underscore'; import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
...@@ -24,7 +24,7 @@ export default { ...@@ -24,7 +24,7 @@ export default {
}, },
computed: { computed: {
hasRef() { hasRef() {
return !_.isEmpty(this.pipeline.ref); return !isEmpty(this.pipeline.ref);
}, },
isTriggeredByMergeRequest() { isTriggeredByMergeRequest() {
return Boolean(this.pipeline.merge_request); return Boolean(this.pipeline.merge_request);
......
import _ from 'underscore'; import { isEmpty, isString } from 'lodash';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
...@@ -7,15 +7,15 @@ export const hasUnmetPrerequisitesFailure = state => ...@@ -7,15 +7,15 @@ export const hasUnmetPrerequisitesFailure = state =>
state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites'; state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites';
export const shouldRenderCalloutMessage = state => export const shouldRenderCalloutMessage = state =>
!_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); !isEmpty(state.job.status) && !isEmpty(state.job.callout_message);
/** /**
* When job has not started the key will be null * When job has not started the key will be null
* When job started the key will be a string with a date. * When job started the key will be a string with a date.
*/ */
export const shouldRenderTriggeredLabel = state => _.isString(state.job.started); export const shouldRenderTriggeredLabel = state => isString(state.job.started);
export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); export const hasEnvironment = state => !isEmpty(state.job.deployment_status);
/** /**
* Checks if it the job has trace. * Checks if it the job has trace.
...@@ -23,7 +23,7 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); ...@@ -23,7 +23,7 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* @returns {Boolean} * @returns {Boolean}
*/ */
export const hasTrace = state => export const hasTrace = state =>
state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); state.job.has_trace || (!isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state => export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {}; (state.job && state.job.status && state.job.status.illustration) || {};
...@@ -38,8 +38,8 @@ export const emptyStateAction = state => ...@@ -38,8 +38,8 @@ export const emptyStateAction = state =>
* @returns {Boolean} * @returns {Boolean}
*/ */
export const shouldRenderSharedRunnerLimitWarning = state => export const shouldRenderSharedRunnerLimitWarning = state =>
!_.isEmpty(state.job.runners) && !isEmpty(state.job.runners) &&
!_.isEmpty(state.job.runners.quota) && !isEmpty(state.job.runners.quota) &&
state.job.runners.quota.used >= state.job.runners.quota.limit; state.job.runners.quota.used >= state.job.runners.quota.limit;
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
......
...@@ -127,7 +127,6 @@ export default { ...@@ -127,7 +127,6 @@ export default {
}); });
const yAxisWithOffset = { const yAxisWithOffset = {
name: this.yAxisLabel,
axisLabel: { axisLabel: {
formatter: num => roundOffFloat(num - this.yOffset, 3).toString(), formatter: num => roundOffFloat(num - this.yOffset, 3).toString(),
}, },
...@@ -162,6 +161,7 @@ export default { ...@@ -162,6 +161,7 @@ export default {
}), }),
); );
} }
return { yAxis: yAxisWithOffset, series: boundarySeries }; return { yAxis: yAxisWithOffset, series: boundarySeries };
}, },
}, },
......
...@@ -162,7 +162,8 @@ export default { ...@@ -162,7 +162,8 @@ export default {
); );
}, },
chartOptions() { chartOptions() {
const option = omit(this.option, 'series'); const { yAxis, xAxis } = this.option;
const option = omit(this.option, ['series', 'yAxis', 'xAxis']);
const dataYAxis = { const dataYAxis = {
name: this.yAxisLabel, name: this.yAxisLabel,
...@@ -173,7 +174,9 @@ export default { ...@@ -173,7 +174,9 @@ export default {
axisLabel: { axisLabel: {
formatter: num => roundOffFloat(num, 3).toString(), formatter: num => roundOffFloat(num, 3).toString(),
}, },
...yAxis,
}; };
const deploymentsYAxis = { const deploymentsYAxis = {
show: false, show: false,
min: deploymentYAxisCoords.min, min: deploymentYAxisCoords.min,
...@@ -184,18 +187,21 @@ export default { ...@@ -184,18 +187,21 @@ export default {
}, },
}; };
const timeXAxis = {
name: __('Time'),
type: 'time',
axisLabel: {
formatter: date => dateFormat(date, dateFormats.timeOfDay),
},
axisPointer: {
snap: true,
},
...xAxis,
};
return { return {
series: this.chartOptionSeries, series: this.chartOptionSeries,
xAxis: { xAxis: timeXAxis,
name: __('Time'),
type: 'time',
axisLabel: {
formatter: date => dateFormat(date, dateFormats.timeOfDay),
},
axisPointer: {
snap: true,
},
},
yAxis: [dataYAxis, deploymentsYAxis], yAxis: [dataYAxis, deploymentsYAxis],
dataZoom: [this.dataZoomConfig], dataZoom: [this.dataZoomConfig],
...option, ...option,
......
...@@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index'; ...@@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initRegistryImages(); initRegistryImages();
registryExplorer(); const { attachMainComponent, attachBreadcrumb } = registryExplorer();
attachBreadcrumb();
attachMainComponent();
}); });
...@@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index'; ...@@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initRegistryImages(); initRegistryImages();
registryExplorer(); const { attachMainComponent, attachBreadcrumb } = registryExplorer();
attachBreadcrumb();
attachMainComponent();
}); });
<script>
import { initial, first, last } from 'lodash';
export default {
props: {
crumbs: {
type: Array,
required: true,
},
},
computed: {
rootRoute() {
return this.$router.options.routes.find(r => r.meta.root);
},
isRootRoute() {
return this.$route.name === this.rootRoute.name;
},
rootCrumbs() {
return initial(this.crumbs);
},
divider() {
const { classList, tagName, innerHTML } = first(this.crumbs).querySelector('svg');
return { classList: [...classList], tagName, innerHTML };
},
lastCrumb() {
const { children } = last(this.crumbs);
const { tagName, classList } = first(children);
return {
tagName,
classList: [...classList],
text: this.$route.meta.nameGenerator(this.$route),
path: { to: this.$route.name },
};
},
},
};
</script>
<template>
<ul>
<li
v-for="(crumb, index) in rootCrumbs"
:key="index"
:class="crumb.classList"
v-html="crumb.innerHTML"
></li>
<li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path">
{{ rootRoute.meta.nameGenerator(rootRoute) }}
</router-link>
<component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" />
</li>
<li>
<component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.classList">
<router-link ref="childRouteLink" :to="lastCrumb.path">{{ lastCrumb.text }}</router-link>
</component>
</li>
</ul>
</template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import RegistryExplorer from './pages/index.vue'; import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
import { createStore } from './stores'; import { createStore } from './stores';
import createRouter from './router'; import createRouter from './router';
...@@ -19,15 +20,39 @@ export default () => { ...@@ -19,15 +20,39 @@ export default () => {
const router = createRouter(endpoint, store); const router = createRouter(endpoint, store);
store.dispatch('setInitialState', el.dataset); store.dispatch('setInitialState', el.dataset);
return new Vue({ const attachMainComponent = () =>
el, new Vue({
store, el,
router, store,
components: { router,
RegistryExplorer, components: {
}, RegistryExplorer,
render(createElement) { },
return createElement('registry-explorer'); render(createElement) {
}, return createElement('registry-explorer');
}); },
});
const attachBreadcrumb = () => {
const breadCrumbEl = document.querySelector('nav .js-breadcrumbs-list');
const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')];
return new Vue({
el: breadCrumbEl,
store,
router,
components: {
RegistryBreadcrumb,
},
render(createElement) {
return createElement('registry-breadcrumb', {
class: breadCrumbEl.className,
props: {
crumbs,
},
});
},
});
};
return { attachBreadcrumb, attachMainComponent };
}; };
...@@ -19,6 +19,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; ...@@ -19,6 +19,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { decodeAndParse } from '../utils';
import { import {
LIST_KEY_TAG, LIST_KEY_TAG,
LIST_KEY_IMAGE_ID, LIST_KEY_IMAGE_ID,
...@@ -62,7 +63,7 @@ export default { ...@@ -62,7 +63,7 @@ export default {
computed: { computed: {
...mapState(['tags', 'tagsPagination', 'isLoading', 'config']), ...mapState(['tags', 'tagsPagination', 'isLoading', 'config']),
imageName() { imageName() {
const { name } = JSON.parse(window.atob(this.$route.params.id)); const { name } = decodeAndParse(this.$route.params.id);
return name; return name;
}, },
fields() { fields() {
...@@ -169,7 +170,7 @@ export default { ...@@ -169,7 +170,7 @@ export default {
}, },
handleSingleDelete(itemToDelete) { handleSingleDelete(itemToDelete) {
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
this.requestDeleteTag({ tag: itemToDelete, imageId: this.$route.params.id }); this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id });
}, },
handleMultipleDelete() { handleMultipleDelete() {
const { itemsToBeDeleted } = this; const { itemsToBeDeleted } = this;
...@@ -178,7 +179,7 @@ export default { ...@@ -178,7 +179,7 @@ export default {
this.requestDeleteTags({ this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name), ids: itemsToBeDeleted.map(x => this.tags[x].name),
imageId: this.$route.params.id, params: this.$route.params.id,
}); });
}, },
onDeletionConfirmed() { onDeletionConfirmed() {
......
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
this.itemToDelete = {}; this.itemToDelete = {};
}, },
encodeListItem(item) { encodeListItem(item) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path }); const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
return window.btoa(params); return window.btoa(params);
}, },
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import { __ } from '~/locale'; import { s__ } from '~/locale';
import List from './pages/list.vue'; import List from './pages/list.vue';
import Details from './pages/details.vue'; import Details from './pages/details.vue';
import { decodeAndParse } from './utils';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -16,7 +17,8 @@ export default function createRouter(base, store) { ...@@ -16,7 +17,8 @@ export default function createRouter(base, store) {
path: '/', path: '/',
component: List, component: List,
meta: { meta: {
name: __('Container Registry'), nameGenerator: () => s__('ContainerRegistry|Container Registry'),
root: true,
}, },
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
store.dispatch('requestImagesList'); store.dispatch('requestImagesList');
...@@ -28,10 +30,10 @@ export default function createRouter(base, store) { ...@@ -28,10 +30,10 @@ export default function createRouter(base, store) {
path: '/:id', path: '/:id',
component: Details, component: Details,
meta: { meta: {
name: __('Tags'), nameGenerator: route => decodeAndParse(route.params.id).name,
}, },
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
store.dispatch('requestTagsList', { id: to.params.id }); store.dispatch('requestTagsList', { params: to.params.id });
next(); next();
}, },
}, },
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
DELETE_IMAGE_ERROR_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE,
DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_SUCCESS_MESSAGE,
} from '../constants'; } from '../constants';
import { decodeAndParse } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
...@@ -43,9 +44,9 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {}) ...@@ -43,9 +44,9 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {})
}); });
}; };
export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => { export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
const { tags_path } = JSON.parse(window.atob(id)); const { tags_path } = decodeAndParse(params);
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination; const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios return axios
...@@ -61,13 +62,13 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) = ...@@ -61,13 +62,13 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) =
}); });
}; };
export const requestDeleteTag = ({ commit, dispatch, state }, { tag, imageId }) => { export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
return axios return axios
.delete(tag.destroy_path) .delete(tag.destroy_path)
.then(() => { .then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success'); createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, id: imageId }); dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE); createFlash(DELETE_TAG_ERROR_MESSAGE);
...@@ -77,15 +78,16 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, imageId }) ...@@ -77,15 +78,16 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, imageId })
}); });
}; };
export const requestDeleteTags = ({ commit, dispatch, state }, { ids, imageId }) => { export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
const url = `/${state.config.projectPath}/registry/repository/${imageId}/tags/bulk_destroy`; const { id } = decodeAndParse(params);
const url = `/${state.config.projectPath}/registry/repository/${id}/tags/bulk_destroy`;
return axios return axios
.delete(url, { params: { ids } }) .delete(url, { params: { ids } })
.then(() => { .then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success'); createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, id: imageId }); dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .catch(() => {
createFlash(DELETE_TAGS_ERROR_MESSAGE); createFlash(DELETE_TAGS_ERROR_MESSAGE);
......
// eslint-disable-next-line import/prefer-default-export
export const decodeAndParse = param => JSON.parse(window.atob(param));
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
eventHub.$emit('EnablePolling'); eventHub.$emit('EnablePolling');
}, },
updateTimer() { updateTimer() {
this.timer = this.timer - 1; this.timer -= 1;
if (this.timer === 0) { if (this.timer === 0) {
this.refresh(); this.refresh();
......
...@@ -192,7 +192,7 @@ ...@@ -192,7 +192,7 @@
.stage-events { .stage-events {
width: 60%; width: 60%;
overflow: scroll; overflow: scroll;
height: 467px; min-height: 467px;
} }
.stage-event-list { .stage-event-list {
......
# frozen_string_literal: true
module Mutations
module Issues
class Update < Base
graphql_name 'UpdateIssue'
# Add arguments here instead of creating separate mutations
def resolve(project_path:, iid:, **args)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project, current_user, args).execute(issue)
{
issue: issue,
errors: issue.errors.full_messages
}
end
end
end
end
Mutations::Issues::Update.prepend_if_ee('::EE::Mutations::Issues::Update')
...@@ -11,6 +11,7 @@ module Types ...@@ -11,6 +11,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetMilestone
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- current_text ||= nil - current_text ||= nil
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) - supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
- qa_selector = local_assigns.fetch(:qa_selector, '')
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
...@@ -10,7 +11,8 @@ ...@@ -10,7 +11,8 @@
placeholder: placeholder, placeholder: placeholder,
dir: 'auto', dir: 'auto',
data: { supports_quick_actions: supports_quick_actions, data: { supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete } supports_autocomplete: supports_autocomplete,
qa_selector: qa_selector }
- else - else
= text_area_tag attr, current_text, class: classes, placeholder: placeholder = text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
......
...@@ -16,10 +16,10 @@ ...@@ -16,10 +16,10 @@
= f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold' = f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold'
.js-collapsible-input .js-collapsible-input
.js-collapsed{ class: ('d-none' if is_expanded) } .js-collapsed{ class: ('d-none' if is_expanded) }
= text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder = text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder, data: { qa_selector: 'description_placeholder' }
.js-expanded{ class: ('d-none' if !is_expanded) } .js-expanded{ class: ('d-none' if !is_expanded) }
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder, qa_selector: 'description_field'
= render 'shared/notes/hints' = render 'shared/notes/hints'
.form-group.file-editor .form-group.file-editor
......
---
title: Time series extends axis options correctly
merge_request: 25399
author:
type: fixed
...@@ -1877,6 +1877,11 @@ type Epic implements Noteable { ...@@ -1877,6 +1877,11 @@ type Epic implements Noteable {
""" """
hasIssues: Boolean! hasIssues: Boolean!
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
""" """
ID of the epic ID of the epic
""" """
...@@ -2252,6 +2257,11 @@ type EpicIssue implements Noteable { ...@@ -2252,6 +2257,11 @@ type EpicIssue implements Noteable {
""" """
epicIssueId: ID! epicIssueId: ID!
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
""" """
Global ID of the epic-issue relation Global ID of the epic-issue relation
""" """
...@@ -3059,6 +3069,15 @@ type GroupPermissions { ...@@ -3059,6 +3069,15 @@ type GroupPermissions {
readGroup: Boolean! readGroup: Boolean!
} }
"""
Health status of an issue or epic
"""
enum HealthStatus {
atRisk
needsAttention
onTrack
}
""" """
State of a GitLab issue or merge request State of a GitLab issue or merge request
""" """
...@@ -3179,6 +3198,11 @@ type Issue implements Noteable { ...@@ -3179,6 +3198,11 @@ type Issue implements Noteable {
""" """
epic: Epic epic: Epic
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
""" """
Internal ID of the issue Internal ID of the issue
""" """
...@@ -4667,6 +4691,7 @@ type Mutation { ...@@ -4667,6 +4691,7 @@ type Mutation {
be destroyed during the update, and no Note will be returned be destroyed during the update, and no Note will be returned
""" """
updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload
updateIssue(input: UpdateIssueInput!): UpdateIssuePayload
""" """
Updates a Note. If the body of the Note contains only quick actions, the Note Updates a Note. If the body of the Note contains only quick actions, the Note
...@@ -7601,6 +7626,11 @@ input UpdateEpicInput { ...@@ -7601,6 +7626,11 @@ input UpdateEpicInput {
""" """
groupPath: ID! groupPath: ID!
"""
The desired health status
"""
healthStatus: HealthStatus
""" """
The iid of the epic to mutate The iid of the epic to mutate
""" """
...@@ -7697,6 +7727,51 @@ type UpdateImageDiffNotePayload { ...@@ -7697,6 +7727,51 @@ type UpdateImageDiffNotePayload {
note: Note note: Note
} }
"""
Autogenerated input type of UpdateIssue
"""
input UpdateIssueInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The iid of the issue to mutate
"""
iid: String!
"""
The project the issue to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of UpdateIssue
"""
type UpdateIssuePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
""" """
Autogenerated input type of UpdateNote Autogenerated input type of UpdateNote
""" """
......
...@@ -11162,6 +11162,35 @@ ...@@ -11162,6 +11162,35 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "ENUM",
"name": "HealthStatus",
"description": "Health status of an issue or epic",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "onTrack",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "needsAttention",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "atRisk",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "DesignConnection", "name": "DesignConnection",
...@@ -19453,6 +19482,33 @@ ...@@ -19453,6 +19482,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "updateIssue",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateIssueInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateIssuePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "updateNote", "name": "updateNote",
"description": "Updates a Note. If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned", "description": "Updates a Note. If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned",
...@@ -20262,6 +20318,132 @@ ...@@ -20262,6 +20318,132 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "UpdateIssuePayload",
"description": "Autogenerated return type of UpdateIssue",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issue",
"description": "The issue after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateIssueInput",
"description": "Autogenerated input type of UpdateIssue",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the issue to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The iid of the issue to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "healthStatus",
"description": "The desired health status",
"type": {
"kind": "ENUM",
"name": "HealthStatus",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "MergeRequestSetLabelsPayload", "name": "MergeRequestSetLabelsPayload",
...@@ -23906,6 +24088,16 @@ ...@@ -23906,6 +24088,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "healthStatus",
"description": "The desired health status",
"type": {
"kind": "ENUM",
"name": "HealthStatus",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "clientMutationId", "name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.", "description": "A unique identifier for the client performing the mutation.",
......
...@@ -293,6 +293,7 @@ Represents an epic. ...@@ -293,6 +293,7 @@ Represents an epic.
| `group` | Group! | Group to which the epic belongs | | `group` | Group! | Group to which the epic belongs |
| `hasChildren` | Boolean! | Indicates if the epic has children | | `hasChildren` | Boolean! | Indicates if the epic has children |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues | | `hasIssues` | Boolean! | Indicates if the epic has direct issues |
| `healthStatus` | HealthStatus | Current health status. Available only when feature flag save_issuable_health_status is enabled. |
| `id` | ID! | ID of the epic | | `id` | ID! | ID of the epic |
| `iid` | ID! | Internal ID of the epic | | `iid` | ID! | Internal ID of the epic |
| `parent` | Epic | Parent epic of the epic | | `parent` | Epic | Parent epic of the epic |
...@@ -342,6 +343,7 @@ Relationship between an epic and an issue ...@@ -342,6 +343,7 @@ Relationship between an epic and an issue
| `dueDate` | Time | Due date of the issue | | `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs | | `epic` | Epic | Epic to which this issue belongs |
| `epicIssueId` | ID! | ID of the epic-issue relation | | `epicIssueId` | ID! | ID of the epic-issue relation |
| `healthStatus` | HealthStatus | Current health status. Available only when feature flag save_issuable_health_status is enabled. |
| `id` | ID | Global ID of the epic-issue relation | | `id` | ID | Global ID of the epic-issue relation |
| `iid` | ID! | Internal ID of the issue | | `iid` | ID! | Internal ID of the issue |
| `milestone` | Milestone | Milestone of the issue | | `milestone` | Milestone | Milestone of the issue |
...@@ -461,6 +463,7 @@ Autogenerated return type of EpicTreeReorder ...@@ -461,6 +463,7 @@ Autogenerated return type of EpicTreeReorder
| `downvotes` | Int! | Number of downvotes the issue has received | | `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue | | `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs | | `epic` | Epic | Epic to which this issue belongs |
| `healthStatus` | HealthStatus | Current health status. Available only when feature flag save_issuable_health_status is enabled. |
| `iid` | ID! | Internal ID of the issue | | `iid` | ID! | Internal ID of the issue |
| `milestone` | Milestone | Milestone of the issue | | `milestone` | Milestone | Milestone of the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default | | `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
...@@ -1240,6 +1243,16 @@ Autogenerated return type of UpdateImageDiffNote ...@@ -1240,6 +1243,16 @@ Autogenerated return type of UpdateImageDiffNote
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation | | `note` | Note | The note after mutation |
## UpdateIssuePayload
Autogenerated return type of UpdateIssue
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `issue` | Issue | The issue after mutation |
## UpdateNotePayload ## UpdateNotePayload
Autogenerated return type of UpdateNote Autogenerated return type of UpdateNote
......
...@@ -166,6 +166,43 @@ In the following example: ...@@ -166,6 +166,43 @@ In the following example:
![Collapsible sections](img/collapsible_log_v12_6.png) ![Collapsible sections](img/collapsible_log_v12_6.png)
#### Custom collapsible sections
You can create collapsible sections in job logs by manually outputting special codes
that GitLab will use to determine what sections to collapse:
- Section start marker: `section_start:UNIX_TIMESTAMP:SECTION_NAME\r\e[0K` + `TEXT_OF_SECTION_HEADER`
- Section end marker: `section_end:UNIX_TIMESTAMP:SECTION_NAME\r\e[0K`
You must add these codes to the script section of the CI configuration. For example,
using `echo`:
```yaml
job1:
script:
- echo -e "section_start:`date +%s`:my_first_section\r\e[0KHeader of the 1st collapsible section"
- echo 'this line should be hidden when collapsed'
- echo -e "section_end:`date +%s`:my_first_section\r\e[0K"
```
In the example above:
- `date +%s`: The Unix timestamp (for example `1560896352`).
- `my_first_section`: The name given to the section.
- `\r\e[0K`: Prevents the section markers from displaying in the rendered (colored)
job log, but they are displayed in the raw job log. To see them, in the top right
of the job log, click **{doc-text}** (**Show complete raw**).
- `\r`: carriage return.
- `\e[0K`: clear line ANSI escape code.
Sample raw job log:
```plaintext
section_start:1560896352:my_first_section\r\e[0KHeader of the 1st collapsible section
this line should be hidden when collapsed
section_end:1560896353:my_first_section\r\e[0K
```
## Configuring pipelines ## Configuring pipelines
Pipelines, and their component jobs and stages, are defined in the [`.gitlab-ci.yml`](yaml/README.md) file for each project. Pipelines, and their component jobs and stages, are defined in the [`.gitlab-ci.yml`](yaml/README.md) file for each project.
......
...@@ -23,6 +23,7 @@ There are two places defined variables can be used. On the: ...@@ -23,6 +23,7 @@ There are two places defined variables can be used. On the:
|:-------------------------------------------|:-----------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:-------------------------------------------|:-----------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<br/><br/>Supported are all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules).<br/><br/>Not supported are variables defined in Runner's `config.toml` and variables created in job's `script`. | | `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<br/><br/>Supported are all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules).<br/><br/>Not supported are variables defined in Runner's `config.toml` and variables created in job's `script`. |
| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | | `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). |
| `resource_group` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). |
| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
......
...@@ -52,6 +52,30 @@ bundle exec rails db RAILS_ENV=development ...@@ -52,6 +52,30 @@ bundle exec rails db RAILS_ENV=development
- `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run - `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run
- `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration - `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration
## Access the GDK database with Visual Studio Code
Use these instructions for exploring the GitLab database while developing with the GDK:
1. Install or open [Visual Studio Code](https://code.visualstudio.com/download).
1. Install the [PostgreSQL VSCode Extension](https://marketplace.visualstudio.com/items?itemName=ckolkman.vscode-postgres) by Chris Kolkman.
1. In Visual Studio Code click on the PostgreSQL Explorer button in the left toolbar.
1. In the top bar of the new window, click on the `+` to **Add Database Connection**, and follow the prompts to fill in the details:
1. **Hostname**: the path to the PostgreSQL folder in your GDK directory (for example `/dev/gitlab-development-kit/postgresql`).
1. **PostgreSQL user to authenticate as**: usually your local username, unless otherwise specified during PostgreSQL installation.
1. **Password of the PostgreSQL user**: the password you set when installing PostgreSQL.
1. **Port number to connect to**: `5432` (default).
1. **Use an ssl connection?** This depends on your installation. Options are:
- **Use Secure Connection**
- **Standard Connection** (default)
1. **(Optional) The database to connect to**: `gitlabhq_development`.
1. **The display name for the database connection**: `gitlabhq_development`.
Your database connection should now be displayed in the PostgreSQL Explorer pane and
you can explore the `gitlabhq_development` database. If you cannot connect, ensure
that GDK is running. For further instructions on how to use the PostgreSQL Explorer
Extension for Visual Studio Code, read the [usage section](https://marketplace.visualstudio.com/items?itemName=ckolkman.vscode-postgres#usage)
of the extension documentation.
## FAQ ## FAQ
### `ActiveRecord::PendingMigrationError` with Spring ### `ActiveRecord::PendingMigrationError` with Spring
......
# Event tracking # Product Analytics
At GitLab, we encourage event tracking so we can iterate on and improve the project and user experience. At GitLab, we encourage event tracking so we can iterate on and improve the project and user experience.
...@@ -44,6 +44,8 @@ From the backend, the events that are tracked will likely consist of things like ...@@ -44,6 +44,8 @@ From the backend, the events that are tracked will likely consist of things like
See [Backend tracking guide](backend.md). See [Backend tracking guide](backend.md).
Also, see [Application performance metrics](../instrumentation.md) if you are after instrumenting application performance metrics.
## Enabling tracking ## Enabling tracking
Tracking can be enabled at: Tracking can be enabled at:
......
# Instrumenting Ruby Code # Application Performance Metrics for Ruby Code
[GitLab Performance Monitoring](../administration/monitoring/performance/index.md) allows instrumenting of both methods and custom [GitLab Performance Monitoring](../administration/monitoring/performance/index.md) allows instrumenting of both methods and custom
blocks of Ruby code. Method instrumentation is the primary form of blocks of Ruby code. Method instrumentation is the primary form of
instrumentation with block-based instrumentation only being used when we want to instrumentation with block-based instrumentation only being used when we want to
drill down to specific regions of code within a method. drill down to specific regions of code within a method.
Please refer to [Product analytics](event_tracking/index.md) if you are after tracking product usage patterns.
## Instrumenting Methods ## Instrumenting Methods
Instrumenting methods is done by using the `Gitlab::Metrics::Instrumentation` Instrumenting methods is done by using the `Gitlab::Metrics::Instrumentation`
......
...@@ -5,6 +5,7 @@ module Gitlab ...@@ -5,6 +5,7 @@ module Gitlab
class Blob class Blob
include Gitlab::BlobHelper include Gitlab::BlobHelper
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
include Gitlab::Metrics::Methods
extend Gitlab::Git::WrapsGitalyErrors extend Gitlab::Git::WrapsGitalyErrors
# This number is the maximum amount of data that we want to display to # This number is the maximum amount of data that we want to display to
...@@ -26,6 +27,14 @@ module Gitlab ...@@ -26,6 +27,14 @@ module Gitlab
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
define_counter :gitlab_blob_truncated_true do
docstring 'blob.truncated? == true'
end
define_counter :gitlab_blob_truncated_false do
docstring 'blob.truncated? == false'
end
class << self class << self
def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE) def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
tree_entry(repository, sha, path, limit) tree_entry(repository, sha, path, limit)
......
...@@ -1538,6 +1538,9 @@ msgstr "" ...@@ -1538,6 +1538,9 @@ msgstr ""
msgid "All changes are committed" msgid "All changes are committed"
msgstr "" msgstr ""
msgid "All default stages are currently visible"
msgstr ""
msgid "All email addresses will be used to identify your commits." msgid "All email addresses will be used to identify your commits."
msgstr "" msgstr ""
...@@ -6110,6 +6113,9 @@ msgstr "" ...@@ -6110,6 +6113,9 @@ msgstr ""
msgid "Default projects limit" msgid "Default projects limit"
msgstr "" msgstr ""
msgid "Default stages"
msgstr ""
msgid "Default: Directly import the Google Code email address or username" msgid "Default: Directly import the Google Code email address or username"
msgstr "" msgstr ""
...@@ -15727,6 +15733,9 @@ msgstr "" ...@@ -15727,6 +15733,9 @@ msgstr ""
msgid "Recipe" msgid "Recipe"
msgstr "" msgstr ""
msgid "Recover hidden stage"
msgstr ""
msgid "Recovery Codes" msgid "Recovery Codes"
msgstr "" msgstr ""
...@@ -21561,6 +21570,30 @@ msgstr "" ...@@ -21561,6 +21570,30 @@ msgstr ""
msgid "VulnerabilityChart|Severity" msgid "VulnerabilityChart|Severity"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|A true-positive and will fix"
msgstr ""
msgid "VulnerabilityManagement|Change status"
msgstr ""
msgid "VulnerabilityManagement|Confirm"
msgstr ""
msgid "VulnerabilityManagement|Dismiss"
msgstr ""
msgid "VulnerabilityManagement|Resolved"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr ""
msgid "VulnerabilityManagement|Verified as fixed or mitigated"
msgstr ""
msgid "VulnerabilityManagement|Will not fix or a false-positive"
msgstr ""
msgid "Vulnerability|Class" msgid "Vulnerability|Class"
msgstr "" msgstr ""
......
...@@ -10,17 +10,25 @@ module QA ...@@ -10,17 +10,25 @@ module QA
end end
view 'app/views/shared/snippets/_form.html.haml' do view 'app/views/shared/snippets/_form.html.haml' do
element :description_field
element :description_placeholder
element :snippet_title element :snippet_title
element :snippet_file_name element :snippet_file_name
element :create_snippet_button element :create_snippet_button
end end
view 'app/views/projects/_zen.html.haml' do
# This 'element' is here only to ensure the changes in the view source aren't mistakenly changed
element :_, "qa_selector = local_assigns.fetch(:qa_selector, '')" # rubocop:disable QA/ElementWithPattern
end
def fill_title(title) def fill_title(title)
fill_element :snippet_title, title fill_element :snippet_title, title
end end
def fill_description(description) def fill_description(description)
fill_element :issuable_form_description, description click_element :description_placeholder
fill_element :description_field, description
end end
def set_visibility(visibility) def set_visibility(visibility)
......
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
context 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/205511', type: :bug } do context 'Create', :smoke do
describe 'Snippet creation' do describe 'Snippet creation' do
it 'User creates a snippet' do it 'User creates a snippet' do
Flow::Login.sign_in Flow::Login.sign_in
......
...@@ -6,6 +6,12 @@ plugins: ...@@ -6,6 +6,12 @@ plugins:
extends: extends:
- 'plugin:jest/recommended' - 'plugin:jest/recommended'
settings: settings:
# We have to teach eslint-plugin-import what node modules we use
# otherwise there is an error when it tries to resolve them
import/core-modules:
- events
- fs
- path
import/resolver: import/resolver:
jest: jest:
jestConfigFile: 'jest.config.js' jestConfigFile: 'jest.config.js'
......
/* eslint-disable arrow-body-style */
import $ from 'jquery'; import $ from 'jquery';
import GlFieldErrors from '~/gl_field_errors'; import GlFieldErrors from '~/gl_field_errors';
......
...@@ -358,6 +358,45 @@ describe('Time series component', () => { ...@@ -358,6 +358,45 @@ describe('Time series component', () => {
expect(optionSeries[0].name).toEqual(mockSeriesName); expect(optionSeries[0].name).toEqual(mockSeriesName);
}); });
}); });
it('additional y axis data', () => {
const mockCustomYAxisOption = {
name: 'Custom y axis label',
axisLabel: {
formatter: jest.fn(),
},
};
timeSeriesChart.setProps({
option: {
yAxis: mockCustomYAxisOption,
},
});
return timeSeriesChart.vm.$nextTick().then(() => {
const { yAxis } = getChartOptions();
expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
});
});
it('additional x axis data', () => {
const mockCustomXAxisOption = {
name: 'Custom x axis label',
};
timeSeriesChart.setProps({
option: {
xAxis: mockCustomXAxisOption,
},
});
return timeSeriesChart.vm.$nextTick().then(() => {
const { xAxis } = getChartOptions();
expect(xAxis).toMatchObject(mockCustomXAxisOption);
});
});
}); });
describe('yAxis formatter', () => { describe('yAxis formatter', () => {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<ul>
<li
class="foo bar"
>
baz
</li>
<li
class="foo bar"
>
foo
</li>
<!---->
<li>
<a
class="foo"
>
<a>
</a>
</a>
</li>
</ul>
`;
import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/registry_breadcrumb.vue';
describe('Registry Breadcrumb', () => {
let wrapper;
const nameGenerator = jest.fn();
const crumb = {
classList: ['foo', 'bar'],
tagName: 'div',
innerHTML: 'baz',
querySelector: jest.fn(),
children: [
{
tagName: 'a',
classList: ['foo'],
},
],
};
const querySelectorReturnValue = {
classList: ['js-divider'],
tagName: 'svg',
innerHTML: 'foo',
};
const crumbs = [crumb, { ...crumb, innerHTML: 'foo' }, { ...crumb, classList: ['baz'] }];
const routes = [
{ name: 'foo', meta: { nameGenerator, root: true } },
{ name: 'baz', meta: { nameGenerator } },
];
const findDivider = () => wrapper.find('.js-divider');
const findRootRoute = () => wrapper.find({ ref: 'rootRouteLink' });
const findChildRoute = () => wrapper.find({ ref: 'childRouteLink' });
const findLastCrumb = () => wrapper.find({ ref: 'lastCrumb' });
const mountComponent = $route => {
wrapper = shallowMount(component, {
propsData: {
crumbs,
},
stubs: {
'router-link': { name: 'router-link', template: '<a><slot></slot></a>', props: ['to'] },
},
mocks: {
$route,
$router: {
options: {
routes,
},
},
},
});
};
beforeEach(() => {
nameGenerator.mockClear();
crumb.querySelector = jest.fn();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when is rootRoute', () => {
beforeEach(() => {
mountComponent(routes[0]);
});
it('renders', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('contains a router-link for the child route', () => {
expect(findChildRoute().exists()).toBe(true);
});
it('the link text is calculated by nameGenerator', () => {
expect(nameGenerator).toHaveBeenCalledWith(routes[0]);
expect(nameGenerator).toHaveBeenCalledTimes(1);
});
});
describe('when is not rootRoute', () => {
beforeEach(() => {
crumb.querySelector.mockReturnValue(querySelectorReturnValue);
mountComponent(routes[1]);
});
it('renders a divider', () => {
expect(findDivider().exists()).toBe(true);
});
it('contains a router-link for the root route', () => {
expect(findRootRoute().exists()).toBe(true);
});
it('contains a router-link for the child route', () => {
expect(findChildRoute().exists()).toBe(true);
});
it('the link text is calculated by nameGenerator', () => {
expect(nameGenerator).toHaveBeenCalledWith(routes[1]);
expect(nameGenerator).toHaveBeenCalledTimes(2);
});
});
describe('last crumb', () => {
const lastChildren = crumb.children[0];
beforeEach(() => {
nameGenerator.mockReturnValue('foo');
mountComponent(routes[0]);
});
it('has the same tag as the last children of the crumbs', () => {
expect(findLastCrumb().is(lastChildren.tagName)).toBe(true);
});
it('has the same classes as the last children of the crumbs', () => {
expect(findLastCrumb().classes()).toEqual(lastChildren.classList);
});
it('has a link to the current route', () => {
expect(findChildRoute().props('to')).toEqual({ to: routes[0].name });
});
it('the link has the correct text', () => {
expect(findChildRoute().text()).toEqual('foo');
});
});
});
...@@ -254,7 +254,7 @@ describe('Details Page', () => { ...@@ -254,7 +254,7 @@ describe('Details Page', () => {
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', { expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
tag: store.state.tags[0], tag: store.state.tags[0],
imageId: wrapper.vm.$route.params.id, params: wrapper.vm.$route.params.id,
}); });
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
...@@ -271,7 +271,7 @@ describe('Details Page', () => { ...@@ -271,7 +271,7 @@ describe('Details Page', () => {
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', { expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
ids: store.state.tags.map(t => t.name), ids: store.state.tags.map(t => t.name),
imageId: wrapper.vm.$route.params.id, params: wrapper.vm.$route.params.id,
}); });
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
......
...@@ -121,14 +121,14 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -121,14 +121,14 @@ describe('Actions RegistryExplorer Store', () => {
describe('fetch tags list', () => { describe('fetch tags list', () => {
const url = `${endpoint}/1}`; const url = `${endpoint}/1}`;
const path = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}` })); const params = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}` }));
it('sets the tagsList', done => { it('sets the tagsList', done => {
mock.onGet(url).replyOnce(200, registryServerResponse, {}); mock.onGet(url).replyOnce(200, registryServerResponse, {});
testAction( testAction(
actions.requestTagsList, actions.requestTagsList,
{ id: path }, { params },
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
...@@ -147,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -147,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => {
it('should create flash on error', done => { it('should create flash on error', done => {
testAction( testAction(
actions.requestTagsList, actions.requestTagsList,
{ id: path }, { params },
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
...@@ -165,7 +165,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -165,7 +165,7 @@ describe('Actions RegistryExplorer Store', () => {
describe('request delete single tag', () => { describe('request delete single tag', () => {
it('successfully performs the delete request', done => { it('successfully performs the delete request', done => {
const deletePath = 'delete/path'; const deletePath = 'delete/path';
const url = window.btoa(`${endpoint}/1}`); const params = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}`, id: 1 }));
mock.onDelete(deletePath).replyOnce(200); mock.onDelete(deletePath).replyOnce(200);
...@@ -175,7 +175,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -175,7 +175,7 @@ describe('Actions RegistryExplorer Store', () => {
tag: { tag: {
destroy_path: deletePath, destroy_path: deletePath,
}, },
imageId: url, params,
}, },
{ {
tagsPagination: {}, tagsPagination: {},
...@@ -187,7 +187,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -187,7 +187,7 @@ describe('Actions RegistryExplorer Store', () => {
[ [
{ {
type: 'requestTagsList', type: 'requestTagsList',
payload: { pagination: {}, id: url }, payload: { pagination: {}, params },
}, },
], ],
() => { () => {
...@@ -220,9 +220,10 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -220,9 +220,10 @@ describe('Actions RegistryExplorer Store', () => {
}); });
describe('request delete multiple tags', () => { describe('request delete multiple tags', () => {
const imageId = 1; const id = 1;
const params = window.btoa(JSON.stringify({ id }));
const projectPath = 'project-path'; const projectPath = 'project-path';
const url = `${projectPath}/registry/repository/${imageId}/tags/bulk_destroy`; const url = `${projectPath}/registry/repository/${id}/tags/bulk_destroy`;
it('successfully performs the delete request', done => { it('successfully performs the delete request', done => {
mock.onDelete(url).replyOnce(200); mock.onDelete(url).replyOnce(200);
...@@ -231,7 +232,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -231,7 +232,7 @@ describe('Actions RegistryExplorer Store', () => {
actions.requestDeleteTags, actions.requestDeleteTags,
{ {
ids: [1, 2], ids: [1, 2],
imageId, params,
}, },
{ {
config: { config: {
...@@ -246,7 +247,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -246,7 +247,7 @@ describe('Actions RegistryExplorer Store', () => {
[ [
{ {
type: 'requestTagsList', type: 'requestTagsList',
payload: { pagination: {}, id: 1 }, payload: { pagination: {}, params },
}, },
], ],
() => { () => {
...@@ -263,7 +264,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -263,7 +264,7 @@ describe('Actions RegistryExplorer Store', () => {
actions.requestDeleteTags, actions.requestDeleteTags,
{ {
ids: [1, 2], ids: [1, 2],
imageId, params,
}, },
{ {
config: { config: {
......
...@@ -18,7 +18,7 @@ describe('IssueAssigneesComponent', () => { ...@@ -18,7 +18,7 @@ describe('IssueAssigneesComponent', () => {
...props, ...props,
}, },
}); });
vm = wrapper.vm; // eslint-disable-line vm = wrapper.vm;
}; };
const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text(); const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
......
...@@ -588,4 +588,14 @@ describe Gitlab::Git::Blob, :seed_helper do ...@@ -588,4 +588,14 @@ describe Gitlab::Git::Blob, :seed_helper do
end end
end end
end end
describe 'metrics' do
it 'defines :gitlab_blob_truncated_true counter' do
expect(described_class).to respond_to(:gitlab_blob_truncated_true)
end
it 'defines :gitlab_blob_truncated_false counter' do
expect(described_class).to respond_to(:gitlab_blob_truncated_false)
end
end
end end
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册