diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 13:34:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 13:34:06 +0300 |
commit | 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch) | |
tree | d7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/assets/javascripts/pipeline_editor | |
parent | 446d496a6d000c73a304be52587cd9bbc7493136 (diff) |
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipeline_editor')
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> |