summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipeline_editor
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /app/assets/javascripts/pipeline_editor
parent4b1de649d0168371549608993deac953eb692019 (diff)
downloadgitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipeline_editor')
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue139
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue143
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue26
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue81
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue69
-rw-r--r--app/assets/javascripts/pipeline_editor/components/text_editor.vue14
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql26
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql22
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql11
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js31
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js12
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue246
13 files changed, 774 insertions, 48 deletions
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
new file mode 100644
index 00000000000..9279273283e
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -0,0 +1,139 @@
+<script>
+import {
+ GlButton,
+ GlForm,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlForm,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+ },
+ props: {
+ defaultBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ defaultMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isSaving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ message: this.defaultMessage,
+ branch: this.defaultBranch,
+ openMergeRequest: false,
+ };
+ },
+ computed: {
+ isDefaultBranch() {
+ return this.branch === this.defaultBranch;
+ },
+ submitDisabled() {
+ return !(this.message && this.branch);
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$emit('submit', {
+ message: this.message,
+ branch: this.branch,
+ openMergeRequest: this.openMergeRequest,
+ });
+ },
+ onReset() {
+ this.$emit('cancel');
+ },
+ },
+ i18n: {
+ commitMessage: __('Commit message'),
+ targetBranch: __('Target Branch'),
+ startMergeRequest: __('Start a %{new_merge_request} with these changes'),
+ newMergeRequest: __('new merge request'),
+ commitChanges: __('Commit changes'),
+ cancel: __('Cancel'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
+ <gl-form-group
+ id="commit-group"
+ :label="$options.i18n.commitMessage"
+ label-cols-sm="2"
+ label-for="commit-message"
+ >
+ <gl-form-textarea
+ id="commit-message"
+ v-model="message"
+ class="gl-font-monospace!"
+ required
+ :placeholder="defaultMessage"
+ />
+ </gl-form-group>
+ <gl-form-group
+ id="target-branch-group"
+ :label="$options.i18n.targetBranch"
+ label-cols-sm="2"
+ label-for="target-branch-field"
+ >
+ <gl-form-input
+ id="target-branch-field"
+ v-model="branch"
+ class="gl-font-monospace!"
+ required
+ />
+ <gl-form-checkbox
+ v-if="!isDefaultBranch"
+ v-model="openMergeRequest"
+ data-testid="new-mr-checkbox"
+ class="gl-mt-3"
+ >
+ <gl-sprintf :message="$options.i18n.startMergeRequest">
+ <template #new_merge_request>
+ <strong>{{ $options.i18n.newMergeRequest }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-form-checkbox>
+ </gl-form-group>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-p-5 gl-bg-gray-10 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1"
+ >
+ <gl-button
+ type="submit"
+ class="js-no-auto-disable"
+ category="primary"
+ variant="success"
+ :disabled="submitDisabled"
+ :loading="isSaving"
+ >
+ {{ $options.i18n.commitChanges }}
+ </gl-button>
+ <gl-button type="reset" category="secondary" class="gl-mr-3">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
new file mode 100644
index 00000000000..0d1c214c5b1
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
@@ -0,0 +1,143 @@
+<script>
+import { GlAlert, GlLink, GlSprintf, 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 {
+ correct: {
+ variant: 'success',
+ text: __('syntax is correct.'),
+ },
+ incorrect: {
+ variant: 'danger',
+ text: __('syntax is incorrect.'),
+ },
+ includesText: __(
+ 'CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}',
+ ),
+ warningTitle: __('The form contains the following warning:'),
+ fields: [
+ {
+ key: 'parameter',
+ label: __('Parameter'),
+ thClass: thBorderColor,
+ },
+ {
+ key: 'value',
+ label: __('Value'),
+ thClass: thBorderColor,
+ },
+ ],
+ components: {
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ 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,
+ },
+ lintHelpPagePath: {
+ type: String,
+ 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>
+ <gl-alert
+ class="gl-mb-5"
+ :variant="status.variant"
+ :title="__('Status:')"
+ :dismissible="false"
+ data-testid="ci-lint-status"
+ >{{ status.text }}
+ <gl-sprintf :message="$options.includesText">
+ <template #code="{content}">
+ <code>
+ {{ content }}
+ </code>
+ </template>
+ <template #link>
+ <gl-link :href="lintHelpPagePath" target="_blank">
+ {{ __('More information') }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </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/pipeline_editor/components/lint/ci_lint_results_param.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue
new file mode 100644
index 00000000000..23808bcb292
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/lint/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/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue
new file mode 100644
index 00000000000..4929c3206df
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/lint/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/pipeline_editor/components/lint/ci_lint_warnings.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue
new file mode 100644
index 00000000000..ac0332cb0bd
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/lint/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/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
index a925077c906..22f2a32c9ac 100644
--- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
@@ -5,22 +5,10 @@ export default {
components: {
EditorLite,
},
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
};
</script>
<template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1">
- <editor-lite
- v-model="value"
- file-name="*.yml"
- :editor-options="{ readOnly: true }"
- @editor-ready="$emit('editor-ready')"
- />
+ <editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
new file mode 100644
index 00000000000..70bab8092c0
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -0,0 +1,2 @@
+export const CI_CONFIG_STATUS_VALID = 'VALID';
+export const CI_CONFIG_STATUS_INVALID = 'INVALID';
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
new file mode 100644
index 00000000000..11bca42fd69
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
@@ -0,0 +1,26 @@
+mutation commitCIFileMutation(
+ $projectPath: ID!
+ $branch: String!
+ $startBranch: String
+ $message: String!
+ $filePath: String!
+ $lastCommitId: String!
+ $content: String
+) {
+ commitCreate(
+ input: {
+ projectPath: $projectPath
+ branch: $branch
+ startBranch: $startBranch
+ message: $message
+ actions: [
+ { action: UPDATE, filePath: $filePath, lastCommitId: $lastCommitId, content: $content }
+ ]
+ }
+ ) {
+ commit {
+ id
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql
new file mode 100644
index 00000000000..496036f690f
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/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/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
new file mode 100644
index 00000000000..d65d9892260
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
@@ -0,0 +1,11 @@
+#import "~/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql"
+
+query getCiConfigData($content: String!) {
+ ciConfig(content: $content) {
+ errors
+ status
+ stages {
+ ...PipelineStagesConnection
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index 7b8c70ac93e..c1cdb5eb2ee 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,4 +1,5 @@
import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
export const resolvers = {
Query: {
@@ -11,6 +12,32 @@ export 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 => {
+ const only = job.only ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } : null;
-export default resolvers;
+ return {
+ 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,
+ except: job.except,
+ __typename: 'CiLintJob',
+ };
+ }),
+ __typename: 'CiLintContent',
+ }));
+ },
+ },
+};
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index ccd7b74064f..8268a907a29 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -10,7 +10,11 @@ import PipelineEditorApp from './pipeline_editor_app.vue';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
- const { projectPath, defaultBranch, ciConfigPath } = el?.dataset;
+ if (!el) {
+ return null;
+ }
+
+ const { ciConfigPath, commitId, defaultBranch, newMergeRequestPath, projectPath } = el?.dataset;
Vue.use(VueApollo);
@@ -24,9 +28,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
render(h) {
return h(PipelineEditorApp, {
props: {
- projectPath,
- defaultBranch,
ciConfigPath,
+ commitId,
+ defaultBranch,
+ newMergeRequestPath,
+ projectPath,
},
});
},
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 50b946af456..96dc782964b 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,21 +1,38 @@
<script>
-import { GlLoadingIcon, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
+import { mergeUrlParams, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import TextEditor from './components/text_editor.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import CommitForm from './components/commit/commit_form.vue';
+import TextEditor from './components/text_editor.vue';
+import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
+import getCiConfigData from './graphql/queries/ci_config.graphql';
+import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+const COMMIT_FAILURE = 'COMMIT_FAILURE';
+const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
+const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF';
+const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export default {
components: {
- GlLoadingIcon,
+ CommitForm,
GlAlert,
- GlTabs,
+ GlLoadingIcon,
GlTab,
- TextEditor,
+ GlTabs,
PipelineGraph,
+ TextEditor,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
projectPath: {
type: String,
@@ -26,16 +43,31 @@ export default {
required: false,
default: null,
},
+ commitId: {
+ type: String,
+ required: false,
+ default: null,
+ },
ciConfigPath: {
type: String,
required: true,
},
+ newMergeRequestPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- error: null,
+ ciConfigData: {},
content: '',
+ contentModel: '',
+ currentTabIndex: 0,
editorIsReady: false,
+ failureType: null,
+ failureReasons: [],
+ isSaving: false,
+ showFailureAlert: false,
};
},
apollo: {
@@ -51,58 +83,212 @@ export default {
update(data) {
return data?.blobContent?.rawData;
},
+ result({ data }) {
+ this.contentModel = data?.blobContent?.rawData ?? '';
+ },
error(error) {
- this.error = error;
+ this.handleBlobContentError(error);
+ },
+ },
+ ciConfigData: {
+ query: getCiConfigData,
+ // If content is not loaded, we can't lint the data
+ skip: ({ contentModel }) => {
+ return !contentModel;
+ },
+ variables() {
+ return {
+ content: this.contentModel,
+ };
+ },
+ update(data) {
+ const { ciConfigData } = data || {};
+ const stageNodes = ciConfigData?.stages?.nodes || [];
+ const stages = unwrapStagesWithNeeds(stageNodes);
+
+ return { ...ciConfigData, stages };
+ },
+ error() {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN);
},
},
},
computed: {
- loading() {
+ isBlobContentLoading() {
return this.$apollo.queries.content.loading;
},
- errorMessage() {
- const { message: generalReason, networkError } = this.error ?? {};
-
- const { data } = networkError?.response ?? {};
- // 404 for missing file uses `message`
- // 400 for a missing ref uses `error`
- const networkReason = data?.message ?? data?.error;
-
- const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError;
- return sprintf(this.$options.i18n.errorMessageWithReason, { reason });
+ isVisualizationTabLoading() {
+ return this.$apollo.queries.ciConfigData.loading;
+ },
+ isVisualizeTabActive() {
+ return this.currentTabIndex === 1;
},
- pipelineData() {
- // Note data will loaded as part of https://gitlab.com/gitlab-org/gitlab/-/issues/263141
- return {};
+ defaultCommitMessage() {
+ return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
+ },
+ failure() {
+ switch (this.failureType) {
+ case LOAD_FAILURE_NO_REF:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_NO_REF],
+ variant: 'danger',
+ };
+ case LOAD_FAILURE_NO_FILE:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_NO_FILE],
+ variant: 'danger',
+ };
+ case LOAD_FAILURE_UNKNOWN:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
+ variant: 'danger',
+ };
+ case COMMIT_FAILURE:
+ return {
+ text: this.$options.errorTexts[COMMIT_FAILURE],
+ variant: 'danger',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
+ variant: 'danger',
+ };
+ }
},
},
i18n: {
- unknownError: __('Unknown Error'),
- errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'),
+ defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
},
+ errorTexts: {
+ [LOAD_FAILURE_NO_REF]: s__(
+ 'Pipelines|Repository does not have a default branch, please set one.',
+ ),
+ [LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'),
+ [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ },
+ methods: {
+ handleBlobContentError(error = {}) {
+ const { networkError } = error;
+
+ const { response } = networkError;
+ if (response?.status === 404) {
+ // 404 for missing CI file
+ this.reportFailure(LOAD_FAILURE_NO_FILE);
+ } else if (response?.status === 400) {
+ // 400 for a missing ref when no default branch is set
+ this.reportFailure(LOAD_FAILURE_NO_REF);
+ } else {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN);
+ }
+ },
+ dismissFailure() {
+ this.showFailureAlert = false;
+ },
+ reportFailure(type, reasons = []) {
+ this.showFailureAlert = true;
+ this.failureType = type;
+ this.failureReasons = reasons;
+ },
+ redirectToNewMergeRequest(sourceBranch) {
+ const url = mergeUrlParams(
+ {
+ [MR_SOURCE_BRANCH]: sourceBranch,
+ [MR_TARGET_BRANCH]: this.defaultBranch,
+ },
+ this.newMergeRequestPath,
+ );
+ redirectTo(url);
+ },
+ async onCommitSubmit(event) {
+ this.isSaving = true;
+ const { message, branch, openMergeRequest } = event;
+
+ try {
+ const {
+ data: {
+ commitCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: commitCiFileMutation,
+ variables: {
+ projectPath: this.projectPath,
+ branch,
+ startBranch: this.defaultBranch,
+ message,
+ filePath: this.ciConfigPath,
+ content: this.contentModel,
+ lastCommitId: this.commitId,
+ },
+ });
+
+ if (errors?.length) {
+ this.reportFailure(COMMIT_FAILURE, errors);
+ return;
+ }
+
+ if (openMergeRequest) {
+ this.redirectToNewMergeRequest(branch);
+ } else {
+ // Refresh the page to ensure commit is updated
+ refreshCurrentPage();
+ }
+ } catch (error) {
+ this.reportFailure(COMMIT_FAILURE, [error?.message]);
+ } finally {
+ this.isSaving = false;
+ }
+ },
+ onCommitCancel() {
+ this.contentModel = this.content;
+ },
+ },
};
</script>
<template>
<div class="gl-mt-4">
- <gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert>
+ <gl-alert
+ v-if="showFailureAlert"
+ :variant="failure.variant"
+ :dismissible="true"
+ @dismiss="dismissFailure"
+ >
+ {{ failure.text }}
+ <ul v-if="failureReasons.length" class="gl-mb-0">
+ <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
+ </ul>
+ </gl-alert>
<div class="gl-mt-4">
- <gl-loading-icon v-if="loading" size="lg" />
- <div v-else class="file-editor">
- <gl-tabs>
+ <gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
+ <div v-else class="file-editor gl-mb-3">
+ <gl-tabs v-model="currentTabIndex">
<!-- editor should be mounted when its tab is visible, so the container has a size -->
<gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady">
<!-- editor should be mounted only once, when the tab is displayed -->
- <text-editor v-model="content" @editor-ready="editorIsReady = true" />
+ <text-editor v-model="contentModel" @editor-ready="editorIsReady = true" />
</gl-tab>
- <gl-tab :title="$options.i18n.tabGraph">
- <pipeline-graph :pipeline-data="pipelineData" />
+ <gl-tab
+ v-if="glFeatures.ciConfigVisualizationTab"
+ :title="$options.i18n.tabGraph"
+ :lazy="!isVisualizeTabActive"
+ data-testid="visualization-tab"
+ >
+ <gl-loading-icon v-if="isVisualizationTabLoading" size="lg" class="gl-m-3" />
+ <pipeline-graph v-else :pipeline-data="ciConfigData" />
</gl-tab>
</gl-tabs>
</div>
+ <commit-form
+ :default-branch="defaultBranch"
+ :default-message="defaultCommitMessage"
+ :is-saving="isSaving"
+ @cancel="onCommitCancel"
+ @submit="onCommitSubmit"
+ />
</div>
</div>
</template>