diff options
Diffstat (limited to 'app/assets/javascripts/ci_lint')
7 files changed, 462 insertions, 10 deletions
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue index 135d02e4f76..2532f4b86d2 100644 --- a/app/assets/javascripts/ci_lint/components/ci_lint.vue +++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue @@ -1,14 +1,120 @@ <script> +import { GlButton, GlFormCheckbox, GlIcon, GlLink, GlAlert } from '@gitlab/ui'; +import EditorLite from '~/vue_shared/components/editor_lite.vue'; +import CiLintResults from './ci_lint_results.vue'; +import lintCIMutation from '../graphql/mutations/lint_ci.mutation.graphql'; + export default { + components: { + GlButton, + GlFormCheckbox, + GlIcon, + GlLink, + GlAlert, + CiLintResults, + EditorLite, + }, props: { endpoint: { type: String, required: true, }, + helpPagePath: { + type: String, + required: true, + }, + }, + data() { + return { + content: '', + valid: false, + errors: null, + warnings: null, + jobs: [], + dryRun: false, + showingResults: false, + apiError: null, + isErrorDismissed: false, + }; + }, + computed: { + shouldShowError() { + return this.apiError && !this.isErrorDismissed; + }, + }, + methods: { + async lint() { + try { + const { + data: { + lintCI: { valid, errors, warnings, jobs }, + }, + } = await this.$apollo.mutate({ + mutation: lintCIMutation, + variables: { endpoint: this.endpoint, content: this.content, dry: this.dryRun }, + }); + + this.showingResults = true; + this.valid = valid; + this.errors = errors; + this.warnings = warnings; + this.jobs = jobs; + } catch (error) { + this.apiError = error; + this.isErrorDismissed = false; + } + }, + clear() { + this.content = ''; + }, }, }; </script> -<template - ><div></div -></template> +<template> + <div class="row"> + <div class="col-sm-12"> + <gl-alert + v-if="shouldShowError" + class="gl-mb-3" + variant="danger" + @dismiss="isErrorDismissed = true" + >{{ apiError }}</gl-alert + > + <div class="file-holder gl-mb-3"> + <div class="js-file-title file-title clearfix"> + {{ __('Contents of .gitlab-ci.yml') }} + </div> + <editor-lite v-model="content" file-name="*.yml" /> + </div> + </div> + + <div class="col-sm-12 gl-display-flex gl-justify-content-space-between"> + <div class="gl-display-flex gl-align-items-center"> + <gl-button + class="gl-mr-4" + category="primary" + variant="success" + data-testid="ci-lint-validate" + @click="lint" + >{{ __('Validate') }}</gl-button + > + <gl-form-checkbox v-model="dryRun" + >{{ __('Simulate a pipeline created for the default branch') }} + <gl-link :href="helpPagePath" target="_blank" + ><gl-icon class="gl-text-blue-600" name="question-o"/></gl-link + ></gl-form-checkbox> + </div> + <gl-button data-testid="ci-lint-clear" @click="clear">{{ __('Clear') }}</gl-button> + </div> + + <ci-lint-results + v-if="showingResults" + :valid="valid" + :jobs="jobs" + :errors="errors" + :warnings="warnings" + :dry-run="dryRun" + /> + </div> +</template> diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_results.vue b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue index 9fd1bd30c49..28b2a028b29 100644 --- a/app/assets/javascripts/ci_lint/components/ci_lint_results.vue +++ b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue @@ -1,9 +1,116 @@ <script> +import { GlAlert, GlTable } from '@gitlab/ui'; +import CiLintWarnings from './ci_lint_warnings.vue'; +import CiLintResultsValue from './ci_lint_results_value.vue'; +import CiLintResultsParam from './ci_lint_results_param.vue'; +import { __ } from '~/locale'; + +const thBorderColor = 'gl-border-gray-100!'; + export default { - props: {}, + correct: { variant: 'success', text: __('syntax is correct') }, + incorrect: { variant: 'danger', text: __('syntax is incorrect') }, + warningTitle: __('The form contains the following warning:'), + fields: [ + { + key: 'parameter', + label: __('Parameter'), + thClass: thBorderColor, + }, + { + key: 'value', + label: __('Value'), + thClass: thBorderColor, + }, + ], + components: { + GlAlert, + GlTable, + CiLintWarnings, + CiLintResultsValue, + CiLintResultsParam, + }, + props: { + valid: { + type: Boolean, + required: true, + }, + jobs: { + type: Array, + required: true, + }, + errors: { + type: Array, + required: true, + }, + warnings: { + type: Array, + required: true, + }, + dryRun: { + type: Boolean, + required: true, + }, + }, + data() { + return { + isWarningDismissed: false, + }; + }, + computed: { + status() { + return this.valid ? this.$options.correct : this.$options.incorrect; + }, + shouldShowTable() { + return this.errors.length === 0; + }, + shouldShowError() { + return this.errors.length > 0; + }, + shouldShowWarning() { + return this.warnings.length > 0 && !this.isWarningDismissed; + }, + }, }; </script> -<template - ><div></div -></template> +<template> + <div class="col-sm-12 gl-mt-5"> + <gl-alert + class="gl-mb-5" + :variant="status.variant" + :title="__('Status:')" + :dismissible="false" + data-testid="ci-lint-status" + >{{ status.text }}</gl-alert + > + + <pre + v-if="shouldShowError" + class="gl-mb-5" + data-testid="ci-lint-errors" + ><div v-for="error in errors" :key="error">{{ error }}</div></pre> + + <ci-lint-warnings + v-if="shouldShowWarning" + :warnings="warnings" + data-testid="ci-lint-warnings" + @dismiss="isWarningDismissed = true" + /> + + <gl-table + v-if="shouldShowTable" + :items="jobs" + :fields="$options.fields" + bordered + data-testid="ci-lint-table" + > + <template #cell(parameter)="{ item }"> + <ci-lint-results-param :stage="item.stage" :job-name="item.name" /> + </template> + <template #cell(value)="{ item }"> + <ci-lint-results-value :item="item" :dry-run="dryRun" /> + </template> + </gl-table> + </div> +</template> diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_results_param.vue b/app/assets/javascripts/ci_lint/components/ci_lint_results_param.vue new file mode 100644 index 00000000000..23808bcb292 --- /dev/null +++ b/app/assets/javascripts/ci_lint/components/ci_lint_results_param.vue @@ -0,0 +1,26 @@ +<script> +import { __ } from '~/locale'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +export default { + props: { + stage: { + type: String, + required: true, + }, + jobName: { + type: String, + required: true, + }, + }, + computed: { + formatParameter() { + return __(`${capitalizeFirstCharacter(this.stage)} Job - ${this.jobName}`); + }, + }, +}; +</script> + +<template> + <span data-testid="ci-lint-parameter">{{ formatParameter }}</span> +</template> diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_results_value.vue b/app/assets/javascripts/ci_lint/components/ci_lint_results_value.vue new file mode 100644 index 00000000000..4929c3206df --- /dev/null +++ b/app/assets/javascripts/ci_lint/components/ci_lint_results_value.vue @@ -0,0 +1,81 @@ +<script> +import { isEmpty } from 'lodash'; + +export default { + props: { + item: { + type: Object, + required: true, + }, + dryRun: { + type: Boolean, + required: true, + }, + }, + computed: { + tagList() { + return this.item.tagList.join(', '); + }, + onlyPolicy() { + return this.item.only ? this.item.only.refs.join(', ') : this.item.only; + }, + exceptPolicy() { + return this.item.except ? this.item.except.refs.join(', ') : this.item.except; + }, + scripts() { + return { + beforeScript: { + show: !isEmpty(this.item.beforeScript), + content: this.item.beforeScript.join('\n'), + }, + script: { + show: !isEmpty(this.item.script), + content: this.item.script.join('\n'), + }, + afterScript: { + show: !isEmpty(this.item.afterScript), + content: this.item.afterScript.join('\n'), + }, + }; + }, + }, +}; +</script> + +<template> + <div> + <pre v-if="scripts.beforeScript.show" data-testid="ci-lint-before-script">{{ + scripts.beforeScript.content + }}</pre> + <pre v-if="scripts.script.show" data-testid="ci-lint-script">{{ scripts.script.content }}</pre> + <pre v-if="scripts.afterScript.show" data-testid="ci-lint-after-script">{{ + scripts.afterScript.content + }}</pre> + + <ul class="gl-list-style-none gl-pl-0 gl-mb-0"> + <li> + <b>{{ __('Tag list:') }}</b> + {{ tagList }} + </li> + <div v-if="!dryRun" data-testid="ci-lint-only-except"> + <li> + <b>{{ __('Only policy:') }}</b> + {{ onlyPolicy }} + </li> + <li> + <b>{{ __('Except policy:') }}</b> + {{ exceptPolicy }} + </li> + </div> + <li> + <b>{{ __('Environment:') }}</b> + {{ item.environment }} + </li> + <li> + <b>{{ __('When:') }}</b> + {{ item.when }} + <b v-if="item.allowFailure">{{ __('Allowed to fail') }}</b> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_warnings.vue b/app/assets/javascripts/ci_lint/components/ci_lint_warnings.vue new file mode 100644 index 00000000000..ac0332cb0bd --- /dev/null +++ b/app/assets/javascripts/ci_lint/components/ci_lint_warnings.vue @@ -0,0 +1,69 @@ +<script> +import { GlAlert, GlSprintf } from '@gitlab/ui'; +import { __, n__ } from '~/locale'; + +export default { + maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'), + components: { + GlAlert, + GlSprintf, + }, + props: { + warnings: { + type: Array, + required: true, + }, + maxWarnings: { + type: Number, + required: false, + default: 25, + }, + title: { + type: String, + required: false, + default: __('The form contains the following warning:'), + }, + }, + computed: { + totalWarnings() { + return this.warnings.length; + }, + overMaxWarningsLimit() { + return this.totalWarnings > this.maxWarnings; + }, + warningsSummary() { + return n__('%d warning found:', '%d warnings found:', this.totalWarnings); + }, + summaryMessage() { + return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary; + }, + limitWarnings() { + return this.warnings.slice(0, this.maxWarnings); + }, + }, +}; +</script> + +<template> + <gl-alert class="gl-mb-4" :title="title" variant="warning" @dismiss="$emit('dismiss')"> + <details> + <summary> + <gl-sprintf :message="summaryMessage"> + <template #total> + {{ totalWarnings }} + </template> + <template #warningsDisplayed> + {{ maxWarnings }} + </template> + </gl-sprintf> + </summary> + <p + v-for="(warning, index) in limitWarnings" + :key="`warning-${index}`" + data-testid="ci-lint-warning" + > + {{ warning }} + </p> + </details> + </gl-alert> +</template> diff --git a/app/assets/javascripts/ci_lint/graphql/mutations/lint_ci.mutation.graphql b/app/assets/javascripts/ci_lint/graphql/mutations/lint_ci.mutation.graphql new file mode 100644 index 00000000000..496036f690f --- /dev/null +++ b/app/assets/javascripts/ci_lint/graphql/mutations/lint_ci.mutation.graphql @@ -0,0 +1,22 @@ +mutation lintCI($endpoint: String, $content: String, $dry: Boolean) { + lintCI(endpoint: $endpoint, content: $content, dry_run: $dry) @client { + valid + errors + warnings + jobs { + afterScript + allowFailure + beforeScript + environment + except + name + only { + refs + } + afterScript + stage + tagList + when + } + } +} diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci_lint/index.js index ed2cf1fe714..c41e6d47d75 100644 --- a/app/assets/javascripts/ci_lint/index.js +++ b/app/assets/javascripts/ci_lint/index.js @@ -1,16 +1,57 @@ import Vue from 'vue'; -import CILint from './components/ci_lint.vue'; +import VueApollo from 'vue-apollo'; +import axios from '~/lib/utils/axios_utils'; +import createDefaultClient from '~/lib/graphql'; +import CiLint from './components/ci_lint.vue'; + +Vue.use(VueApollo); + +const resolvers = { + Mutation: { + lintCI: (_, { endpoint, content, dry_run }) => { + return axios.post(endpoint, { content, dry_run }).then(({ data }) => ({ + valid: data.valid, + errors: data.errors, + warnings: data.warnings, + jobs: data.jobs.map(job => ({ + name: job.name, + stage: job.stage, + beforeScript: job.before_script, + script: job.script, + afterScript: job.after_script, + tagList: job.tag_list, + environment: job.environment, + when: job.when, + allowFailure: job.allow_failure, + only: { + refs: job.only.refs, + __typename: 'CiLintJobOnlyPolicy', + }, + except: job.except, + __typename: 'CiLintJob', + })), + __typename: 'CiLintContent', + })); + }, + }, +}; + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(resolvers), +}); export default (containerId = '#js-ci-lint') => { const containerEl = document.querySelector(containerId); - const { endpoint } = containerEl.dataset; + const { endpoint, helpPagePath } = containerEl.dataset; return new Vue({ el: containerEl, + apolloProvider, render(createElement) { - return createElement(CILint, { + return createElement(CiLint, { props: { endpoint, + helpPagePath, }, }); }, |