Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-21 00:09:23 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-21 00:09:23 +0300
commit192bc8bd3109f30e957bf30a0139ae27fefd7936 (patch)
tree61c8c415c765900386ac43ea0a5a8a5ce94366b6 /app
parentbf213f07c8146b7121240af90a07cb4b2ecc41fa (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js11
-rw-r--r--app/assets/javascripts/helpers/monitor_helper.js3
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue12
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js147
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js17
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue247
-rw-r--r--app/assets/javascripts/pipeline_new/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_new/index.js36
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js18
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/models/releases/link.rb2
-rw-r--r--app/views/projects/pipelines/new.html.haml66
14 files changed, 378 insertions, 199 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