Commit 4720b569 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent cefe554b
......@@ -48,8 +48,10 @@ rules:
no-jquery/no-serialize: error
promise/always-return: off
promise/no-callback-in-promise: off
# Make update to eslint@6 smoother:
prefer-object-spread: off
overrides:
files:
- files:
- '**/spec/**/*'
rules:
"@gitlab/i18n/no-non-i18n-strings": off
/* eslint-disable max-classes-per-file */
import $ from 'jquery';
import Pikaday from 'pikaday';
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 'underscore';
......
<script>
import _ from 'underscore';
import { escape as esc, isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
......@@ -43,7 +43,7 @@ export default {
'%{startLink}%{name}%{endLink}',
{
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>',
},
false,
......@@ -58,10 +58,10 @@ export default {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
},
hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment);
return !isEmpty(this.deploymentStatus.environment);
},
lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable)
return !isEmpty(this.lastDeployment.deployable)
? this.lastDeployment.deployable.build_path
: '';
},
......@@ -74,8 +74,8 @@ export default {
}
const { name, path } = this.deploymentCluster;
const escapedName = _.escape(name);
const escapedPath = _.escape(path);
const escapedName = esc(name);
const escapedPath = esc(path);
if (!escapedPath) {
return escapedName;
......
<script>
import _ from 'underscore';
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -21,7 +21,7 @@ export default {
},
computed: {
isErasedByUser() {
return !_.isEmpty(this.user);
return !isEmpty(this.user);
},
},
};
......
<script>
import _ from 'underscore';
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
......@@ -125,7 +125,7 @@ export default {
// Once the job log is loaded,
// fetch the stages for the dropdown on the sidebar
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
if (isEmpty(oldVal) && !isEmpty(newVal.pipeline)) {
const stages = this.job.pipeline.details.stages || [];
const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage);
......@@ -145,7 +145,7 @@ export default {
},
},
created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.throttled = throttle(this.toggleScrollButtons, 100);
window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll);
......
<script>
import _ from 'underscore';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
......@@ -19,7 +19,9 @@ export default {
validator(value) {
return (
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 {
const newVariable = {
key: this.key,
secret_value: this.secretValue,
id: _.uniqueId(),
id: uniqueId(),
};
this.variables.push(newVariable);
......
<script>
import _ from 'underscore';
import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
......@@ -84,10 +84,10 @@ export default {
);
},
hasArtifact() {
return !_.isEmpty(this.job.artifact);
return !isEmpty(this.job.artifact);
},
hasTriggers() {
return !_.isEmpty(this.job.trigger);
return !isEmpty(this.job.trigger);
},
hasStages() {
return (
......
<script>
import _ from 'underscore';
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
......@@ -24,7 +24,7 @@ export default {
},
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
return !isEmpty(this.pipeline.ref);
},
isTriggeredByMergeRequest() {
return Boolean(this.pipeline.merge_request);
......
import _ from 'underscore';
import { isEmpty, isString } from 'lodash';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
......@@ -7,15 +7,15 @@ export const hasUnmetPrerequisitesFailure = state =>
state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites';
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 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.
......@@ -23,7 +23,7 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* @returns {Boolean}
*/
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 =>
(state.job && state.job.status && state.job.status.illustration) || {};
......@@ -38,8 +38,8 @@ export const emptyStateAction = state =>
* @returns {Boolean}
*/
export const shouldRenderSharedRunnerLimitWarning = state =>
!_.isEmpty(state.job.runners) &&
!_.isEmpty(state.job.runners.quota) &&
!isEmpty(state.job.runners) &&
!isEmpty(state.job.runners.quota) &&
state.job.runners.quota.used >= state.job.runners.quota.limit;
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
......
......@@ -127,7 +127,6 @@ export default {
});
const yAxisWithOffset = {
name: this.yAxisLabel,
axisLabel: {
formatter: num => roundOffFloat(num - this.yOffset, 3).toString(),
},
......@@ -162,6 +161,7 @@ export default {
}),
);
}
return { yAxis: yAxisWithOffset, series: boundarySeries };
},
},
......
......@@ -162,7 +162,8 @@ export default {
);
},
chartOptions() {
const option = omit(this.option, 'series');
const { yAxis, xAxis } = this.option;
const option = omit(this.option, ['series', 'yAxis', 'xAxis']);
const dataYAxis = {
name: this.yAxisLabel,
......@@ -173,7 +174,9 @@ export default {
axisLabel: {
formatter: num => roundOffFloat(num, 3).toString(),
},
...yAxis,
};
const deploymentsYAxis = {
show: false,
min: deploymentYAxisCoords.min,
......@@ -184,9 +187,7 @@ export default {
},
};
return {
series: this.chartOptionSeries,
xAxis: {
const timeXAxis = {
name: __('Time'),
type: 'time',
axisLabel: {
......@@ -195,7 +196,12 @@ export default {
axisPointer: {
snap: true,
},
},
...xAxis,
};
return {
series: this.chartOptionSeries,
xAxis: timeXAxis,
yAxis: [dataYAxis, deploymentsYAxis],
dataZoom: [this.dataZoomConfig],
...option,
......
......@@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index';
document.addEventListener('DOMContentLoaded', () => {
initRegistryImages();
registryExplorer();
const { attachMainComponent, attachBreadcrumb } = registryExplorer();
attachBreadcrumb();
attachMainComponent();
});
......@@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index';
document.addEventListener('DOMContentLoaded', () => {
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 Translate from '~/vue_shared/translate';
import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
import { createStore } from './stores';
import createRouter from './router';
......@@ -19,7 +20,8 @@ export default () => {
const router = createRouter(endpoint, store);
store.dispatch('setInitialState', el.dataset);
return new Vue({
const attachMainComponent = () =>
new Vue({
el,
store,
router,
......@@ -30,4 +32,27 @@ export default () => {
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';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import Tracking from '~/tracking';
import { decodeAndParse } from '../utils';
import {
LIST_KEY_TAG,
LIST_KEY_IMAGE_ID,
......@@ -62,7 +63,7 @@ export default {
computed: {
...mapState(['tags', 'tagsPagination', 'isLoading', 'config']),
imageName() {
const { name } = JSON.parse(window.atob(this.$route.params.id));
const { name } = decodeAndParse(this.$route.params.id);
return name;
},
fields() {
......@@ -169,7 +170,7 @@ export default {
},
handleSingleDelete(itemToDelete) {
this.itemsToBeDeleted = [];
this.requestDeleteTag({ tag: itemToDelete, imageId: this.$route.params.id });
this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id });
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
......@@ -178,7 +179,7 @@ export default {
this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name),
imageId: this.$route.params.id,
params: this.$route.params.id,
});
},
onDeletionConfirmed() {
......
......@@ -70,7 +70,7 @@ export default {
this.itemToDelete = {};
},
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);
},
},
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import { __ } from '~/locale';
import { s__ } from '~/locale';
import List from './pages/list.vue';
import Details from './pages/details.vue';
import { decodeAndParse } from './utils';
Vue.use(VueRouter);
......@@ -16,7 +17,8 @@ export default function createRouter(base, store) {
path: '/',
component: List,
meta: {
name: __('Container Registry'),
nameGenerator: () => s__('ContainerRegistry|Container Registry'),
root: true,
},
beforeEnter: (to, from, next) => {
store.dispatch('requestImagesList');
......@@ -28,10 +30,10 @@ export default function createRouter(base, store) {
path: '/:id',
component: Details,
meta: {
name: __('Tags'),
nameGenerator: route => decodeAndParse(route.params.id).name,
},
beforeEnter: (to, from, next) => {
store.dispatch('requestTagsList', { id: to.params.id });
store.dispatch('requestTagsList', { params: to.params.id });
next();
},
},
......
......@@ -13,6 +13,7 @@ import {
DELETE_IMAGE_ERROR_MESSAGE,
DELETE_IMAGE_SUCCESS_MESSAGE,
} from '../constants';
import { decodeAndParse } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
......@@ -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);
const { tags_path } = JSON.parse(window.atob(id));
const { tags_path } = decodeAndParse(params);
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios
......@@ -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);
return axios
.delete(tag.destroy_path)
.then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, id: imageId });
dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE);
......@@ -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);
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
.delete(url, { params: { ids } })
.then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, id: imageId });
dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
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 {
eventHub.$emit('EnablePolling');
},
updateTimer() {
this.timer = this.timer - 1;
this.timer -= 1;
if (this.timer === 0) {
this.refresh();
......
......@@ -192,7 +192,7 @@
.stage-events {
width: 60%;
overflow: scroll;
height: 467px;
min-height: 467px;
}
.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
mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone
......
......@@ -2,6 +2,7 @@
- current_text ||= nil
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
- qa_selector = local_assigns.fetch(:qa_selector, '')
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
......@@ -10,7 +11,8 @@
placeholder: placeholder,
dir: 'auto',
data: { supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete }
supports_autocomplete: supports_autocomplete,
qa_selector: qa_selector }
- else
= text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
......
......@@ -16,10 +16,10 @@
= f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold'
.js-collapsible-input
.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) }
= 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'
.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 {
"""
hasIssues: Boolean!
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
"""
ID of the epic
"""
......@@ -2252,6 +2257,11 @@ type EpicIssue implements Noteable {
"""
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
"""
......@@ -3059,6 +3069,15 @@ type GroupPermissions {
readGroup: Boolean!
}
"""
Health status of an issue or epic
"""
enum HealthStatus {
atRisk
needsAttention
onTrack
}
"""
State of a GitLab issue or merge request
"""
......@@ -3179,6 +3198,11 @@ type Issue implements Noteable {
"""
epic: Epic
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
"""
Internal ID of the issue
"""
......@@ -4667,6 +4691,7 @@ type Mutation {
be destroyed during the update, and no Note will be returned
"""
updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload
updateIssue(input: UpdateIssueInput!): UpdateIssuePayload
"""
Updates a Note. If the body of the Note contains only quick actions, the Note
......@@ -7601,6 +7626,11 @@ input UpdateEpicInput {
"""
groupPath: ID!
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The iid of the epic to mutate
"""
......@@ -7697,6 +7727,51 @@ type UpdateImageDiffNotePayload {
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
"""
......
......@@ -11162,6 +11162,35 @@
"enumValues": 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",
"name": "DesignConnection",
......@@ -19453,6 +19482,33 @@
"isDeprecated": false,
"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",
"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 @@
"enumValues": 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",
"name": "MergeRequestSetLabelsPayload",
......@@ -23906,6 +24088,16 @@
},
"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.",
......
......@@ -293,6 +293,7 @@ Represents an epic.
| `group` | Group! | Group to which the epic belongs |
| `hasChildren` | Boolean! | Indicates if the epic has children |
| `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 |
| `iid` | ID! | Internal ID of the epic |
| `parent` | Epic | Parent epic of the epic |
......@@ -342,6 +343,7 @@ Relationship between an epic and an issue
| `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs |
| `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 |
| `iid` | ID! | Internal ID of the issue |
| `milestone` | Milestone | Milestone of the issue |
......@@ -461,6 +463,7 @@ Autogenerated return type of EpicTreeReorder
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
| `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 |
| `milestone` | Milestone | Milestone of the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
......@@ -1240,6 +1243,16 @@ Autogenerated return type of UpdateImageDiffNote
| `errors` | String! => Array | Reasons why the mutation failed. |
| `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
Autogenerated return type of UpdateNote
......
......@@ -166,6 +166,43 @@ In the following example:
![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
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:
|:-------------------------------------------|:-----------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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). |
| `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) |
| `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) |
......
......@@ -52,6 +52,30 @@ bundle exec rails db RAILS_ENV=development
- `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run
- `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
### `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.
......@@ -44,6 +44,8 @@ From the backend, the events that are tracked will likely consist of things like
See [Backend tracking guide](backend.md).
Also, see [Application performance metrics](../instrumentation.md) if you are after instrumenting application performance metrics.
## Enabling tracking
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
blocks of Ruby code. Method instrumentation is the primary form of
instrumentation with block-based instrumentation only being used when we want to
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 is done by using the `Gitlab::Metrics::Instrumentation`
......
......@@ -5,6 +5,7 @@ module Gitlab
class Blob
include Gitlab::BlobHelper
include Gitlab::EncodingHelper
include Gitlab::Metrics::Methods
extend Gitlab::Git::WrapsGitalyErrors
# This number is the maximum amount of data that we want to display to
......@@ -26,6 +27,14 @@ module Gitlab
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
def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
tree_entry(repository, sha, path, limit)
......
......@@ -1538,6 +1538,9 @@ msgstr ""
msgid "All changes are committed"
msgstr ""
msgid "All default stages are currently visible"
msgstr ""
msgid "All email addresses will be used to identify your commits."
msgstr ""
......@@ -6110,6 +6113,9 @@ msgstr ""
msgid "Default projects limit"
msgstr ""
msgid "Default stages"
msgstr ""
msgid "Default: Directly import the Google Code email address or username"
msgstr ""
......@@ -15727,6 +15733,9 @@ msgstr ""
msgid "Recipe"
msgstr ""
msgid "Recover hidden stage"
msgstr ""
msgid "Recovery Codes"
msgstr ""
......@@ -21561,6 +21570,30 @@ msgstr ""
msgid "VulnerabilityChart|Severity"
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"
msgstr ""
......
......@@ -10,17 +10,25 @@ module QA
end
view 'app/views/shared/snippets/_form.html.haml' do
element :description_field
element :description_placeholder
element :snippet_title
element :snippet_file_name
element :create_snippet_button
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)
fill_element :snippet_title, title
end
def fill_description(description)
fill_element :issuable_form_description, description
click_element :description_placeholder
fill_element :description_field, description
end
def set_visibility(visibility)
......
# frozen_string_literal: true
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
it 'User creates a snippet' do
Flow::Login.sign_in
......
......@@ -6,6 +6,12 @@ plugins:
extends:
- 'plugin:jest/recommended'
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:
jest:
jestConfigFile: 'jest.config.js'
......
/* eslint-disable arrow-body-style */
import $ from 'jquery';
import GlFieldErrors from '~/gl_field_errors';
......
......@@ -358,6 +358,45 @@ describe('Time series component', () => {
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', () => {
......
// 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', () => {
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
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
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
......@@ -271,7 +271,7 @@ describe('Details Page', () => {
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
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
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
......
......@@ -121,14 +121,14 @@ describe('Actions RegistryExplorer Store', () => {
describe('fetch tags list', () => {
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 => {
mock.onGet(url).replyOnce(200, registryServerResponse, {});
testAction(
actions.requestTagsList,
{ id: path },
{ params },
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
......@@ -147,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => {
it('should create flash on error', done => {
testAction(
actions.requestTagsList,
{ id: path },
{ params },
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
......@@ -165,7 +165,7 @@ describe('Actions RegistryExplorer Store', () => {
describe('request delete single tag', () => {
it('successfully performs the delete request', done => {
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);
......@@ -175,7 +175,7 @@ describe('Actions RegistryExplorer Store', () => {
tag: {
destroy_path: deletePath,
},
imageId: url,
params,
},
{
tagsPagination: {},
......@@ -187,7 +187,7 @@ describe('Actions RegistryExplorer Store', () => {
[
{
type: 'requestTagsList',
payload: { pagination: {}, id: url },
payload: { pagination: {}, params },
},
],
() => {
......@@ -220,9 +220,10 @@ describe('Actions RegistryExplorer Store', () => {
});
describe('request delete multiple tags', () => {
const imageId = 1;
const id = 1;
const params = window.btoa(JSON.stringify({ id }));
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 => {
mock.onDelete(url).replyOnce(200);
......@@ -231,7 +232,7 @@ describe('Actions RegistryExplorer Store', () => {
actions.requestDeleteTags,
{
ids: [1, 2],
imageId,
params,
},
{
config: {
......@@ -246,7 +247,7 @@ describe('Actions RegistryExplorer Store', () => {
[
{
type: 'requestTagsList',
payload: { pagination: {}, id: 1 },
payload: { pagination: {}, params },
},
],
() => {
......@@ -263,7 +264,7 @@ describe('Actions RegistryExplorer Store', () => {
actions.requestDeleteTags,
{
ids: [1, 2],
imageId,
params,
},
{
config: {
......
......@@ -18,7 +18,7 @@ describe('IssueAssigneesComponent', () => {
...props,
},
});
vm = wrapper.vm; // eslint-disable-line
vm = wrapper.vm;
};
const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
......
......@@ -588,4 +588,14 @@ describe Gitlab::Git::Blob, :seed_helper do
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
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment