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',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
pipelinesPath: '/api/:version/projects/:id/pipelines/',
createPipelinePath: '/api/:version/projects/:id/pipeline',
environmentsPath: '/api/:version/projects/:id/environments',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
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) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url);

View File

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

View File

@ -30,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { graphDataToCsv } from '../csv_export';
const events = {
timeRangeZoom: 'timerangezoom',
@ -149,10 +148,13 @@ export default {
return null;
},
csvText() {
if (this.graphData) {
return graphDataToCsv(this.graphData);
}
return null;
const chartData = this.graphData?.metrics[0].result[0].values || [];
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
return chartData.reduce((csv, data) => {
const row = data.join(',');
return `${csv}${row}\r\n`;
}, header);
},
downloadCsv() {
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 NewBranchForm from '~/new_branch_form';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
import initNewPipeline from '~/pipeline_new/index';
document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
const el = document.getElementById('js-new-pipeline');
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'variables_attributes',
});
if (el) {
initNewPipeline();
} 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 = {}) {
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 });
return returnValue;

View File

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

View File

@ -4,6 +4,7 @@ import { defaults, repeat } from 'lodash';
const DEFAULTS = {
subListIndentSpaces: 4,
unorderedListBulletChar: '-',
incrementListMarker: false,
strong: '*',
emphasis: '_',
};
@ -15,12 +16,16 @@ const countIndentSpaces = text => {
};
const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
const { subListIndentSpaces, unorderedListBulletChar, strong, emphasis } = defaults(
formattingPreferences,
DEFAULTS,
);
const {
subListIndentSpaces,
unorderedListBulletChar,
incrementListMarker,
strong,
emphasis,
} = defaults(formattingPreferences, DEFAULTS);
const sublistNode = 'LI OL, LI UL';
const unorderedListItemNode = 'UL LI';
const orderedListItemNode = 'OL LI';
const emphasisNode = 'EM, I';
const strongNode = 'STRONG, B';
@ -61,6 +66,11 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
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) {
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(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, default_enabled: true)
end
before_action :ensure_pipeline, only: [:show]

View File

@ -6,7 +6,7 @@ module Releases
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 :name, presence: true, uniqueness: { scope: :release }

View File

@ -6,37 +6,41 @@
= s_('Pipeline|Run Pipeline')
%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|
= form_errors(@pipeline)
.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")
- if Feature.enabled?(:new_pipeline_form, default_enabled: true)
#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) } }
.col-sm-12.prepend-top-10.js-ci-variable-list-section
%label
= 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
- else
= 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|
= form_errors(@pipeline)
.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")
.form-actions
= 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'
.col-sm-12.prepend-top-10.js-ci-variable-list-section
%label
= 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
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
.form-actions
= 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
> [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
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. 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. 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
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
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
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
> [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
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)**
> [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)**
> - 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
> [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
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
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
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 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
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
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
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
> 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
## 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
**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 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
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
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
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)**
> 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
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
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
> **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
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"
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."
msgstr ""
@ -23227,6 +23230,15 @@ msgstr ""
msgid "Templates"
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"
msgstr ""
@ -23510,6 +23522,9 @@ msgstr[1] ""
msgid "The fork relationship has been removed."
msgstr ""
msgid "The form contains the following error:"
msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr ""
@ -25689,6 +25704,9 @@ msgstr ""
msgid "UsageQuota|Current period usage"
msgstr ""
msgid "UsageQuota|Increase storage temporarily"
msgstr ""
msgid "UsageQuota|LFS Objects"
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) }
before do
stub_feature_flags(new_pipeline_form: false)
sign_in(user)
project.add_maintainer(user)

View File

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

View File

@ -4,7 +4,6 @@
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"filepath": { "type": "string" },
"url": { "type": "string" },
"direct_asset_url": { "type": "string" },
"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', () => {
const defaultConfig = { default: true, name: 'default name' };
const name = 'data name';
const series = [[1, 1], [2, 2], [3, 3]];
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');
});
});
const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }];
describe('makeDataSeries', () => {
const data = ({ metric = { default_name: name }, values = series } = {}) => [
{ metric, values },
];
const expectedDataSeries = [
{
...defaultConfig,
@ -41,17 +15,19 @@ describe('monitor helper', () => {
];
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', () => {
expect(makeDataSeries([], defaultConfig)).toEqual([]);
expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]);
});
it('handles multi-series query results', () => {
const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' };
expect(makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
expectedData,
expectedData,
]);
@ -63,7 +39,10 @@ describe('monitor helper', () => {
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');
});
@ -74,7 +53,7 @@ describe('monitor helper', () => {
name: '',
};
const [result] = makeDataSeries(
const [result] = monitorHelper.makeDataSeries(
[
{
metric: {
@ -100,7 +79,7 @@ describe('monitor helper', () => {
name: 'backend: {{ backend }}',
};
const [result] = makeDataSeries(
const [result] = monitorHelper.makeDataSeries(
[{ metric: { backend: 'HA Server' }, values: series }],
config,
);
@ -111,7 +90,10 @@ describe('monitor helper', () => {
it('supports repeated template variables', () => {
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');
});
@ -119,7 +101,7 @@ describe('monitor helper', () => {
it('supports hyphenated template variables', () => {
const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' };
const [result] = makeDataSeries(
const [result] = monitorHelper.makeDataSeries(
[{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }],
config,
);
@ -133,7 +115,7 @@ describe('monitor helper', () => {
name: '{{job}}: {{cmd}}',
};
const [result] = makeDataSeries(
const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop', job: 'redis' }, values: series }],
config,
);
@ -147,7 +129,7 @@ describe('monitor helper', () => {
name: '{{cmd}}',
};
const [firstSeries, secondSeries] = makeDataSeries(
const [firstSeries, secondSeries] = monitorHelper.makeDataSeries(
[
{ metric: { cmd: 'brpop' }, values: series },
{ metric: { cmd: 'zrangebyscore' }, values: series },

View File

@ -443,7 +443,7 @@ describe('Dashboard Panel', () => {
describe('csvText', () => {
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 firstRow = `${data[0][0]},${data[0][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
*/
export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions;
const { metricCount = 1, isMultiSeries = false } = dataOptions;
return mapPanelToViewModel({
title: 'Time Series Panel',
@ -91,7 +91,7 @@ export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
x_label: 'X Axis',
y_label: 'Y Axis',
metrics: Array.from(Array(metricCount), (_, i) => ({
label: withLabels ? `Metric ${i + 1}` : undefined,
label: `Metric ${i + 1}`,
state: metricStates.OK,
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');
};
it('does not prevent page navigation if there are no blobs', () => {
bootstrap();
window.dispatchEvent(event);
expect(returnValueSetter).not.toHaveBeenCalled();
});
it('does not prevent page navigation if there are no changes to the blobs content', () => {
bootstrap({
blobsActions: {
foo: {
...actionWithContent,
action: '',
},
const actionsWithoutAction = {
blobsActions: {
foo: {
...actionWithContent,
action: '',
},
});
window.dispatchEvent(event);
expect(returnValueSetter).not.toHaveBeenCalled();
});
it('prevents page navigation if there are some changes in the snippet content', () => {
bootstrap({
blobsActions: {
foo: {
...actionWithContent,
action: 'update',
},
},
};
const actionsWithUpdate = {
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);
expect(returnValueSetter).toHaveBeenCalledWith(
'Are you sure you want to lose unsaved changes?',
);
if (expectToBePrevented) {
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', () => {
it.each`
input | strongCharacter | result