From e5826739d7f33d433822521858ba3fc5227defcc Mon Sep 17 00:00:00 2001 From: Miguel Rincon Date: Mon, 1 Mar 2021 18:00:10 +0100 Subject: [PATCH] Load refs for the pipeline on demand The pipeline form does not require all the refs in order to be submitted. This change waits for the user to list them before loading them, improving the performance of the form. --- .../components/pipeline_new_form.vue | 187 ++++++++---------- .../pipeline_new/components/refs_dropdown.vue | 113 +++++++++++ .../javascripts/pipeline_new/constants.js | 1 + app/assets/javascripts/pipeline_new/index.js | 14 +- app/views/projects/pipelines/new.html.haml | 3 +- .../unreleased/321790-load-refs-on-demand.yml | 5 + doc/ci/pipelines/index.md | 4 +- locale/gitlab.pot | 9 +- .../components/pipeline_new_form_spec.js | 141 +++++++------ .../components/refs_dropdown_spec.js | 182 +++++++++++++++++ spec/frontend/pipeline_new/mock_data.js | 20 +- 11 files changed, 474 insertions(+), 205 deletions(-) create mode 100644 app/assets/javascripts/pipeline_new/components/refs_dropdown.vue create mode 100644 changelogs/unreleased/321790-load-refs-on-demand.yml create mode 100644 spec/frontend/pipeline_new/components/refs_dropdown_spec.js 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 bd112697b4960f..ff6a354f673938 100644 --- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -9,10 +9,6 @@ import { GlFormSelect, GlFormTextarea, GlLink, - GlDropdown, - GlDropdownItem, - GlDropdownSectionHeader, - GlSearchBoxByType, GlSprintf, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml, @@ -26,19 +22,26 @@ import httpStatusCodes from '~/lib/utils/http_status'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__, __, n__ } from '~/locale'; 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; + }, }, }; @@ -343,7 +336,7 @@ export default { - - - {{ __('Branches') }} - - {{ branch.shortName }} - - {{ __('Tags') }} - - {{ tag.shortName }} - - + @@ -465,7 +434,7 @@ export default {