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/pipeline_editor')
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue114
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue100
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue (renamed from app/assets/javascripts/pipeline_editor/components/text_editor.vue)30
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue30
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue (renamed from app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue)2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue121
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue26
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js14
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql1
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js36
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue269
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue60
16 files changed, 596 insertions, 220 deletions
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
new file mode 100644
index 00000000000..b40c9a48903
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -0,0 +1,114 @@
+<script>
+import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
+import { COMMIT_FAILURE, COMMIT_SUCCESS } from '../../constants';
+import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
+import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
+
+import CommitForm from './commit_form.vue';
+
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+export default {
+ alertTexts: {
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ },
+ i18n: {
+ defaultCommitMessage: __('Update %{sourcePath} file'),
+ },
+ components: {
+ CommitForm,
+ },
+ inject: ['projectFullPath', 'ciConfigPath', 'defaultBranch', 'newMergeRequestPath'],
+ props: {
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ commit: {},
+ isSaving: false,
+ };
+ },
+ apollo: {
+ commitSha: {
+ query: getCommitSha,
+ },
+ },
+ computed: {
+ defaultCommitMessage() {
+ return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
+ },
+ },
+ methods: {
+ redirectToNewMergeRequest(sourceBranch) {
+ const url = mergeUrlParams(
+ {
+ [MR_SOURCE_BRANCH]: sourceBranch,
+ [MR_TARGET_BRANCH]: this.defaultBranch,
+ },
+ this.newMergeRequestPath,
+ );
+ redirectTo(url);
+ },
+ async onCommitSubmit({ message, branch, openMergeRequest }) {
+ this.isSaving = true;
+
+ try {
+ const {
+ data: {
+ commitCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: commitCIFile,
+ variables: {
+ projectPath: this.projectFullPath,
+ branch,
+ startBranch: this.defaultBranch,
+ message,
+ filePath: this.ciConfigPath,
+ content: this.ciFileContent,
+ lastCommitId: this.commitSha,
+ },
+ update(store, { data }) {
+ const commitSha = data?.commitCreate?.commit?.sha;
+
+ if (commitSha) {
+ store.writeQuery({ query: getCommitSha, data: { commitSha } });
+ }
+ },
+ });
+
+ if (errors?.length) {
+ this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
+ } else if (openMergeRequest) {
+ this.redirectToNewMergeRequest(branch);
+ } else {
+ this.$emit('commit', { type: COMMIT_SUCCESS });
+ }
+ } catch (error) {
+ this.$emit('showError', { type: COMMIT_FAILURE, reasons: [error?.message] });
+ } finally {
+ this.isSaving = false;
+ }
+ },
+ onCommitCancel() {
+ this.$emit('resetContent');
+ },
+ },
+};
+</script>
+
+<template>
+ <commit-form
+ :default-branch="defaultBranch"
+ :default-message="defaultCommitMessage"
+ :is-saving="isSaving"
+ @cancel="onCommitCancel"
+ @submit="onCommitSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
new file mode 100644
index 00000000000..007faa4ed0d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
@@ -0,0 +1,100 @@
+<script>
+import { GlAlert, GlIcon } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { __, s__ } from '~/locale';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
+import { DEFAULT, INVALID_CI_CONFIG } from '~/pipelines/constants';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
+
+export default {
+ i18n: {
+ viewOnlyMessage: s__('Pipelines|Merged YAML is view only'),
+ },
+ errorTexts: {
+ [INVALID_CI_CONFIG]: __('Your CI configuration file is invalid.'),
+ [DEFAULT]: __('An unknown error occurred.'),
+ },
+ components: {
+ EditorLite,
+ GlAlert,
+ GlIcon,
+ },
+ inject: ['ciConfigPath'],
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ failureType: null,
+ };
+ },
+ computed: {
+ failure() {
+ switch (this.failureType) {
+ case INVALID_CI_CONFIG:
+ return this.$options.errorTexts[INVALID_CI_CONFIG];
+ default:
+ return this.$options.errorTexts[DEFAULT];
+ }
+ },
+ fileGlobalId() {
+ return `${this.ciConfigPath}-${uniqueId()}`;
+ },
+ hasError() {
+ return this.failureType;
+ },
+ isInvalidConfiguration() {
+ return this.ciConfigData.status === CI_CONFIG_STATUS_INVALID;
+ },
+ mergedYaml() {
+ return this.ciConfigData.mergedYaml;
+ },
+ },
+ watch: {
+ ciConfigData: {
+ immediate: true,
+ handler() {
+ if (this.isInvalidConfiguration) {
+ this.reportFailure(INVALID_CI_CONFIG);
+ } else if (this.hasError) {
+ this.resetFailure();
+ }
+ },
+ },
+ },
+ methods: {
+ reportFailure(errorType) {
+ this.failureType = errorType;
+ },
+ resetFailure() {
+ this.failureType = null;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-alert v-if="hasError" variant="danger" :dismissible="false">
+ {{ failure }}
+ </gl-alert>
+ <div v-else>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-icon :size="18" name="lock" class="gl-text-gray-500 gl-mr-3" />
+ {{ $options.i18n.viewOnlyMessage }}
+ </div>
+ <div class="gl-mt-3 gl-border-solid gl-border-gray-100 gl-border-1">
+ <editor-lite
+ ref="editor"
+ :value="mergedYaml"
+ :file-name="ciConfigPath"
+ :file-global-id="fileGlobalId"
+ :editor-options="{ readOnly: true }"
+ v-on="$listeners"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
index b8d49d77ea9..872da88d3e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
@@ -1,26 +1,30 @@
<script>
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
export default {
components: {
EditorLite,
},
- inject: ['projectPath', 'projectNamespace'],
+ inject: ['ciConfigPath', 'projectPath', 'projectNamespace'],
inheritAttrs: false,
- props: {
- ciConfigPath: {
- type: String,
- required: true,
- },
+ data() {
+ return {
+ commitSha: '',
+ };
+ },
+ apollo: {
commitSha: {
- type: String,
- required: false,
- default: null,
+ query: getCommitSha,
},
},
methods: {
- onEditorReady() {
+ onCiConfigUpdate(content) {
+ this.$emit('updateCiConfig', content);
+ },
+ registerCiSchema() {
const editorInstance = this.$refs.editor.getEditor();
editorInstance.use(new CiSchemaExtension());
@@ -31,6 +35,7 @@ export default {
});
},
},
+ readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
@@ -39,7 +44,8 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
- @editor-ready="onEditorReady"
+ @[$options.readyEvent]="registerCiSchema"
+ @input="onCiConfigUpdate"
v-on="$listeners"
/>
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
new file mode 100644
index 00000000000..ab41c0170e9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -0,0 +1,30 @@
+<script>
+import ValidationSegment from './validation_segment.vue';
+
+export default {
+ validationSegmentClasses:
+ 'gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base',
+ components: {
+ ValidationSegment,
+ },
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-mb-5">
+ <validation-segment
+ :class="$options.validationSegmentClasses"
+ :loading="isCiConfigDataLoading"
+ :ci-config="ciConfigData"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 22f378c571a..94fb3a66fdd 100644
--- a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import { CI_CONFIG_STATUS_VALID } from '../../constants';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { CI_CONFIG_STATUS_VALID } from '../../constants';
export const i18n = {
learnMore: __('Learn more'),
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
index 58a96c3f725..5d9697c9427 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
@@ -1,9 +1,9 @@
<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';
+import CiLintResultsParam from './ci_lint_results_param.vue';
+import CiLintResultsValue from './ci_lint_results_value.vue';
+import CiLintWarnings from './ci_lint_warnings.vue';
const thBorderColor = 'gl-border-gray-100!';
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
index 23808bcb292..49225a7cac7 100644
--- 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
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
export default {
props: {
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
new file mode 100644
index 00000000000..3bdcf383bee
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+ CI_CONFIG_STATUS_INVALID,
+ CREATE_TAB,
+ LINT_TAB,
+ MERGED_TAB,
+ VISUALIZE_TAB,
+} from '../constants';
+import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
+import TextEditor from './editor/text_editor.vue';
+import CiLint from './lint/ci_lint.vue';
+import EditorTab from './ui/editor_tab.vue';
+
+export default {
+ i18n: {
+ tabEdit: s__('Pipelines|Write pipeline configuration'),
+ tabGraph: s__('Pipelines|Visualize'),
+ tabLint: s__('Pipelines|Lint'),
+ tabMergedYaml: s__('Pipelines|View merged YAML'),
+ },
+ errorTexts: {
+ loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
+ },
+ tabConstants: {
+ CREATE_TAB,
+ LINT_TAB,
+ MERGED_TAB,
+ VISUALIZE_TAB,
+ },
+ components: {
+ CiConfigMergedPreview,
+ CiLint,
+ EditorTab,
+ GlAlert,
+ GlLoadingIcon,
+ GlTab,
+ GlTabs,
+ PipelineGraph,
+ TextEditor,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hasMergedYamlLoadError() {
+ return (
+ !this.ciConfigData?.mergedYaml && this.ciConfigData.status !== CI_CONFIG_STATUS_INVALID
+ );
+ },
+ },
+ methods: {
+ setCurrentTab(tabName) {
+ this.$emit('set-current-tab', tabName);
+ },
+ },
+};
+</script>
+<template>
+ <gl-tabs class="file-editor gl-mb-3">
+ <editor-tab
+ class="gl-mb-3"
+ :title="$options.i18n.tabEdit"
+ lazy
+ data-testid="editor-tab"
+ @click="setCurrentTab($options.tabConstants.CREATE_TAB)"
+ >
+ <text-editor :value="ciFileContent" v-on="$listeners" />
+ </editor-tab>
+ <gl-tab
+ v-if="glFeatures.ciConfigVisualizationTab"
+ class="gl-mb-3"
+ :title="$options.i18n.tabGraph"
+ lazy
+ data-testid="visualization-tab"
+ @click="setCurrentTab($options.tabConstants.VISUALIZE_TAB)"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <pipeline-graph v-else :pipeline-data="ciConfigData" />
+ </gl-tab>
+ <editor-tab
+ class="gl-mb-3"
+ :title="$options.i18n.tabLint"
+ data-testid="lint-tab"
+ @click="setCurrentTab($options.tabConstants.LINT_TAB)"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <ci-lint v-else :ci-config="ciConfigData" />
+ </editor-tab>
+ <gl-tab
+ v-if="glFeatures.ciConfigMergedTab"
+ class="gl-mb-3"
+ :title="$options.i18n.tabMergedYaml"
+ lazy
+ data-testid="merged-tab"
+ @click="setCurrentTab($options.tabConstants.MERGED_TAB)"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <gl-alert v-else-if="hasMergedYamlLoadError" variant="danger" :dismissible="false">
+ {{ $options.errorTexts.loadMergedYaml }}
+ </gl-alert>
+ <ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" />
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue b/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
new file mode 100644
index 00000000000..bc076fbe349
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
@@ -0,0 +1,26 @@
+<script>
+export default {
+ props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ created() {
+ window.addEventListener('beforeunload', this.confirmChanges);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.confirmChanges);
+ },
+ methods: {
+ confirmChanges(e = {}) {
+ if (this.hasUnsavedChanges) {
+ e.preventDefault();
+ // eslint-disable-next-line no-param-reassign
+ e.returnValue = ''; // Chrome requires returnValue to be set
+ }
+ },
+ },
+ render: () => null,
+};
+</script>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 70bab8092c0..e676fdeae02 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -1,2 +1,16 @@
export const CI_CONFIG_STATUS_VALID = 'VALID';
export const CI_CONFIG_STATUS_INVALID = 'INVALID';
+
+export const COMMIT_FAILURE = 'COMMIT_FAILURE';
+export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
+
+export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
+export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+
+export const CREATE_TAB = 'CREATE_TAB';
+export const LINT_TAB = 'LINT_TAB';
+export const MERGED_TAB = 'MERGED_TAB';
+export const VISUALIZE_TAB = 'VISUALIZE_TAB';
+
+export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB];
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
index 0c58749a8b2..2af0cd5f6d4 100644
--- 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
@@ -1,4 +1,4 @@
-mutation commitCIFileMutation(
+mutation commitCIFile(
$projectPath: ID!
$branch: String!
$startBranch: String
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
index dfddb29701d..30c18a96536 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
@@ -3,6 +3,7 @@
query getCiConfigData($projectPath: ID!, $content: String!) {
ciConfig(projectPath: $projectPath, content: $content) {
errors
+ mergedYaml
status
stages {
...PipelineStagesConnection
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
new file mode 100644
index 00000000000..6c7635887ec
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
@@ -0,0 +1,3 @@
+query getCommitSha {
+ commitSha @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 583ba555080..dc427f55b5f 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -2,12 +2,17 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import typeDefs from './graphql/typedefs.graphql';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { resolvers } from './graphql/resolvers';
-
+import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
+ // Prevent issues loading syntax validation workers
+ // Fixes https://gitlab.com/gitlab-org/gitlab/-/issues/297252
+ // TODO Remove when https://gitlab.com/gitlab-org/gitlab/-/issues/321656 is resolved
+ resetServiceWorkersPublicPath();
+
const el = document.querySelector(selector);
if (!el) {
@@ -15,14 +20,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
}
const {
- // props
- ciConfigPath,
+ // Add to apollo cache as it can be updated by future queries
commitSha,
+ // Add to provide/inject API for static values
+ ciConfigPath,
defaultBranch,
- newMergeRequestPath,
-
- // `provide/inject` data
lintHelpPagePath,
+ newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
@@ -35,25 +39,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, { typeDefs }),
});
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ commitSha,
+ },
+ });
+
return new Vue({
el,
apolloProvider,
provide: {
+ ciConfigPath,
+ defaultBranch,
lintHelpPagePath,
+ newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
ymlHelpPagePath,
},
render(h) {
- return h(PipelineEditorApp, {
- props: {
- ciConfigPath,
- commitSha,
- defaultBranch,
- newMergeRequestPath,
- },
- });
+ return h(PipelineEditorApp);
},
});
};
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 21993e2120a..b4a818e2472 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,84 +1,55 @@
<script>
-import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
-import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
+import { __, s__, sprintf } from '~/locale';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import CiLint from './components/lint/ci_lint.vue';
-import CommitForm from './components/commit/commit_form.vue';
-import EditorTab from './components/ui/editor_tab.vue';
-import TextEditor from './components/text_editor.vue';
-import ValidationSegment from './components/info/validation_segment.vue';
-
-import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
+import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
+import {
+ COMMIT_FAILURE,
+ COMMIT_SUCCESS,
+ DEFAULT_FAILURE,
+ LOAD_FAILURE_NO_FILE,
+ LOAD_FAILURE_UNKNOWN,
+} from './constants';
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 COMMIT_SUCCESS = 'COMMIT_SUCCESS';
-const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
-const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
-const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
- CiLint,
- CommitForm,
- EditorTab,
+ ConfirmUnsavedChangesDialog,
GlAlert,
GlLoadingIcon,
- GlTabs,
- GlTab,
- PipelineGraph,
- TextEditor,
- ValidationSegment,
+ PipelineEditorHome,
},
- mixins: [glFeatureFlagsMixin()],
- inject: ['projectFullPath'],
- props: {
- defaultBranch: {
- type: String,
- required: false,
- default: null,
+ inject: {
+ ciConfigPath: {
+ default: '',
},
- commitSha: {
- type: String,
- required: false,
+ defaultBranch: {
default: null,
},
- ciConfigPath: {
- type: String,
- required: true,
- },
- newMergeRequestPath: {
- type: String,
- required: true,
+ projectFullPath: {
+ default: '',
},
},
data() {
return {
ciConfigData: {},
- content: '',
- contentModel: '',
- lastCommitSha: this.commitSha,
- isSaving: false,
-
// Success and failure state
failureType: null,
- showFailureAlert: false,
failureReasons: [],
- successType: null,
+ initialCiFileContent: '',
+ lastCommittedContent: '',
+ currentCiFileContent: '',
+ showFailureAlert: false,
showSuccessAlert: false,
+ successType: null,
};
},
apollo: {
- content: {
+ initialCiFileContent: {
query: getBlobContent,
variables() {
return {
@@ -91,7 +62,10 @@ export default {
return data?.blobContent?.rawData;
},
result({ data }) {
- this.contentModel = data?.blobContent?.rawData ?? '';
+ const fileContent = data?.blobContent?.rawData ?? '';
+
+ this.lastCommittedContent = fileContent;
+ this.currentCiFileContent = fileContent;
},
error(error) {
this.handleBlobContentError(error);
@@ -100,13 +74,13 @@ export default {
ciConfigData: {
query: getCiConfigData,
// If content is not loaded, we can't lint the data
- skip: ({ contentModel }) => {
- return !contentModel;
+ skip: ({ currentCiFileContent }) => {
+ return !currentCiFileContent;
},
variables() {
return {
projectPath: this.projectFullPath,
- content: this.contentModel,
+ content: this.currentCiFileContent,
};
},
update(data) {
@@ -122,8 +96,11 @@ export default {
},
},
computed: {
+ hasUnsavedChanges() {
+ return this.lastCommittedContent !== this.currentCiFileContent;
+ },
isBlobContentLoading() {
- return this.$apollo.queries.content.loading;
+ return this.$apollo.queries.initialCiFileContent.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE;
@@ -131,62 +108,60 @@ export default {
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
- defaultCommitMessage() {
- return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
- },
- success() {
- switch (this.successType) {
- case COMMIT_SUCCESS:
- return {
- text: this.$options.alertTexts[COMMIT_SUCCESS],
- variant: 'info',
- };
- default:
- return null;
- }
- },
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_FILE:
return {
- text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
+ text: sprintf(this.$options.errorTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
return {
- text: this.$options.alertTexts[LOAD_FAILURE_UNKNOWN],
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
variant: 'danger',
};
case COMMIT_FAILURE:
return {
- text: this.$options.alertTexts[COMMIT_FAILURE],
+ text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
default:
return {
- text: this.$options.alertTexts[DEFAULT_FAILURE],
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
variant: 'danger',
};
}
},
+ success() {
+ switch (this.successType) {
+ case COMMIT_SUCCESS:
+ return {
+ text: this.$options.successTexts[COMMIT_SUCCESS],
+ variant: 'info',
+ };
+ default:
+ return null;
+ }
+ },
},
i18n: {
- defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
- alertTexts: {
+ errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
- [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
+ successTexts: {
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ },
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
@@ -207,72 +182,32 @@ export default {
dismissFailure() {
this.showFailureAlert = false;
},
+ dismissSuccess() {
+ this.showSuccessAlert = false;
+ },
reportFailure(type, reasons = []) {
this.showFailureAlert = true;
this.failureType = type;
this.failureReasons = reasons;
},
- dismissSuccess() {
- this.showSuccessAlert = false;
- },
reportSuccess(type) {
this.showSuccessAlert = true;
this.successType = type;
},
-
- redirectToNewMergeRequest(sourceBranch) {
- const url = mergeUrlParams(
- {
- [MR_SOURCE_BRANCH]: sourceBranch,
- [MR_TARGET_BRANCH]: this.defaultBranch,
- },
- this.newMergeRequestPath,
- );
- redirectTo(url);
+ resetContent() {
+ this.currentCiFileContent = this.lastCommittedContent;
},
- async onCommitSubmit(event) {
- this.isSaving = true;
- const { message, branch, openMergeRequest } = event;
-
- try {
- const {
- data: {
- commitCreate: { errors, commit },
- },
- } = await this.$apollo.mutate({
- mutation: commitCiFileMutation,
- variables: {
- projectPath: this.projectFullPath,
- branch,
- startBranch: this.defaultBranch,
- message,
- filePath: this.ciConfigPath,
- content: this.contentModel,
- lastCommitId: this.lastCommitSha,
- },
- });
-
- if (errors?.length) {
- this.reportFailure(COMMIT_FAILURE, errors);
- return;
- }
-
- if (openMergeRequest) {
- this.redirectToNewMergeRequest(branch);
- } else {
- this.reportSuccess(COMMIT_SUCCESS);
-
- // Update latest commit
- this.lastCommitSha = commit.sha;
- }
- } catch (error) {
- this.reportFailure(COMMIT_FAILURE, [error?.message]);
- } finally {
- this.isSaving = false;
- }
+ showErrorAlert({ type, reasons = [] }) {
+ this.reportFailure(type, reasons);
},
- onCommitCancel() {
- this.contentModel = this.content;
+ updateCiConfig(ciFileContent) {
+ this.currentCiFileContent = ciFileContent;
+ },
+ updateOnCommit({ type }) {
+ this.reportSuccess(type);
+ // Keep track of the latest commited content to know
+ // if the user has made changes to the file that are unsaved.
+ this.lastCommittedContent = this.currentCiFileContent;
},
},
};
@@ -280,20 +215,10 @@ export default {
<template>
<div class="gl-mt-4">
- <gl-alert
- v-if="showSuccessAlert"
- :variant="success.variant"
- :dismissible="true"
- @dismiss="dismissSuccess"
- >
+ <gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
</gl-alert>
- <gl-alert
- v-if="showFailureAlert"
- :variant="failure.variant"
- :dismissible="true"
- @dismiss="dismissFailure"
- >
+ <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="dismissFailure">
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
@@ -301,46 +226,16 @@ export default {
</gl-alert>
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
- <div class="file-editor gl-mb-3">
- <div class="info-well gl-display-none gl-display-sm-block">
- <validation-segment
- class="well-segment"
- :loading="isCiConfigDataLoading"
- :ci-config="ciConfigData"
- />
- </div>
-
- <gl-tabs>
- <editor-tab :lazy="true" :title="$options.i18n.tabEdit">
- <text-editor
- v-model="contentModel"
- :ci-config-path="ciConfigPath"
- :commit-sha="lastCommitSha"
- />
- </editor-tab>
- <gl-tab
- v-if="glFeatures.ciConfigVisualizationTab"
- :lazy="true"
- :title="$options.i18n.tabGraph"
- data-testid="visualization-tab"
- >
- <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
- <pipeline-graph v-else :pipeline-data="ciConfigData" />
- </gl-tab>
-
- <editor-tab :title="$options.i18n.tabLint">
- <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
- <ci-lint v-else :ci-config="ciConfigData" />
- </editor-tab>
- </gl-tabs>
- </div>
- <commit-form
- :default-branch="defaultBranch"
- :default-message="defaultCommitMessage"
- :is-saving="isSaving"
- @cancel="onCommitCancel"
- @submit="onCommitSubmit"
+ <pipeline-editor-home
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ :ci-config-data="ciConfigData"
+ :ci-file-content="currentCiFileContent"
+ @commit="updateOnCommit"
+ @resetContent="resetContent"
+ @showError="showErrorAlert"
+ @updateCiConfig="updateCiConfig"
/>
</div>
+ <confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
new file mode 100644
index 00000000000..8c9aad6ed87
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -0,0 +1,60 @@
+<script>
+import CommitSection from './components/commit/commit_section.vue';
+import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
+import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
+import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
+
+export default {
+ components: {
+ CommitSection,
+ PipelineEditorHeader,
+ PipelineEditorTabs,
+ },
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentTab: CREATE_TAB,
+ };
+ },
+ computed: {
+ showCommitForm() {
+ return TABS_WITH_COMMIT_FORM.includes(this.currentTab);
+ },
+ },
+ methods: {
+ setCurrentTab(tabName) {
+ this.currentTab = tabName;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <pipeline-editor-header
+ :ci-config-data="ciConfigData"
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ />
+ <pipeline-editor-tabs
+ :ci-config-data="ciConfigData"
+ :ci-file-content="ciFileContent"
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ v-on="$listeners"
+ @set-current-tab="setCurrentTab"
+ />
+ <commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
+ </div>
+</template>