Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-20 21:09:23 +00:00
parent bf213f07c8
commit 192bc8bd31
58 changed files with 810 additions and 405 deletions

View File

@ -55,6 +55,7 @@ const Api = {
adminStatisticsPath: '/api/:version/application/statistics', adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
pipelinesPath: '/api/:version/projects/:id/pipelines/', pipelinesPath: '/api/:version/projects/:id/pipelines/',
createPipelinePath: '/api/:version/projects/:id/pipeline',
environmentsPath: '/api/:version/projects/:id/environments', environmentsPath: '/api/:version/projects/:id/environments',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw', rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
issuePath: '/api/:version/projects/:id/issues/:issue_iid', issuePath: '/api/:version/projects/:id/issues/:issue_iid',
@ -576,6 +577,16 @@ const Api = {
}); });
}, },
createPipeline(id, data) {
const url = Api.buildUrl(this.createPipelinePath).replace(':id', encodeURIComponent(id));
return axios.post(url, data, {
headers: {
'Content-Type': 'application/json',
},
});
},
environments(id) { environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url); return axios.get(url);

View File

@ -49,7 +49,7 @@ const multiMetricLabel = metricAttributes => {
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance) * @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label * @returns {String} The formatted query label
*/ */
export const getSeriesLabel = (queryLabel, metricAttributes) => { const getSeriesLabel = (queryLabel, metricAttributes) => {
return ( return (
singleAttributeLabel(queryLabel, metricAttributes) || singleAttributeLabel(queryLabel, metricAttributes) ||
templatedLabel(queryLabel, metricAttributes) || templatedLabel(queryLabel, metricAttributes) ||
@ -63,6 +63,7 @@ export const getSeriesLabel = (queryLabel, metricAttributes) => {
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name) * @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
* @returns {Array} The formatted values * @returns {Array} The formatted values
*/ */
// eslint-disable-next-line import/prefer-default-export
export const makeDataSeries = (queryResults, defaultConfig) => export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults.map(result => { queryResults.map(result => {
return { return {

View File

@ -30,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue'; import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils'; import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { graphDataToCsv } from '../csv_export';
const events = { const events = {
timeRangeZoom: 'timerangezoom', timeRangeZoom: 'timerangezoom',
@ -149,10 +148,13 @@ export default {
return null; return null;
}, },
csvText() { csvText() {
if (this.graphData) { const chartData = this.graphData?.metrics[0].result[0].values || [];
return graphDataToCsv(this.graphData); const yLabel = this.graphData.y_label;
} const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
return null; return chartData.reduce((csv, data) => {
const row = data.join(',');
return `${csv}${row}\r\n`;
}, header);
}, },
downloadCsv() { downloadCsv() {
const data = new Blob([this.csvText], { type: 'text/plain' }); const data = new Blob([this.csvText], { type: 'text/plain' });

View File

@ -1,147 +0,0 @@
import { getSeriesLabel } from '~/helpers/monitor_helper';
/**
* Returns a label for a header of the csv.
*
* Includes double quotes ("") in case the header includes commas or other separator.
*
* @param {String} axisLabel
* @param {String} metricLabel
* @param {Object} metricAttributes
*/
const csvHeader = (axisLabel, metricLabel, metricAttributes = {}) =>
`${axisLabel} > ${getSeriesLabel(metricLabel, metricAttributes)}`;
/**
* Returns an array with the header labels given a list of metrics
*
* ```
* metrics = [
* {
* label: "..." // user-defined label
* result: [
* {
* metric: { ... } // metricAttributes
* },
* ...
* ]
* },
* ...
* ]
* ```
*
* When metrics have a `label` or `metricAttributes`, they are
* used to generate the column name.
*
* @param {String} axisLabel - Main label
* @param {Array} metrics - Metrics with results
*/
const csvMetricHeaders = (axisLabel, metrics) =>
metrics.flatMap(({ label, result }) =>
// The `metric` in a `result` is a map of `metricAttributes`
// contains key-values to identify the series, rename it
// here for clarity.
result.map(({ metric: metricAttributes }) => {
return csvHeader(axisLabel, label, metricAttributes);
}),
);
/**
* Returns a (flat) array with all the values arrays in each
* metric and series
*
* ```
* metrics = [
* {
* result: [
* {
* values: [ ... ] // `values`
* },
* ...
* ]
* },
* ...
* ]
* ```
*
* @param {Array} metrics - Metrics with results
*/
const csvMetricValues = metrics =>
metrics.flatMap(({ result }) => result.map(res => res.values || []));
/**
* Returns headers and rows for csv, sorted by their timestamp.
*
* {
* headers: ["timestamp", "<col_1_name>", "col_2_name"],
* rows: [
* [ <timestamp>, <col_1_value>, <col_2_value> ],
* [ <timestamp>, <col_1_value>, <col_2_value> ]
* ...
* ]
* }
*
* @param {Array} metricHeaders
* @param {Array} metricValues
*/
const csvData = (metricHeaders, metricValues) => {
const rowsByTimestamp = {};
metricValues.forEach((values, colIndex) => {
values.forEach(([timestamp, value]) => {
if (!rowsByTimestamp[timestamp]) {
rowsByTimestamp[timestamp] = [];
}
// `value` should be in the right column
rowsByTimestamp[timestamp][colIndex] = value;
});
});
const rows = Object.keys(rowsByTimestamp)
.sort()
.map(timestamp => {
// force each row to have the same number of entries
rowsByTimestamp[timestamp].length = metricHeaders.length;
// add timestamp as the first entry
return [timestamp, ...rowsByTimestamp[timestamp]];
});
// Escape double quotes and enclose headers:
// "If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote."
// https://tools.ietf.org/html/rfc4180#page-2
const headers = metricHeaders.map(header => `"${header.replace(/"/g, '""')}"`);
return {
headers: ['timestamp', ...headers],
rows,
};
};
/**
* Returns dashboard panel's data in a string in CSV format
*
* @param {Object} graphData - Panel contents
* @returns {String}
*/
// eslint-disable-next-line import/prefer-default-export
export const graphDataToCsv = graphData => {
const delimiter = ',';
const br = '\r\n';
const { metrics = [], y_label: axisLabel } = graphData;
const metricsWithResults = metrics.filter(metric => metric.result);
const metricHeaders = csvMetricHeaders(axisLabel, metricsWithResults);
const metricValues = csvMetricValues(metricsWithResults);
const { headers, rows } = csvData(metricHeaders, metricValues);
if (rows.length === 0) {
return '';
}
const headerLine = headers.join(delimiter) + br;
const lines = rows.map(row => row.join(delimiter));
return headerLine + lines.join(br) + br;
};

View File

@ -1,12 +1,19 @@
import $ from 'jquery'; import $ from 'jquery';
import NewBranchForm from '~/new_branch_form'; import NewBranchForm from '~/new_branch_form';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
import initNewPipeline from '~/pipeline_new/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new const el = document.getElementById('js-new-pipeline');
setupNativeFormVariableList({ if (el) {
container: $('.js-ci-variable-list-section'), initNewPipeline();
formField: 'variables_attributes', } else {
}); new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'variables_attributes',
});
}
}); });

View File

@ -0,0 +1,247 @@
<script>
import Vue from 'vue';
import { s__, __ } from '~/locale';
import Api from '~/api';
import { redirectTo } from '~/lib/utils/url_utility';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
import { uniqueId } from 'lodash';
import {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLink,
GlNewDropdown,
GlNewDropdownItem,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
export default {
typeOptions: [
{ value: VARIABLE_TYPE, text: __('Variable') },
{ value: FILE_TYPE, text: __('File') },
],
variablesDescription: s__(
'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
),
formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15',
errorTitle: __('The form contains the following error:'),
components: {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLink,
GlNewDropdown,
GlNewDropdownItem,
GlSearchBoxByType,
GlSprintf,
},
props: {
pipelinesPath: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
},
refs: {
type: Array,
required: true,
},
settingsLink: {
type: String,
required: true,
},
fileParams: {
type: Object,
required: false,
default: () => ({}),
},
refParam: {
type: String,
required: false,
default: '',
},
variableParams: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
searchTerm: '',
refValue: this.refParam,
variables: {},
error: false,
};
},
computed: {
filteredRefs() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.refs.filter(ref => ref.toLowerCase().includes(lowerCasedSearchTerm));
},
variablesLength() {
return Object.keys(this.variables).length;
},
},
created() {
if (this.variableParams) {
this.setVariableParams(VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(FILE_TYPE, this.fileParams);
}
this.addEmptyVariable();
},
methods: {
addEmptyVariable() {
this.variables[uniqueId('var')] = {
variable_type: VARIABLE_TYPE,
key: '',
value: '',
};
},
setVariableParams(type, paramsObj) {
Object.entries(paramsObj).forEach(([key, value]) => {
this.variables[uniqueId('var')] = {
key,
value,
variable_type: type,
};
});
},
setRefSelected(ref) {
this.refValue = ref;
},
isSelected(ref) {
return ref === this.refValue;
},
insertNewVariable() {
Vue.set(this.variables, uniqueId('var'), {
variable_type: VARIABLE_TYPE,
key: '',
value: '',
});
},
removeVariable(key) {
Vue.delete(this.variables, key);
},
canRemove(index) {
return index < this.variablesLength - 1;
},
createPipeline() {
const filteredVariables = Object.values(this.variables).filter(
({ key, value }) => key !== '' && value !== '',
);
return Api.createPipeline(this.projectId, {
ref: this.refValue,
variables: filteredVariables,
})
.then(({ data }) => redirectTo(data.web_url))
.catch(err => {
this.error = err.response.data.message.base;
});
},
},
};
</script>
<template>
<gl-form @submit.prevent="createPipeline">
<gl-alert
v-if="error"
:title="$options.errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-4"
>{{ error }}</gl-alert
>
<gl-form-group :label="s__('Pipeline|Run for')">
<gl-new-dropdown :text="refValue" block>
<gl-search-box-by-type
v-model.trim="searchTerm"
:placeholder="__('Search branches and tags')"
class="gl-p-2"
/>
<gl-new-dropdown-item
v-for="(ref, index) in filteredRefs"
:key="index"
class="gl-font-monospace"
is-check-item
:is-checked="isSelected(ref)"
@click="setRefSelected(ref)"
>
{{ ref }}
</gl-new-dropdown-item>
</gl-new-dropdown>
<template #description>
<div>
{{ s__('Pipeline|Existing branch name or tag') }}
</div></template
>
</gl-form-group>
<gl-form-group :label="s__('Pipeline|Variables')">
<div
v-for="(value, key, index) in variables"
:key="key"
class="gl-display-flex gl-align-items-center gl-mb-4 gl-pb-2 gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-flex-direction-column gl-md-flex-direction-row"
data-testid="ci-variable-row"
>
<gl-form-select
v-model="variables[key].variable_type"
:class="$options.formElementClasses"
:options="$options.typeOptions"
/>
<gl-form-input
v-model="variables[key].key"
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
@change.once="insertNewVariable()"
/>
<gl-form-input
v-model="variables[key].value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mr-5 gl-mb-3 table-section section-15"
/>
<gl-button
v-if="canRemove(index)"
icon="issue-close"
class="gl-mb-3"
data-testid="remove-ci-variable-row"
@click="removeVariable(key)"
/>
</div>
<template #description
><gl-sprintf :message="$options.variablesDescription">
<template #link="{ content }">
<gl-link :href="settingsLink">{{ content }}</gl-link>
</template>
</gl-sprintf></template
>
</gl-form-group>
<div
class="gl-border-t-solid gl-border-gray-100 gl-border-t-1 gl-p-5 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between"
>
<gl-button type="submit" category="primary" variant="success">{{
s__('Pipeline|Run Pipeline')
}}</gl-button>
<gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
</div>
</gl-form>
</template>

View File

@ -0,0 +1,2 @@
export const VARIABLE_TYPE = 'env_var';
export const FILE_TYPE = 'file';

View File

@ -0,0 +1,36 @@
import Vue from 'vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
export default () => {
const el = document.getElementById('js-new-pipeline');
const {
projectId,
pipelinesPath,
refParam,
varParam,
fileParam,
refNames,
settingsLink,
} = el?.dataset;
const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam);
const refs = JSON.parse(refNames);
return new Vue({
el,
render(createElement) {
return createElement(PipelineNewForm, {
props: {
projectId,
pipelinesPath,
refParam,
variableParams,
fileParams,
refs,
settingsLink,
},
});
},
});
};

View File

@ -116,7 +116,7 @@ export default {
onBeforeUnload(e = {}) { onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?'); const returnValue = __('Are you sure you want to lose unsaved changes?');
if (!this.allBlobChangesRegistered) return undefined; if (!this.allBlobChangesRegistered || this.isUpdating) return undefined;
Object.assign(e, { returnValue }); Object.assign(e, { returnValue });
return returnValue; return returnValue;

View File

@ -1,6 +1,6 @@
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { GlPopover, GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui'; import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import ToolbarButton from './toolbar_button.vue'; import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue'; import Icon from '../icon.vue';
@ -9,7 +9,7 @@ export default {
ToolbarButton, ToolbarButton,
Icon, Icon,
GlPopover, GlPopover,
GlDeprecatedButton, GlButton,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
@ -141,9 +141,14 @@ export default {
) )
}} }}
</p> </p>
<gl-deprecated-button variant="primary" size="sm" @click="handleSuggestDismissed"> <gl-button
variant="info"
category="primary"
size="sm"
@click="handleSuggestDismissed"
>
{{ __('Got it') }} {{ __('Got it') }}
</gl-deprecated-button> </gl-button>
</gl-popover> </gl-popover>
</template> </template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />

View File

@ -4,6 +4,7 @@ import { defaults, repeat } from 'lodash';
const DEFAULTS = { const DEFAULTS = {
subListIndentSpaces: 4, subListIndentSpaces: 4,
unorderedListBulletChar: '-', unorderedListBulletChar: '-',
incrementListMarker: false,
strong: '*', strong: '*',
emphasis: '_', emphasis: '_',
}; };
@ -15,12 +16,16 @@ const countIndentSpaces = text => {
}; };
const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => { const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
const { subListIndentSpaces, unorderedListBulletChar, strong, emphasis } = defaults( const {
formattingPreferences, subListIndentSpaces,
DEFAULTS, unorderedListBulletChar,
); incrementListMarker,
strong,
emphasis,
} = defaults(formattingPreferences, DEFAULTS);
const sublistNode = 'LI OL, LI UL'; const sublistNode = 'LI OL, LI UL';
const unorderedListItemNode = 'UL LI'; const unorderedListItemNode = 'UL LI';
const orderedListItemNode = 'OL LI';
const emphasisNode = 'EM, I'; const emphasisNode = 'EM, I';
const strongNode = 'STRONG, B'; const strongNode = 'STRONG, B';
@ -61,6 +66,11 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
}, },
[orderedListItemNode](node, subContent) {
const baseResult = baseRenderer.convert(node, subContent);
return incrementListMarker ? baseResult : baseResult.replace(/^(\s*)\d\./, '$11.');
},
[emphasisNode](node, subContent) { [emphasisNode](node, subContent) {
const result = baseRenderer.convert(node, subContent); const result = baseRenderer.convert(node, subContent);

View File

@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true) push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true) push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project) push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, default_enabled: true)
end end
before_action :ensure_pipeline, only: [:show] before_action :ensure_pipeline, only: [:show]

View File

@ -6,7 +6,7 @@ module Releases
belongs_to :release belongs_to :release
FILEPATH_REGEX = /\A\/([\-\.\w]+\/?)*[\da-zA-Z]+\z/.freeze FILEPATH_REGEX = %r{\A/(?:[\-\.\w]+/?)*[\da-zA-Z]+\z}.freeze
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release } validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
validates :name, presence: true, uniqueness: { scope: :release } validates :name, presence: true, uniqueness: { scope: :release }

View File

@ -6,37 +6,41 @@
= s_('Pipeline|Run Pipeline') = s_('Pipeline|Run Pipeline')
%hr %hr
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f| - if Feature.enabled?(:new_pipeline_form, default_enabled: true)
= form_errors(@pipeline) #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project) } }
.form-group.row
.col-sm-12
= f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide monospace',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.form-text.text-muted
= s_("Pipeline|Existing branch name or tag")
.col-sm-12.prepend-top-10.js-ci-variable-list-section - else
%label = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
= s_('Pipeline|Variables') = form_errors(@pipeline)
%ul.ci-variable-list .form-group.row
- if params[:var] .col-sm-12
- params[:var].each do |variable| = f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
= render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
- if params[:file_var] = dropdown_tag(params[:ref] || @project.default_branch,
- params[:file_var].each do |variable| options: { toggle_class: 'js-branch-select wide monospace',
- variable.push("file") filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
= render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
= render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true .form-text.text-muted
.form-text.text-muted = s_("Pipeline|Existing branch name or tag")
= (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
.form-actions .col-sm-12.prepend-top-10.js-ci-variable-list-section
= f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3 %label
= link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right' = s_('Pipeline|Variables')
%ul.ci-variable-list
- if params[:var]
- params[:var].each do |variable|
= render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
- if params[:file_var]
- params[:file_var].each do |variable|
- variable.push("file")
= render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
= render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
.form-text.text-muted
= (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
-# haml-lint:disable InlineJavaScript .form-actions
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe = f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe

View File

@ -1,5 +0,0 @@
---
title: Fix CSV downloads for multiple series in the same chart
merge_request: 36556
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: When generating markdown for ordered lists, the list marker should not increment
merge_request: 36851
author:
type: changed

View File

@ -51,7 +51,7 @@ To send separate email notifications to users with
## Configure PagerDuty integration ## Configure PagerDuty integration
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/119018) in GitLab 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/119018) in GitLab 13.3.
You can set up a webhook with PagerDuty to automatically create a GitLab issue You can set up a webhook with PagerDuty to automatically create a GitLab issue
for each PagerDuty incident. This configuration requires you to make changes for each PagerDuty incident. This configuration requires you to make changes
@ -61,7 +61,7 @@ in both PagerDuty and GitLab:
1. Navigate to **{settings}** **Settings > Operations > Incidents** and expand **Incidents**. 1. Navigate to **{settings}** **Settings > Operations > Incidents** and expand **Incidents**.
1. Select the **PagerDuty integration** tab: 1. Select the **PagerDuty integration** tab:
![PagerDuty incidents integration](img/pagerduty_incidents_integration_13_2.png) ![PagerDuty incidents integration](img/pagerduty_incidents_integration_13_3.png)
1. Activate the integration, and save the changes in GitLab. 1. Activate the integration, and save the changes in GitLab.
1. Copy the value of **Webhook URL** for use in a later step. 1. Copy the value of **Webhook URL** for use in a later step.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Atlassian Bamboo CI Service # Atlassian Bamboo CI Service
GitLab provides integration with Atlassian Bamboo for continuous integration. GitLab provides integration with Atlassian Bamboo for continuous integration.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Bugzilla Service # Bugzilla Service
Navigate to the [Integrations page](overview.md#accessing-integrations), Navigate to the [Integrations page](overview.md#accessing-integrations),

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Custom Issue Tracker service # Custom Issue Tracker service
To enable the Custom Issue Tracker integration in a project: To enable the Custom Issue Tracker integration in a project:

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Discord Notifications service # Discord Notifications service
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22684) in GitLab 11.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22684) in GitLab 11.6.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Enabling emails on push # Enabling emails on push
By enabling this service, you will receive email notifications for every change By enabling this service, you will receive email notifications for every change

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitHub project integration **(PREMIUM)** # GitHub project integration **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3836) in GitLab Premium 10.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3836) in GitLab Premium 10.6.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Slack application **(FREE ONLY)** # GitLab Slack application **(FREE ONLY)**
> - Introduced in GitLab 9.4. > - Introduced in GitLab 9.4.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Hangouts Chat service # Hangouts Chat service
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/43756) in GitLab 11.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/43756) in GitLab 11.2.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Atlassian HipChat # Atlassian HipChat
GitLab provides a way to send HipChat notifications upon a number of events, GitLab provides a way to send HipChat notifications upon a number of events,

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Project integrations # Project integrations
You can find the available integrations under your project's You can find the available integrations under your project's

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Irker IRC Gateway # Irker IRC Gateway
GitLab provides a way to push update messages to an Irker server. When GitLab provides a way to push update messages to an Irker server. When

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Jira integration # GitLab Jira integration
GitLab Issues are a powerful tool for discussing ideas and planning and tracking work. GitLab Issues are a powerful tool for discussing ideas and planning and tracking work.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Creating an API token in Jira Cloud # Creating an API token in Jira Cloud
An API token is needed when integrating with Jira Cloud, follow the steps An API token is needed when integrating with Jira Cloud, follow the steps

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Creating a username and password for Jira Server # Creating a username and password for Jira Server
We need to create a user in Jira which will have access to all projects that We need to create a user in Jira which will have access to all projects that

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Mattermost Notifications Service # Mattermost Notifications Service
The Mattermost Notifications Service allows your GitLab project to send events (e.g., `issue created`) to your existing Mattermost team as notifications. This requires configurations in both Mattermost and GitLab. The Mattermost Notifications Service allows your GitLab project to send events (e.g., `issue created`) to your existing Mattermost team as notifications. This requires configurations in both Mattermost and GitLab.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Mattermost slash commands # Mattermost slash commands
> Introduced in GitLab 8.14 > Introduced in GitLab 8.14

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Microsoft Teams service # Microsoft Teams service
## On Microsoft Teams ## On Microsoft Teams

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Mock CI Service # Mock CI Service
**NB: This service is only listed if you are in a development environment!** **NB: This service is only listed if you are in a development environment!**

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Integrations # Integrations
Integrations allow you to integrate GitLab with other applications. They Integrations allow you to integrate GitLab with other applications. They

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Redmine Service # Redmine Service
1. To enable the Redmine integration in a project, navigate to the 1. To enable the Redmine integration in a project, navigate to the

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Service templates # Service templates
Using a service template, GitLab administrators can provide default values for configuring integrations at the project level. Using a service template, GitLab administrators can provide default values for configuring integrations at the project level.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Slack Notifications Service # Slack Notifications Service
The Slack Notifications Service allows your GitLab project to send events The Slack Notifications Service allows your GitLab project to send events

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Slack slash commands **(CORE ONLY)** # Slack slash commands **(CORE ONLY)**
> Introduced in GitLab 8.15. > Introduced in GitLab 8.15.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Unify Circuit service # Unify Circuit service
The Unify Circuit service sends notifications from GitLab to the conversation for which the webhook was created. The Unify Circuit service sends notifications from GitLab to the conversation for which the webhook was created.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Webex Teams service # Webex Teams service
You can configure GitLab to send notifications to a Webex Teams space. You can configure GitLab to send notifications to a Webex Teams space.

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Webhooks # Webhooks
> **Note:** > **Note:**

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# YouTrack Service # YouTrack Service
JetBrains [YouTrack](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html) is a web-based issue tracking and project management platform. JetBrains [YouTrack](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html) is a web-based issue tracking and project management platform.

View File

@ -17281,6 +17281,9 @@ msgstr ""
msgid "Pipeline|Skipped" msgid "Pipeline|Skipped"
msgstr "" msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default."
msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default." msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr "" msgstr ""
@ -23227,6 +23230,15 @@ msgstr ""
msgid "Templates" msgid "Templates"
msgstr "" msgstr ""
msgid "TemporaryStorage|GitLab allows you a %{strongStart}free, one-time storage increase%{strongEnd}. For 30 days your storage will be unlimited. This gives you time to reduce your storage usage. After 30 days, your original storage limit of %{limit} applies. If you are at maximum storage capacity, your account will be read-only. To continue using GitLab you'll have to purchase additional storage or decrease storage usage."
msgstr ""
msgid "TemporaryStorage|Increase storage temporarily"
msgstr ""
msgid "TemporaryStorage|Temporarily increase storage now?"
msgstr ""
msgid "Terminal" msgid "Terminal"
msgstr "" msgstr ""
@ -23510,6 +23522,9 @@ msgstr[1] ""
msgid "The fork relationship has been removed." msgid "The fork relationship has been removed."
msgstr "" msgstr ""
msgid "The form contains the following error:"
msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account." msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr "" msgstr ""
@ -25689,6 +25704,9 @@ msgstr ""
msgid "UsageQuota|Current period usage" msgid "UsageQuota|Current period usage"
msgstr "" msgstr ""
msgid "UsageQuota|Increase storage temporarily"
msgstr ""
msgid "UsageQuota|LFS Objects" msgid "UsageQuota|LFS Objects"
msgstr "" msgstr ""

View File

@ -8,6 +8,7 @@ RSpec.describe "Populate new pipeline CI variables with url params", :js do
let(:page_path) { new_project_pipeline_path(project) } let(:page_path) { new_project_pipeline_path(project) }
before do before do
stub_feature_flags(new_pipeline_form: false)
sign_in(user) sign_in(user)
project.add_maintainer(user) project.add_maintainer(user)

View File

@ -652,6 +652,7 @@ RSpec.describe 'Pipelines', :js do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
before do before do
stub_feature_flags(new_pipeline_form: false)
visit new_project_pipeline_path(project) visit new_project_pipeline_path(project)
end end
@ -718,6 +719,7 @@ RSpec.describe 'Pipelines', :js do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
before do before do
stub_feature_flags(new_pipeline_form: false)
visit new_project_pipeline_path(project) visit new_project_pipeline_path(project)
end end

View File

@ -4,7 +4,6 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"name": { "type": "string" }, "name": { "type": "string" },
"filepath": { "type": "string" },
"url": { "type": "string" }, "url": { "type": "string" },
"direct_asset_url": { "type": "string" }, "direct_asset_url": { "type": "string" },
"external": { "type": "boolean" }, "external": { "type": "boolean" },

View File

@ -891,4 +891,34 @@ describe('Api', () => {
}); });
}); });
}); });
describe('createPipeline', () => {
it('creates new pipeline', () => {
const redirectUrl = 'ci-project/-/pipelines/95';
const projectId = 8;
const postData = {
ref: 'tag-1',
variables: [
{ key: 'test_file', value: 'test_file_val', variable_type: 'file' },
{ key: 'test_var', value: 'test_var_val', variable_type: 'env_var' },
],
};
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/pipeline`;
jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(200, {
web_url: redirectUrl,
});
return Api.createPipeline(projectId, postData).then(({ data }) => {
expect(data.web_url).toBe(redirectUrl);
expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, {
headers: {
'Content-Type': 'application/json',
},
});
});
});
});
}); });

View File

@ -1,38 +1,12 @@
import { getSeriesLabel, makeDataSeries } from '~/helpers/monitor_helper'; import * as monitorHelper from '~/helpers/monitor_helper';
describe('monitor helper', () => { describe('monitor helper', () => {
const defaultConfig = { default: true, name: 'default name' }; const defaultConfig = { default: true, name: 'default name' };
const name = 'data name'; const name = 'data name';
const series = [[1, 1], [2, 2], [3, 3]]; const series = [[1, 1], [2, 2], [3, 3]];
const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }];
describe('getSeriesLabel', () => {
const metricAttributes = { __name__: 'up', app: 'prometheus' };
it('gets a single attribute label', () => {
expect(getSeriesLabel('app', metricAttributes)).toBe('app: prometheus');
});
it('gets a templated label', () => {
expect(getSeriesLabel('{{__name__}}', metricAttributes)).toBe('up');
expect(getSeriesLabel('{{app}}', metricAttributes)).toBe('prometheus');
expect(getSeriesLabel('{{missing}}', metricAttributes)).toBe('{{missing}}');
});
it('gets a multiple label', () => {
expect(getSeriesLabel(null, metricAttributes)).toBe('__name__: up, app: prometheus');
expect(getSeriesLabel('', metricAttributes)).toBe('__name__: up, app: prometheus');
});
it('gets a simple label', () => {
expect(getSeriesLabel('A label', {})).toBe('A label');
});
});
describe('makeDataSeries', () => { describe('makeDataSeries', () => {
const data = ({ metric = { default_name: name }, values = series } = {}) => [
{ metric, values },
];
const expectedDataSeries = [ const expectedDataSeries = [
{ {
...defaultConfig, ...defaultConfig,
@ -41,17 +15,19 @@ describe('monitor helper', () => {
]; ];
it('converts query results to data series', () => { it('converts query results to data series', () => {
expect(makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(expectedDataSeries); expect(monitorHelper.makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(
expectedDataSeries,
);
}); });
it('returns an empty array if no query results exist', () => { it('returns an empty array if no query results exist', () => {
expect(makeDataSeries([], defaultConfig)).toEqual([]); expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]);
}); });
it('handles multi-series query results', () => { it('handles multi-series query results', () => {
const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' }; const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' };
expect(makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([ expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
expectedData, expectedData,
expectedData, expectedData,
]); ]);
@ -63,7 +39,10 @@ describe('monitor helper', () => {
name: '{{cmd}}', name: '{{cmd}}',
}; };
const [result] = makeDataSeries([{ metric: { cmd: 'brpop' }, values: series }], config); const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop' }, values: series }],
config,
);
expect(result.name).toEqual('brpop'); expect(result.name).toEqual('brpop');
}); });
@ -74,7 +53,7 @@ describe('monitor helper', () => {
name: '', name: '',
}; };
const [result] = makeDataSeries( const [result] = monitorHelper.makeDataSeries(
[ [
{ {
metric: { metric: {
@ -100,7 +79,7 @@ describe('monitor helper', () => {
name: 'backend: {{ backend }}', name: 'backend: {{ backend }}',
}; };
const [result] = makeDataSeries( const [result] = monitorHelper.makeDataSeries(
[{ metric: { backend: 'HA Server' }, values: series }], [{ metric: { backend: 'HA Server' }, values: series }],
config, config,
); );
@ -111,7 +90,10 @@ describe('monitor helper', () => {
it('supports repeated template variables', () => { it('supports repeated template variables', () => {
const config = { ...defaultConfig, name: '{{cmd}}, {{cmd}}' }; const config = { ...defaultConfig, name: '{{cmd}}, {{cmd}}' };
const [result] = makeDataSeries([{ metric: { cmd: 'brpop' }, values: series }], config); const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop' }, values: series }],
config,
);
expect(result.name).toEqual('brpop, brpop'); expect(result.name).toEqual('brpop, brpop');
}); });
@ -119,7 +101,7 @@ describe('monitor helper', () => {
it('supports hyphenated template variables', () => { it('supports hyphenated template variables', () => {
const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' }; const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' };
const [result] = makeDataSeries( const [result] = monitorHelper.makeDataSeries(
[{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }], [{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }],
config, config,
); );
@ -133,7 +115,7 @@ describe('monitor helper', () => {
name: '{{job}}: {{cmd}}', name: '{{job}}: {{cmd}}',
}; };
const [result] = makeDataSeries( const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop', job: 'redis' }, values: series }], [{ metric: { cmd: 'brpop', job: 'redis' }, values: series }],
config, config,
); );
@ -147,7 +129,7 @@ describe('monitor helper', () => {
name: '{{cmd}}', name: '{{cmd}}',
}; };
const [firstSeries, secondSeries] = makeDataSeries( const [firstSeries, secondSeries] = monitorHelper.makeDataSeries(
[ [
{ metric: { cmd: 'brpop' }, values: series }, { metric: { cmd: 'brpop' }, values: series },
{ metric: { cmd: 'zrangebyscore' }, values: series }, { metric: { cmd: 'zrangebyscore' }, values: series },

View File

@ -443,7 +443,7 @@ describe('Dashboard Panel', () => {
describe('csvText', () => { describe('csvText', () => {
it('converts metrics data from json to csv', () => { it('converts metrics data from json to csv', () => {
const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`; const header = `timestamp,${graphData.y_label}`;
const data = graphData.metrics[0].result[0].values; const data = graphData.metrics[0].result[0].values;
const firstRow = `${data[0][0]},${data[0][1]}`; const firstRow = `${data[0][0]},${data[0][1]}`;
const secondRow = `${data[1][0]},${data[1][1]}`; const secondRow = `${data[1][0]},${data[1][1]}`;

View File

@ -1,126 +0,0 @@
import { timeSeriesGraphData } from './graph_data';
import { graphDataToCsv } from '~/monitoring/csv_export';
describe('monitoring export_csv', () => {
describe('graphDataToCsv', () => {
const expectCsvToMatchLines = (csv, lines) => expect(`${lines.join('\r\n')}\r\n`).toEqual(csv);
it('should return a csv with 0 metrics', () => {
const data = timeSeriesGraphData({}, { metricCount: 0 });
expect(graphDataToCsv(data)).toEqual('');
});
it('should return a csv with 1 metric with no data', () => {
const data = timeSeriesGraphData({}, { metricCount: 1 });
// When state is NO_DATA, result is null
data.metrics[0].result = null;
expect(graphDataToCsv(data)).toEqual('');
});
it('should return a csv with multiple metrics and one with no data', () => {
const data = timeSeriesGraphData({}, { metricCount: 2 });
// When state is NO_DATA, result is null
data.metrics[0].result = null;
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 2"`,
'2015-07-01T20:10:51.781Z,1',
'2015-07-01T20:11:06.781Z,2',
'2015-07-01T20:11:21.781Z,3',
]);
});
it('should return a csv when not all metrics have the same timestamps', () => {
const data = timeSeriesGraphData({}, { metricCount: 3 });
// Add an "odd" timestamp that is not in the dataset
Object.assign(data.metrics[2].result[0], {
value: ['2016-01-01T00:00:00.000Z', 9],
values: [['2016-01-01T00:00:00.000Z', 9]],
});
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
'2015-07-01T20:10:51.781Z,1,1,',
'2015-07-01T20:11:06.781Z,2,2,',
'2015-07-01T20:11:21.781Z,3,3,',
'2016-01-01T00:00:00.000Z,,,9',
]);
});
it('should return a csv with 1 metric', () => {
const data = timeSeriesGraphData({}, { metricCount: 1 });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1"`,
'2015-07-01T20:10:51.781Z,1',
'2015-07-01T20:11:06.781Z,2',
'2015-07-01T20:11:21.781Z,3',
]);
});
it('should escape double quotes in metric labels with two double quotes ("")', () => {
const data = timeSeriesGraphData({}, { metricCount: 1 });
data.metrics[0].label = 'My "quoted" metric';
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > My ""quoted"" metric"`,
'2015-07-01T20:10:51.781Z,1',
'2015-07-01T20:11:06.781Z,2',
'2015-07-01T20:11:21.781Z,3',
]);
});
it('should return a csv with multiple metrics', () => {
const data = timeSeriesGraphData({}, { metricCount: 3 });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
'2015-07-01T20:10:51.781Z,1,1,1',
'2015-07-01T20:11:06.781Z,2,2,2',
'2015-07-01T20:11:21.781Z,3,3,3',
]);
});
it('should return a csv with 1 metric and multiple series with labels', () => {
const data = timeSeriesGraphData({}, { isMultiSeries: true });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1","Y Axis > Metric 1"`,
'2015-07-01T20:10:51.781Z,1,4',
'2015-07-01T20:11:06.781Z,2,5',
'2015-07-01T20:11:21.781Z,3,6',
]);
});
it('should return a csv with 1 metric and multiple series', () => {
const data = timeSeriesGraphData({}, { isMultiSeries: true, withLabels: false });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
'2015-07-01T20:10:51.781Z,1,4',
'2015-07-01T20:11:06.781Z,2,5',
'2015-07-01T20:11:21.781Z,3,6',
]);
});
it('should return a csv with multiple metrics and multiple series', () => {
const data = timeSeriesGraphData(
{},
{ metricCount: 3, isMultiSeries: true, withLabels: false },
);
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
'2015-07-01T20:10:51.781Z,1,4,1,4,1,4',
'2015-07-01T20:11:06.781Z,2,5,2,5,2,5',
'2015-07-01T20:11:21.781Z,3,6,3,6,3,6',
]);
});
});
});

View File

@ -83,7 +83,7 @@ const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'
* @param {Object} dataOptions.isMultiSeries * @param {Object} dataOptions.isMultiSeries
*/ */
export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => { export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions; const { metricCount = 1, isMultiSeries = false } = dataOptions;
return mapPanelToViewModel({ return mapPanelToViewModel({
title: 'Time Series Panel', title: 'Time Series Panel',
@ -91,7 +91,7 @@ export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
x_label: 'X Axis', x_label: 'X Axis',
y_label: 'Y Axis', y_label: 'Y Axis',
metrics: Array.from(Array(metricCount), (_, i) => ({ metrics: Array.from(Array(metricCount), (_, i) => ({
label: withLabels ? `Metric ${i + 1}` : undefined, label: `Metric ${i + 1}`,
state: metricStates.OK, state: metricStates.OK,
result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(), result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
})), })),

View File

@ -0,0 +1,108 @@
import Api from '~/api';
import { mount, shallowMount } from '@vue/test-utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import { GlNewDropdown, GlNewDropdownItem, GlForm } from '@gitlab/ui';
import { mockRefs, mockParams, mockPostParams, mockProjectId } from '../mock_data';
describe('Pipeline New Form', () => {
let wrapper;
const dummySubmitEvent = {
preventDefault() {},
};
const findForm = () => wrapper.find(GlForm);
const findDropdown = () => wrapper.find(GlNewDropdown);
const findDropdownItems = () => wrapper.findAll(GlNewDropdownItem);
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
const createComponent = (term = '', props = {}, method = shallowMount) => {
wrapper = method(PipelineNewForm, {
propsData: {
projectId: mockProjectId,
pipelinesPath: '',
refs: mockRefs,
defaultBranch: 'master',
settingsLink: '',
...props,
},
data() {
return {
searchTerm: term,
};
},
});
};
beforeEach(() => {
jest.spyOn(Api, 'createPipeline').mockResolvedValue({ data: { web_url: '/' } });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('Dropdown with branches and tags', () => {
it('displays dropdown with all branches and tags', () => {
createComponent();
expect(findDropdownItems().length).toBe(mockRefs.length);
});
it('when user enters search term the list is filtered', () => {
createComponent('master');
expect(findDropdownItems().length).toBe(1);
expect(
findDropdownItems()
.at(0)
.text(),
).toBe('master');
});
});
describe('Form', () => {
beforeEach(() => {
createComponent('', mockParams, mount);
});
it('displays the correct values for the provided query params', () => {
expect(findDropdown().props('text')).toBe('tag-1');
return wrapper.vm.$nextTick().then(() => {
expect(findVariableRows().length).toBe(3);
});
});
it('does not display remove icon for last row', () => {
expect(findRemoveIcons().length).toBe(2);
});
it('removes ci variable row on remove icon button click', () => {
findRemoveIcons()
.at(1)
.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(findVariableRows().length).toBe(2);
});
});
it('creates a pipeline on submit', () => {
findForm().vm.$emit('submit', dummySubmitEvent);
expect(Api.createPipeline).toHaveBeenCalledWith(mockProjectId, mockPostParams);
});
it('creates blank variable on input change event', () => {
findKeyInputs()
.at(2)
.trigger('change');
return wrapper.vm.$nextTick().then(() => {
expect(findVariableRows().length).toBe(4);
});
});
});
});

View File

@ -0,0 +1,21 @@
export const mockRefs = ['master', 'branch-1', 'tag-1'];
export const mockParams = {
refParam: 'tag-1',
variableParams: {
test_var: 'test_var_val',
},
fileParams: {
test_file: 'test_file_val',
},
};
export const mockProjectId = '21';
export const mockPostParams = {
ref: 'tag-1',
variables: [
{ key: 'test_var', value: 'test_var_val', variable_type: 'env_var' },
{ key: 'test_file', value: 'test_file_val', variable_type: 'file' },
],
};

View File

@ -388,42 +388,49 @@ describe('Snippet Edit app', () => {
returnValueSetter = jest.spyOn(event, 'returnValue', 'set'); returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
}; };
it('does not prevent page navigation if there are no blobs', () => { const actionsWithoutAction = {
bootstrap(); blobsActions: {
window.dispatchEvent(event); foo: {
...actionWithContent,
expect(returnValueSetter).not.toHaveBeenCalled(); action: '',
});
it('does not prevent page navigation if there are no changes to the blobs content', () => {
bootstrap({
blobsActions: {
foo: {
...actionWithContent,
action: '',
},
}, },
}); },
window.dispatchEvent(event); };
const actionsWithUpdate = {
expect(returnValueSetter).not.toHaveBeenCalled(); blobsActions: {
}); foo: {
...actionWithContent,
it('prevents page navigation if there are some changes in the snippet content', () => { action: 'update',
bootstrap({
blobsActions: {
foo: {
...actionWithContent,
action: 'update',
},
}, },
}); },
};
const actionsWithUpdateWhileSaving = {
blobsActions: {
foo: {
...actionWithContent,
action: 'update',
},
},
isUpdating: true,
};
it.each`
bool | expectToBePrevented | data | condition
${'does not prevent'} | ${false} | ${undefined} | ${'there are no blobs'}
${'does not prevent'} | ${false} | ${actionsWithoutAction} | ${'there are no changes to the blobs content'}
${'prevents'} | ${true} | ${actionsWithUpdate} | ${'there are changes to the blobs content'}
${'does not prevent'} | ${false} | ${actionsWithUpdateWhileSaving} | ${'the snippet is being saved'}
`('$bool page navigation if $condition', ({ expectToBePrevented, data }) => {
bootstrap(data);
window.dispatchEvent(event); window.dispatchEvent(event);
expect(returnValueSetter).toHaveBeenCalledWith( if (expectToBePrevented) {
'Are you sure you want to lose unsaved changes?', expect(returnValueSetter).toHaveBeenCalledWith(
); 'Are you sure you want to lose unsaved changes?',
);
} else {
expect(returnValueSetter).not.toHaveBeenCalled();
}
}); });
}); });
}); });

View File

@ -68,6 +68,28 @@ describe('HTMLToMarkdownRenderer', () => {
); );
}); });
describe('OL LI visitor', () => {
it.each`
listItem | result | incrementListMarker | action
${'2. list item'} | ${'1. list item'} | ${false} | ${'increments'}
${' 3. list item'} | ${' 1. list item'} | ${false} | ${'increments'}
${'3. list item'} | ${'3. list item'} | ${true} | ${'does not increment'}
`(
'$action a list item counter when incrementListMaker is $incrementListMarker',
({ listItem, result, incrementListMarker }) => {
const subContent = null;
htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
incrementListMarker,
});
baseRenderer.convert.mockReturnValueOnce(listItem);
expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result);
expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent);
},
);
});
describe('STRONG, B visitor', () => { describe('STRONG, B visitor', () => {
it.each` it.each`
input | strongCharacter | result input | strongCharacter | result