diff options
58 files changed, 810 insertions, 405 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 246231d969b..64b55b4d12f 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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); diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js index 5e345321013..5f85ee58779 100644 --- a/app/assets/javascripts/helpers/monitor_helper.js +++ b/app/assets/javascripts/helpers/monitor_helper.js @@ -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 { diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue index 610bef37fdb..3e3c8408de3 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue @@ -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' }); diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js deleted file mode 100644 index 734e8dc07a7..00000000000 --- a/app/assets/javascripts/monitoring/csv_export.js +++ /dev/null @@ -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; -}; diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js index b0b077a5e4c..d5563143f0c 100644 --- a/app/assets/javascripts/pages/projects/pipelines/new/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -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', + }); + } }); diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue new file mode 100644 index 00000000000..c2c5e58eedd --- /dev/null +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -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> diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/pipeline_new/constants.js new file mode 100644 index 00000000000..b4ab1143f60 --- /dev/null +++ b/app/assets/javascripts/pipeline_new/constants.js @@ -0,0 +1,2 @@ +export const VARIABLE_TYPE = 'env_var'; +export const FILE_TYPE = 'file'; diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js new file mode 100644 index 00000000000..1c4812c2e0e --- /dev/null +++ b/app/assets/javascripts/pipeline_new/index.js @@ -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, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 69593dc77f8..71ba4e0c183 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -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; diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 049f5e71849..dfa4730d4fa 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -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" /> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js index 21ded83a771..89a0df395d3 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js @@ -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); diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index d8e11ddd423..fde2a7e5d92 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -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] diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb index dc7e78a85a9..e1dc3b904b9 100644 --- a/app/models/releases/link.rb +++ b/app/models/releases/link.rb @@ -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 } diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index a3e46a0939c..11fdbd31382 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -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 diff --git a/changelogs/unreleased/214627-fix-incorrect-csv-export.yml b/changelogs/unreleased/214627-fix-incorrect-csv-export.yml deleted file mode 100644 index e69a392f4ac..00000000000 --- a/changelogs/unreleased/214627-fix-incorrect-csv-export.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix CSV downloads for multiple series in the same chart -merge_request: 36556 -author: -type: fixed diff --git a/changelogs/unreleased/227598-make-ordered-list-configurable.yml b/changelogs/unreleased/227598-make-ordered-list-configurable.yml new file mode 100644 index 00000000000..3c9f3ed1eec --- /dev/null +++ b/changelogs/unreleased/227598-make-ordered-list-configurable.yml @@ -0,0 +1,5 @@ +--- +title: When generating markdown for ordered lists, the list marker should not increment +merge_request: 36851 +author: +type: changed diff --git a/doc/user/incident_management/img/pagerduty_incidents_integration_13_2.png b/doc/user/incident_management/img/pagerduty_incidents_integration_13_3.png Binary files differindex 0991e963e02..0991e963e02 100644 --- a/doc/user/incident_management/img/pagerduty_incidents_integration_13_2.png +++ b/doc/user/incident_management/img/pagerduty_incidents_integration_13_3.png diff --git a/doc/user/incident_management/index.md b/doc/user/incident_management/index.md index a8714660afd..6d72dbaa90f 100644 --- a/doc/user/incident_management/index.md +++ b/doc/user/incident_management/index.md @@ -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. diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index 7b21c590c8a..9cade323ed2 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -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. diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index 6d44c56743e..2ed14a4c69c 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -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), diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 7d15ae82b6f..5ec044731d1 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -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: diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index aa45cc38cb5..f261362eeae 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -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. diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index b0838690d3b..d8b864e0396 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -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 diff --git a/doc/user/project/integrations/github.md b/doc/user/project/integrations/github.md index 416996fb629..a0594b4537e 100644 --- a/doc/user/project/integrations/github.md +++ b/doc/user/project/integrations/github.md @@ -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. diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index 7a827364d41..ef4cf372f56 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -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. diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index f65b31150a9..54f9bd8d622 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -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. diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md index 2ed7f13db9b..718f00273bd 100644 --- a/doc/user/project/integrations/hipchat.md +++ b/doc/user/project/integrations/hipchat.md @@ -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, diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 75565dd2750..0a1db5da61d 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -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 diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 2d807d4302b..f2e769dcfc0 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -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 diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 541c65041ad..3f02f85a8bd 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -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. diff --git a/doc/user/project/integrations/jira_cloud_configuration.md b/doc/user/project/integrations/jira_cloud_configuration.md index c7157b6bd0e..14999734c00 100644 --- a/doc/user/project/integrations/jira_cloud_configuration.md +++ b/doc/user/project/integrations/jira_cloud_configuration.md @@ -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 diff --git a/doc/user/project/integrations/jira_server_configuration.md b/doc/user/project/integrations/jira_server_configuration.md index c8278a0f083..38098d7d15b 100644 --- a/doc/user/project/integrations/jira_server_configuration.md +++ b/doc/user/project/integrations/jira_server_configuration.md @@ -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 diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 67d60984c22..c12a969ca3c 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -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. diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index 6a202c9a130..a392b20ee41 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -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 diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md index 611ae1a01af..b2a2f1c3e7b 100644 --- a/doc/user/project/integrations/microsoft_teams.md +++ b/doc/user/project/integrations/microsoft_teams.md @@ -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 diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md index b06ccda8287..4567d345336 100644 --- a/doc/user/project/integrations/mock_ci.md +++ b/doc/user/project/integrations/mock_ci.md @@ -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!** diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index 79c55e2d140..3b54b3c9696 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -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 diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index c92ddf38ad2..2a85dd9b79b 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -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 diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md index bc2bdde2f64..688643a85a7 100644 --- a/doc/user/project/integrations/services_templates.md +++ b/doc/user/project/integrations/services_templates.md @@ -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. diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 6c5dc787c5e..1365f11ebe0 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -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 diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index d25a367bd1f..7c2413fce81 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -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. diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md index 98dc6f298d5..c4959a8711b 100644 --- a/doc/user/project/integrations/unify_circuit.md +++ b/doc/user/project/integrations/unify_circuit.md @@ -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. diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md index 10735e33746..39daa14407f 100644 --- a/doc/user/project/integrations/webex_teams.md +++ b/doc/user/project/integrations/webex_teams.md @@ -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. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 5a0ca03a646..d84ae9ac910 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -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:** diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index e067ab6071e..d243ffc7a37 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -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. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bac41af8a66..d556e96db61 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/features/populate_new_pipeline_vars_with_params_spec.rb b/spec/features/populate_new_pipeline_vars_with_params_spec.rb index f931e8497fc..37fea5331a3 100644 --- a/spec/features/populate_new_pipeline_vars_with_params_spec.rb +++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb @@ -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) diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 0eb92f3e679..8747b3ab54c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -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 diff --git a/spec/fixtures/api/schemas/release/link.json b/spec/fixtures/api/schemas/release/link.json index b3aebfa131e..669f0a39343 100644 --- a/spec/fixtures/api/schemas/release/link.json +++ b/spec/fixtures/api/schemas/release/link.json @@ -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" }, diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index b4e25867fad..b76cfe6204d 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -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', + }, + }); + }); + }); + }); }); diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js index 219b05e312b..083b6404125 100644 --- a/spec/frontend/helpers/monitor_helper_spec.js +++ b/spec/frontend/helpers/monitor_helper_spec.js @@ -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 }, diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js index a38af9770cf..507598ad3aa 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js @@ -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]}`; diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js deleted file mode 100644 index 90d6eaa435f..00000000000 --- a/spec/frontend/monitoring/csv_export_spec.js +++ /dev/null @@ -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', - ]); - }); - }); -}); diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js index fcdca95ac09..8e81ad7a585 100644 --- a/spec/frontend/monitoring/graph_data.js +++ b/spec/frontend/monitoring/graph_data.js @@ -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(), })), diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js new file mode 100644 index 00000000000..5ad1cdbfa51 --- /dev/null +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -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); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js new file mode 100644 index 00000000000..55ec1fb5afc --- /dev/null +++ b/spec/frontend/pipeline_new/mock_data.js @@ -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' }, + ], +}; diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index 6149ecbf00c..0148439d74c 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -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(); + } }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js index 218ca8f3f5a..2bbd3572d4b 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js @@ -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 |