Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bf213f07c8
commit
192bc8bd31
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
export const VARIABLE_TYPE = 'env_var';
|
||||
export const FILE_TYPE = '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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix CSV downloads for multiple series in the same chart
|
||||
merge_request: 36556
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: When generating markdown for ordered lists, the list marker should not increment
|
||||
merge_request: 36851
|
||||
author:
|
||||
type: changed
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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]}`;
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(),
|
||||
})),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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' },
|
||||
],
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue