summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipeline_new
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipeline_new')
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue189
-rw-r--r--app/assets/javascripts/pipeline_new/components/refs_dropdown.vue113
-rw-r--r--app/assets/javascripts/pipeline_new/constants.js1
-rw-r--r--app/assets/javascripts/pipeline_new/index.js14
4 files changed, 200 insertions, 117 deletions
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 5070971c563..ff6a354f673 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -9,14 +9,11 @@ import {
GlFormSelect,
GlFormTextarea,
GlLink,
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
GlSprintf,
GlLoadingIcon,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
@@ -24,21 +21,27 @@ import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
-import * as Sentry from '~/sentry/wrapper';
import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
+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}'),
+};
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.',
- ),
+ i18n,
formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
- errorTitle: __('Pipeline cannot be run.'),
- warningTitle: __('The form contains the following warning:'),
- maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
// 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' },
@@ -52,12 +55,9 @@ export default {
GlFormSelect,
GlFormTextarea,
GlLink,
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
GlSprintf,
GlLoadingIcon,
+ RefsDropdown,
},
directives: { SafeHtml },
props: {
@@ -77,14 +77,6 @@ export default {
type: String,
required: true,
},
- branches: {
- type: Array,
- required: true,
- },
- tags: {
- type: Array,
- required: true,
- },
settingsLink: {
type: String,
required: true,
@@ -111,11 +103,11 @@ export default {
},
data() {
return {
- searchTerm: '',
refValue: {
shortName: this.refParam,
},
form: {},
+ errorTitle: null,
error: null,
warnings: [],
totalWarnings: 0,
@@ -125,22 +117,6 @@ export default {
};
},
computed: {
- lowerCasedSearchTerm() {
- return this.searchTerm.toLowerCase();
- },
- filteredBranches() {
- return this.branches.filter((branch) =>
- branch.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
- );
- },
- filteredTags() {
- return this.tags.filter((tag) =>
- tag.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
- );
- },
- hasTags() {
- return this.tags.length > 0;
- },
overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings;
},
@@ -148,7 +124,7 @@ export default {
return n__('%d warning found:', '%d warnings found:', this.warnings.length);
},
summaryMessage() {
- return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary;
+ return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary;
},
shouldShowWarning() {
return this.warnings.length > 0 && !this.isWarningDismissed;
@@ -166,6 +142,11 @@ export default {
return this.form[this.refFullName]?.descriptions ?? {};
},
},
+ watch: {
+ refValue() {
+ this.loadConfigVariablesForm();
+ },
+ },
created() {
// this is needed until we add support for ref type in url query strings
// ensure default branch is called with full ref on load
@@ -174,7 +155,7 @@ export default {
this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
}
- this.setRefSelected(this.refValue);
+ this.loadConfigVariablesForm();
},
methods: {
addEmptyVariable(refValue) {
@@ -213,49 +194,47 @@ export default {
this.setVariable(refValue, type, key, value);
});
},
- setRefSelected(refValue) {
- this.refValue = refValue;
-
- if (!this.form[this.refFullName]) {
- this.fetchConfigVariables(this.refFullName || this.refShortName)
- .then(({ descriptions, params }) => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions,
- });
-
- // Add default variables from yml
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
- })
- .catch(() => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions: {},
- });
- })
- .finally(() => {
- // 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);
- });
- }
- },
- isSelected(ref) {
- return ref.fullName === this.refValue.fullName;
- },
removeVariable(index) {
this.variables.splice(index, 1);
},
canRemove(index) {
return index < this.variables.length - 1;
},
+ loadConfigVariablesForm() {
+ // Skip when variables already cached in `form`
+ if (this.form[this.refFullName]) {
+ return;
+ }
+
+ this.fetchConfigVariables(this.refFullName || this.refShortName)
+ .then(({ descriptions, params }) => {
+ Vue.set(this.form, this.refFullName, {
+ variables: [],
+ descriptions,
+ });
+
+ // Add default variables from yml
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
+ })
+ .catch(() => {
+ Vue.set(this.form, this.refFullName, {
+ variables: [],
+ descriptions: {},
+ });
+ })
+ .finally(() => {
+ // 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);
+ });
+ },
fetchConfigVariables(refValue) {
this.isLoading = true;
@@ -330,11 +309,25 @@ export default {
} = err?.response?.data;
const [error] = errors;
- this.error = error;
- this.warnings = warnings;
- this.totalWarnings = totalWarnings;
+ 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;
+ },
},
};
</script>
@@ -343,7 +336,7 @@ export default {
<gl-form @submit.prevent="createPipeline">
<gl-alert
v-if="error"
- :title="$options.errorTitle"
+ :title="errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-4"
@@ -353,7 +346,7 @@ export default {
</gl-alert>
<gl-alert
v-if="shouldShowWarning"
- :title="$options.warningTitle"
+ :title="$options.i18n.warningTitle"
variant="warning"
class="gl-mb-4"
data-testid="run-pipeline-warning-alert"
@@ -380,31 +373,7 @@ export default {
</details>
</gl-alert>
<gl-form-group :label="s__('Pipeline|Run for branch name or tag')">
- <gl-dropdown :text="refShortName" block>
- <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search refs')" />
- <gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="branch in filteredBranches"
- :key="branch.fullName"
- class="gl-font-monospace"
- is-check-item
- :is-checked="isSelected(branch)"
- @click="setRefSelected(branch)"
- >
- {{ branch.shortName }}
- </gl-dropdown-item>
- <gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="tag in filteredTags"
- :key="tag.fullName"
- class="gl-font-monospace"
- is-check-item
- :is-checked="isSelected(tag)"
- @click="setRefSelected(tag)"
- >
- {{ tag.shortName }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" />
</gl-form-group>
<gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" />
@@ -465,7 +434,7 @@ export default {
</div>
<template #description
- ><gl-sprintf :message="$options.variablesDescription">
+ ><gl-sprintf :message="$options.i18n.variablesDescription">
<template #link="{ content }">
<gl-link :href="settingsLink">{{ content }}</gl-link>
</template>
diff --git a/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue b/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
new file mode 100644
index 00000000000..ed5c659d1df
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { BRANCH_REF_TYPE, TAG_REF_TYPE, DEBOUNCE_REFS_SEARCH_MS } from '../constants';
+import formatRefs from '../utils/format_refs';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ inject: ['projectRefsEndpoint'],
+ props: {
+ value: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ searchTerm: '',
+ branches: [],
+ tags: [],
+ };
+ },
+ computed: {
+ lowerCasedSearchTerm() {
+ return this.searchTerm.toLowerCase();
+ },
+ refShortName() {
+ return this.value.shortName;
+ },
+ hasTags() {
+ return this.tags.length > 0;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.debouncedLoadRefs();
+ },
+ },
+ 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.branches = formatRefs(Branches, BRANCH_REF_TYPE);
+ this.tags = formatRefs(Tags, TAG_REF_TYPE);
+ })
+ .catch((e) => {
+ this.$emit('loadingError', e);
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ debouncedLoadRefs: debounce(function debouncedLoadRefs() {
+ this.loadRefs();
+ }, DEBOUNCE_REFS_SEARCH_MS),
+ setRefSelected(ref) {
+ this.$emit('input', ref);
+ },
+ isSelected(ref) {
+ return ref.fullName === this.value.fullName;
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown :text="refShortName" block @show.once="loadRefs">
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :is-loading="isLoading"
+ :placeholder="__('Search refs')"
+ />
+ <gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="branch in branches"
+ :key="branch.fullName"
+ class="gl-font-monospace"
+ is-check-item
+ :is-checked="isSelected(branch)"
+ @click="setRefSelected(branch)"
+ >
+ {{ branch.shortName }}
+ </gl-dropdown-item>
+ <gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="tag in tags"
+ :key="tag.fullName"
+ class="gl-font-monospace"
+ is-check-item
+ :is-checked="isSelected(tag)"
+ @click="setRefSelected(tag)"
+ >
+ {{ tag.shortName }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/pipeline_new/constants.js
index 004bbe7daf4..681755dc6ab 100644
--- a/app/assets/javascripts/pipeline_new/constants.js
+++ b/app/assets/javascripts/pipeline_new/constants.js
@@ -1,5 +1,6 @@
export const VARIABLE_TYPE = 'env_var';
export const FILE_TYPE = 'file';
+export const DEBOUNCE_REFS_SEARCH_MS = 250;
export const CONFIG_VARIABLES_TIMEOUT = 5000;
export const BRANCH_REF_TYPE = 'branch';
export const TAG_REF_TYPE = 'tag';
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index 0b85184ec90..a645ea8603b 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -1,10 +1,13 @@
import Vue from 'vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
-import formatRefs from './utils/format_refs';
export default () => {
const el = document.getElementById('js-new-pipeline');
const {
+ // provide/inject
+ projectRefsEndpoint,
+
+ // props
projectId,
pipelinesPath,
configVariablesPath,
@@ -12,19 +15,18 @@ export default () => {
refParam,
varParam,
fileParam,
- branchRefs,
- tagRefs,
settingsLink,
maxWarnings,
} = el?.dataset;
const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam);
- const branches = formatRefs(JSON.parse(branchRefs), 'branch');
- const tags = formatRefs(JSON.parse(tagRefs), 'tag');
return new Vue({
el,
+ provide: {
+ projectRefsEndpoint,
+ },
render(createElement) {
return createElement(PipelineNewForm, {
props: {
@@ -35,8 +37,6 @@ export default () => {
refParam,
variableParams,
fileParams,
- branches,
- tags,
settingsLink,
maxWarnings: Number(maxWarnings),
},