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
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ci/pipeline_new')
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue474
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue86
-rw-r--r--app/assets/javascripts/ci/pipeline_new/constants.js14
-rw-r--r--app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql9
-rw-r--r--app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql11
-rw-r--r--app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js29
-rw-r--r--app/assets/javascripts/ci/pipeline_new/index.js61
-rw-r--r--app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js13
-rw-r--r--app/assets/javascripts/ci/pipeline_new/utils/format_refs.js55
9 files changed, 752 insertions, 0 deletions
diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
new file mode 100644
index 00000000000..5692627abef
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
@@ -0,0 +1,474 @@
+<script>
+import {
+ GlAlert,
+ GlIcon,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlLink,
+ GlSprintf,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { uniqueId } from 'lodash';
+import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__, __, n__ } from '~/locale';
+import { VARIABLE_TYPE, FILE_TYPE, CC_VALIDATION_REQUIRED_ERROR } from '../constants';
+import createPipelineMutation from '../graphql/mutations/create_pipeline.mutation.graphql';
+import ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql';
+import filterVariables from '../utils/filter_variables';
+import RefsDropdown from './refs_dropdown.vue';
+
+const i18n = {
+ 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.',
+ ),
+ defaultError: __('Something went wrong on our end. Please try again.'),
+ refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'),
+ submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'),
+ warningTitle: __('The form contains the following warning:'),
+ maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
+ removeVariableLabel: s__('CiVariables|Remove variable'),
+};
+
+export default {
+ typeOptions: {
+ [VARIABLE_TYPE]: __('Variable'),
+ [FILE_TYPE]: __('File'),
+ },
+ i18n,
+ formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
+ // this height value is used inline on the textarea to match the input field height
+ // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used
+ textAreaStyle: { height: '32px' },
+ components: {
+ GlAlert,
+ GlIcon,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlLink,
+ GlSprintf,
+ GlLoadingIcon,
+ RefsDropdown,
+ CcValidationRequiredAlert: () =>
+ import('ee_component/billings/components/cc_validation_required_alert.vue'),
+ },
+ directives: { SafeHtml },
+ props: {
+ pipelinesPath: {
+ type: String,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ settingsLink: {
+ type: String,
+ required: true,
+ },
+ fileParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ refParam: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ variableParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ maxWarnings: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ refValue: {
+ shortName: this.refParam,
+ // this is needed until we add support for ref type in url query strings
+ // ensure default branch is called with full ref on load
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
+ fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
+ },
+ form: {},
+ errorTitle: null,
+ error: null,
+ predefinedValueOptions: {},
+ warnings: [],
+ totalWarnings: 0,
+ isWarningDismissed: false,
+ submitted: false,
+ ccAlertDismissed: false,
+ };
+ },
+ apollo: {
+ ciConfigVariables: {
+ query: ciConfigVariablesQuery,
+ // Skip when variables already cached in `form`
+ skip() {
+ return Object.keys(this.form).includes(this.refFullName);
+ },
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ ref: this.refQueryParam,
+ };
+ },
+ update({ project }) {
+ return project?.ciConfigVariables || [];
+ },
+ result({ data }) {
+ const predefinedVars = data?.project?.ciConfigVariables || [];
+ const params = {};
+ const descriptions = {};
+
+ predefinedVars.forEach(({ description, key, value, valueOptions }) => {
+ if (description) {
+ params[key] = value;
+ descriptions[key] = description;
+ this.predefinedValueOptions[key] = valueOptions;
+ }
+ });
+
+ Vue.set(this.form, this.refFullName, { descriptions, variables: [] });
+
+ // Add default variables from yml
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
+
+ // Add/update variables, e.g. from query string
+ if (this.variableParams) {
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
+ }
+
+ if (this.fileParams) {
+ this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
+ }
+
+ // Adds empty var at the end of the form
+ this.addEmptyVariable(this.refFullName);
+ },
+ error(error) {
+ Sentry.captureException(error);
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.ciConfigVariables.loading;
+ },
+ overMaxWarningsLimit() {
+ return this.totalWarnings > this.maxWarnings;
+ },
+ warningsSummary() {
+ return n__('%d warning found:', '%d warnings found:', this.warnings.length);
+ },
+ summaryMessage() {
+ return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary;
+ },
+ shouldShowWarning() {
+ return this.warnings.length > 0 && !this.isWarningDismissed;
+ },
+ refShortName() {
+ return this.refValue.shortName;
+ },
+ refFullName() {
+ return this.refValue.fullName;
+ },
+ refQueryParam() {
+ return this.refFullName || this.refShortName;
+ },
+ variables() {
+ return this.form[this.refFullName]?.variables ?? [];
+ },
+ descriptions() {
+ return this.form[this.refFullName]?.descriptions ?? {};
+ },
+ ccRequiredError() {
+ return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
+ },
+ },
+ methods: {
+ addEmptyVariable(refValue) {
+ const { variables } = this.form[refValue];
+
+ const lastVar = variables[variables.length - 1];
+ if (lastVar?.key === '' && lastVar?.value === '') {
+ return;
+ }
+
+ variables.push({
+ uniqueId: uniqueId(`var-${refValue}`),
+ variable_type: VARIABLE_TYPE,
+ key: '',
+ value: '',
+ });
+ },
+ setVariable(refValue, type, key, value) {
+ const { variables } = this.form[refValue];
+
+ const variable = variables.find((v) => v.key === key);
+ if (variable) {
+ variable.type = type;
+ variable.value = value;
+ } else {
+ variables.push({
+ uniqueId: uniqueId(`var-${refValue}`),
+ key,
+ value,
+ variable_type: type,
+ });
+ }
+ },
+ setVariableAttribute(key, attribute, value) {
+ const { variables } = this.form[this.refFullName];
+ const variable = variables.find((v) => v.key === key);
+ variable[attribute] = value;
+ },
+ setVariableParams(refValue, type, paramsObj) {
+ Object.entries(paramsObj).forEach(([key, value]) => {
+ this.setVariable(refValue, type, key, value);
+ });
+ },
+ shouldShowValuesDropdown(key) {
+ return this.predefinedValueOptions[key]?.length > 1;
+ },
+ removeVariable(index) {
+ this.variables.splice(index, 1);
+ },
+ canRemove(index) {
+ return index < this.variables.length - 1;
+ },
+ async createPipeline() {
+ this.submitted = true;
+ this.ccAlertDismissed = false;
+
+ const { data } = await this.$apollo.mutate({
+ mutation: createPipelineMutation,
+ variables: {
+ endpoint: this.pipelinesPath,
+ // send shortName as fall back for query params
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
+ ref: this.refQueryParam,
+ variablesAttributes: filterVariables(this.variables),
+ },
+ });
+
+ const { id, errors, totalWarnings, warnings } = data.createPipeline;
+
+ if (id) {
+ redirectTo(`${this.pipelinesPath}/${id}`);
+ return;
+ }
+
+ // always re-enable submit button
+ this.submitted = false;
+ const [error] = errors;
+
+ this.reportError({
+ title: i18n.submitErrorTitle,
+ error,
+ warnings,
+ totalWarnings,
+ });
+ },
+ onRefsLoadingError(error) {
+ this.reportError({ title: i18n.refsLoadingErrorTitle });
+
+ Sentry.captureException(error);
+ },
+ reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) {
+ this.errorTitle = title;
+ this.error = error;
+ this.warnings = warnings;
+ this.totalWarnings = totalWarnings;
+ },
+ dismissError() {
+ this.ccAlertDismissed = true;
+ this.error = null;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form @submit.prevent="createPipeline">
+ <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" />
+ <gl-alert
+ v-else-if="error"
+ :title="errorTitle"
+ :dismissible="false"
+ variant="danger"
+ class="gl-mb-4"
+ data-testid="run-pipeline-error-alert"
+ >
+ <span v-safe-html="error"></span>
+ </gl-alert>
+ <gl-alert
+ v-if="shouldShowWarning"
+ :title="$options.i18n.warningTitle"
+ variant="warning"
+ class="gl-mb-4"
+ data-testid="run-pipeline-warning-alert"
+ @dismiss="isWarningDismissed = true"
+ >
+ <details>
+ <summary>
+ <gl-sprintf :message="summaryMessage">
+ <template #total>
+ {{ totalWarnings }}
+ </template>
+ <template #warningsDisplayed>
+ {{ maxWarnings }}
+ </template>
+ </gl-sprintf>
+ </summary>
+ <p
+ v-for="(warning, index) in warnings"
+ :key="`warning-${index}`"
+ data-testid="run-pipeline-warning"
+ >
+ {{ warning }}
+ </p>
+ </details>
+ </gl-alert>
+ <gl-form-group :label="s__('Pipeline|Run for branch name or tag')">
+ <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" />
+ </gl-form-group>
+
+ <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" />
+
+ <gl-form-group v-else :label="s__('Pipeline|Variables')">
+ <div
+ v-for="(variable, index) in variables"
+ :key="variable.uniqueId"
+ class="gl-mb-3 gl-pb-2"
+ data-testid="ci-variable-row"
+ data-qa-selector="ci_variable_row_container"
+ >
+ <div
+ class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
+ >
+ <gl-dropdown
+ :text="$options.typeOptions[variable.variable_type]"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-type"
+ >
+ <gl-dropdown-item
+ v-for="type in Object.keys($options.typeOptions)"
+ :key="type"
+ @click="setVariableAttribute(variable.key, 'variable_type', type)"
+ >
+ {{ $options.typeOptions[type] }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-form-input
+ v-model="variable.key"
+ :placeholder="s__('CiVariables|Input variable key')"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-key"
+ data-qa-selector="ci_variable_key_field"
+ @change="addEmptyVariable(refFullName)"
+ />
+ <gl-dropdown
+ v-if="shouldShowValuesDropdown(variable.key)"
+ :text="variable.value"
+ :class="$options.formElementClasses"
+ class="gl-flex-grow-1 gl-mr-0!"
+ data-testid="pipeline-form-ci-variable-value-dropdown"
+ data-qa-selector="ci_variable_value_dropdown"
+ >
+ <gl-dropdown-item
+ v-for="value in predefinedValueOptions[variable.key]"
+ :key="value"
+ data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ data-qa-selector="ci_variable_value_dropdown_item"
+ @click="setVariableAttribute(variable.key, 'value', value)"
+ >
+ {{ value }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-form-textarea
+ v-else
+ v-model="variable.value"
+ :placeholder="s__('CiVariables|Input variable value')"
+ class="gl-mb-3"
+ :style="$options.textAreaStyle"
+ :no-resize="false"
+ data-testid="pipeline-form-ci-variable-value"
+ data-qa-selector="ci_variable_value_field"
+ />
+
+ <template v-if="variables.length > 1">
+ <gl-button
+ v-if="canRemove(index)"
+ class="gl-md-ml-3 gl-mb-3"
+ data-testid="remove-ci-variable-row"
+ variant="danger"
+ category="secondary"
+ :aria-label="$options.i18n.removeVariableLabel"
+ @click="removeVariable(index)"
+ >
+ <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" />
+ <span class="gl-md-display-none">{{ $options.i18n.removeVariableLabel }}</span>
+ </gl-button>
+ <gl-button
+ v-else
+ class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
+ icon="clear"
+ :aria-label="$options.i18n.removeVariableLabel"
+ />
+ </template>
+ </div>
+ <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
+ {{ descriptions[variable.key] }}
+ </div>
+ </div>
+
+ <template #description
+ ><gl-sprintf :message="$options.i18n.variablesDescription">
+ <template #link="{ content }">
+ <gl-link :href="settingsLink">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf></template
+ >
+ </gl-form-group>
+ <div class="gl-pt-5 gl-display-flex">
+ <gl-button
+ type="submit"
+ category="primary"
+ variant="confirm"
+ class="js-no-auto-disable gl-mr-3"
+ data-qa-selector="run_pipeline_button"
+ data-testid="run_pipeline_button"
+ :disabled="submitted"
+ >{{ s__('Pipeline|Run pipeline') }}</gl-button
+ >
+ <gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue b/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue
new file mode 100644
index 00000000000..060527f2662
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { DEBOUNCE_REFS_SEARCH_MS } from '../constants';
+import { formatListBoxItems, searchByFullNameInListboxOptions } from '../utils/format_refs';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+ inject: ['projectRefsEndpoint'],
+ props: {
+ value: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ searchTerm: '',
+ listBoxItems: [],
+ };
+ },
+ computed: {
+ lowerCasedSearchTerm() {
+ return this.searchTerm.toLowerCase();
+ },
+ refShortName() {
+ return this.value.shortName;
+ },
+ },
+ methods: {
+ loadRefs() {
+ this.isLoading = true;
+
+ axios
+ .get(this.projectRefsEndpoint, {
+ params: {
+ search: this.lowerCasedSearchTerm,
+ },
+ })
+ .then(({ data }) => {
+ // Note: These keys are uppercase in API
+ const { Branches = [], Tags = [] } = data;
+
+ this.listBoxItems = formatListBoxItems(Branches, Tags);
+ })
+ .catch((e) => {
+ this.$emit('loadingError', e);
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ debouncedLoadRefs: debounce(function debouncedLoadRefs() {
+ this.loadRefs();
+ }, DEBOUNCE_REFS_SEARCH_MS),
+ setRefSelected(refFullName) {
+ const ref = searchByFullNameInListboxOptions(refFullName, this.listBoxItems);
+ this.$emit('input', ref);
+ },
+ setSearchTerm(searchQuery) {
+ this.searchTerm = searchQuery?.trim();
+ this.debouncedLoadRefs();
+ },
+ },
+};
+</script>
+<template>
+ <gl-collapsible-listbox
+ class="gl-w-full gl-font-monospace"
+ :items="listBoxItems"
+ :searchable="true"
+ :searching="isLoading"
+ :search-placeholder="__('Search refs')"
+ :selected="value.fullName"
+ toggle-class="gl-flex-direction-column gl-align-items-stretch!"
+ :toggle-text="refShortName"
+ @search="setSearchTerm"
+ @select="setRefSelected"
+ @shown.once="loadRefs"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_new/constants.js b/app/assets/javascripts/ci/pipeline_new/constants.js
new file mode 100644
index 00000000000..43f7634083b
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/constants.js
@@ -0,0 +1,14 @@
+import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+export const VARIABLE_TYPE = 'env_var';
+export const FILE_TYPE = 'file';
+export const DEBOUNCE_REFS_SEARCH_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
+export const CONFIG_VARIABLES_TIMEOUT = 5000;
+export const BRANCH_REF_TYPE = 'branch';
+export const TAG_REF_TYPE = 'tag';
+
+// must match pipeline/chain/validate/after_config.rb
+export const CC_VALIDATION_REQUIRED_ERROR = __(
+ 'Credit card required to be on file in order to create a pipeline',
+);
diff --git a/app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
new file mode 100644
index 00000000000..a76e8f6b95b
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
@@ -0,0 +1,9 @@
+mutation createPipeline($endpoint: String, $ref: String, $variablesAttributes: Array) {
+ createPipeline(endpoint: $endpoint, ref: $ref, variablesAttributes: $variablesAttributes)
+ @client {
+ id
+ errors
+ totalWarnings
+ warnings
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
new file mode 100644
index 00000000000..648cd8b66b5
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
@@ -0,0 +1,11 @@
+query ciConfigVariables($fullPath: ID!, $ref: String!) {
+ project(fullPath: $fullPath) {
+ id
+ ciConfigVariables(sha: $ref) {
+ description
+ key
+ value
+ valueOptions
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js b/app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js
new file mode 100644
index 00000000000..7b0f58e8cf9
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js
@@ -0,0 +1,29 @@
+import axios from '~/lib/utils/axios_utils';
+
+export const resolvers = {
+ Mutation: {
+ createPipeline: (_, { endpoint, ref, variablesAttributes }) => {
+ return axios
+ .post(endpoint, { ref, variables_attributes: variablesAttributes })
+ .then((response) => {
+ const { id } = response.data;
+ return {
+ id,
+ errors: [],
+ totalWarnings: 0,
+ warnings: [],
+ };
+ })
+ .catch((err) => {
+ const { errors = [], totalWarnings = 0, warnings = [] } = err.response.data;
+
+ return {
+ id: null,
+ errors,
+ totalWarnings,
+ warnings,
+ };
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci/pipeline_new/index.js b/app/assets/javascripts/ci/pipeline_new/index.js
new file mode 100644
index 00000000000..71c76aeab36
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/index.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import PipelineNewForm from './components/pipeline_new_form.vue';
+import { resolvers } from './graphql/resolvers';
+
+const mountPipelineNewForm = (el) => {
+ const {
+ // provide/inject
+ projectRefsEndpoint,
+
+ // props
+ defaultBranch,
+ fileParam,
+ maxWarnings,
+ pipelinesPath,
+ projectId,
+ projectPath,
+ refParam,
+ settingsLink,
+ varParam,
+ } = el.dataset;
+
+ const variableParams = JSON.parse(varParam);
+ const fileParams = JSON.parse(fileParam);
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectRefsEndpoint,
+ },
+ render(createElement) {
+ return createElement(PipelineNewForm, {
+ props: {
+ defaultBranch,
+ fileParams,
+ maxWarnings: Number(maxWarnings),
+ pipelinesPath,
+ projectId,
+ projectPath,
+ refParam,
+ settingsLink,
+ variableParams,
+ },
+ });
+ },
+ });
+};
+
+export default () => {
+ const el = document.getElementById('js-new-pipeline');
+
+ mountPipelineNewForm(el);
+};
diff --git a/app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js b/app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js
new file mode 100644
index 00000000000..57ce3d13a9a
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js
@@ -0,0 +1,13 @@
+// We need to filter out blank variables
+// and filter out variables that have no key
+// before sending to the API to create a pipeline.
+
+export default (variables) => {
+ return variables
+ .filter(({ key }) => key !== '')
+ .map(({ variable_type, key, value }) => ({
+ variable_type,
+ key,
+ secret_value: value,
+ }));
+};
diff --git a/app/assets/javascripts/ci/pipeline_new/utils/format_refs.js b/app/assets/javascripts/ci/pipeline_new/utils/format_refs.js
new file mode 100644
index 00000000000..e6d26b32d47
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/utils/format_refs.js
@@ -0,0 +1,55 @@
+import { __ } from '~/locale';
+import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '../constants';
+
+function convertToListBoxItems(items) {
+ return items.map(({ shortName, fullName }) => ({ text: shortName, value: fullName }));
+}
+
+export function formatRefs(refs, type) {
+ let fullName;
+
+ return refs.map((ref) => {
+ if (type === BRANCH_REF_TYPE) {
+ fullName = `refs/heads/${ref}`;
+ } else if (type === TAG_REF_TYPE) {
+ fullName = `refs/tags/${ref}`;
+ }
+
+ return {
+ shortName: ref,
+ fullName,
+ };
+ });
+}
+
+export const formatListBoxItems = (branches, tags) => {
+ const finalResults = [];
+
+ if (branches.length > 0) {
+ finalResults.push({
+ text: __('Branches'),
+ options: convertToListBoxItems(formatRefs(branches, BRANCH_REF_TYPE)),
+ });
+ }
+
+ if (tags.length > 0) {
+ finalResults.push({
+ text: __('Tags'),
+ options: convertToListBoxItems(formatRefs(tags, TAG_REF_TYPE)),
+ });
+ }
+
+ return finalResults;
+};
+
+export const searchByFullNameInListboxOptions = (fullName, listBox) => {
+ const optionsToSearch =
+ listBox.length > 1 ? listBox[0].options.concat(listBox[1].options) : listBox[0]?.options;
+
+ const foundOption = optionsToSearch.find(({ value }) => value === fullName);
+
+ return {
+ shortName: foundOption.text,
+ fullName: foundOption.value,
+ };
+};