diff options
Diffstat (limited to 'app/assets/javascripts/pipeline_editor')
12 files changed, 410 insertions, 90 deletions
diff --git a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue new file mode 100644 index 00000000000..22f378c571a --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue @@ -0,0 +1,84 @@ +<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'; + +export const i18n = { + learnMore: __('Learn more'), + loading: s__('Pipelines|Validating GitLab CI configuration…'), + invalid: s__('Pipelines|This GitLab CI configuration is invalid.'), + invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'), + valid: s__('Pipelines|This GitLab CI configuration is valid.'), +}; + +export default { + i18n, + components: { + GlIcon, + GlLink, + GlLoadingIcon, + TooltipOnTruncate, + }, + inject: { + ymlHelpPagePath: { + default: '', + }, + }, + props: { + ciConfig: { + type: Object, + required: false, + default: () => ({}), + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isValid() { + return this.ciConfig?.status === CI_CONFIG_STATUS_VALID; + }, + icon() { + if (this.isValid) { + return 'check'; + } + return 'warning-solid'; + }, + message() { + if (this.isValid) { + return this.$options.i18n.valid; + } + + // Only display first error as a reason + const [reason] = this.ciConfig?.errors || []; + if (reason) { + return sprintf(this.$options.i18n.invalidWithReason, { reason }, false); + } + return this.$options.i18n.invalid; + }, + }, +}; +</script> + +<template> + <div> + <template v-if="loading"> + <gl-loading-icon inline /> + {{ $options.i18n.loading }} + </template> + + <span v-else class="gl-display-inline-flex gl-white-space-nowrap gl-max-w-full"> + <tooltip-on-truncate :title="message" class="gl-text-truncate"> + <gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span> + </tooltip-on-truncate> + <span class="gl-flex-shrink-0 gl-pl-2"> + <gl-link data-testid="learnMoreLink" :href="ymlHelpPagePath"> + {{ $options.i18n.learnMore }} + </gl-link> + </span> + </span> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue new file mode 100644 index 00000000000..b27ab9a39d3 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue @@ -0,0 +1,53 @@ +<script> +import { flatten } from 'lodash'; +import { CI_CONFIG_STATUS_VALID } from '../../constants'; +import CiLintResults from './ci_lint_results.vue'; + +export default { + components: { + CiLintResults, + }, + inject: { + lintHelpPagePath: { + default: '', + }, + }, + props: { + ciConfig: { + type: Object, + required: true, + }, + }, + computed: { + isValid() { + return this.ciConfig?.status === CI_CONFIG_STATUS_VALID; + }, + stages() { + return this.ciConfig?.stages || []; + }, + jobs() { + const groupedJobs = this.stages.reduce((acc, { groups, name: stageName }) => { + return acc.concat( + groups.map(({ jobs }) => { + return jobs.map((job) => ({ + stage: stageName, + ...job, + })); + }), + ); + }, []); + + return flatten(groupedJobs); + }, + }, +}; +</script> + +<template> + <ci-lint-results + :valid="isValid" + :jobs="jobs" + :errors="ciConfig.errors" + :lint-help-page-path="lintHelpPagePath" + /> +</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 index 0d1c214c5b1..58a96c3f725 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 @@ -10,11 +10,11 @@ const thBorderColor = 'gl-border-gray-100!'; export default { correct: { variant: 'success', - text: __('syntax is correct.'), + text: __('Syntax is correct.'), }, incorrect: { variant: 'danger', - text: __('syntax is incorrect.'), + text: __('Syntax is incorrect.'), }, includesText: __( 'CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}', @@ -48,19 +48,23 @@ export default { }, jobs: { type: Array, - required: true, + required: false, + default: () => [], }, errors: { type: Array, - required: true, + required: false, + default: () => [], }, warnings: { type: Array, - required: true, + required: false, + default: () => [], }, dryRun: { type: Boolean, - required: true, + required: false, + default: false, }, lintHelpPagePath: { type: String, @@ -99,7 +103,7 @@ export default { data-testid="ci-lint-status" >{{ status.text }} <gl-sprintf :message="$options.includesText"> - <template #code="{content}"> + <template #code="{ content }"> <code> {{ content }} </code> 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 index 4929c3206df..ef2be2a5fba 100644 --- 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 @@ -14,7 +14,7 @@ export default { }, computed: { tagList() { - return this.item.tagList.join(', '); + return this.item.tags?.join(', '); }, onlyPolicy() { return this.item.only ? this.item.only.refs.join(', ') : this.item.only; @@ -26,15 +26,15 @@ export default { return { beforeScript: { show: !isEmpty(this.item.beforeScript), - content: this.item.beforeScript.join('\n'), + content: this.item.beforeScript?.join('\n'), }, script: { show: !isEmpty(this.item.script), - content: this.item.script.join('\n'), + content: this.item.script?.join('\n'), }, afterScript: { show: !isEmpty(this.item.afterScript), - content: this.item.afterScript.join('\n'), + content: this.item.afterScript?.join('\n'), }, }; }, @@ -43,35 +43,43 @@ export default { </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 + <div data-testid="ci-lint-value"> + <pre + v-if="scripts.beforeScript.show" + class="gl-white-space-pre-wrap" + data-testid="ci-lint-before-script" + >{{ scripts.beforeScript.content }}</pre + > + <pre v-if="scripts.script.show" class="gl-white-space-pre-wrap" data-testid="ci-lint-script">{{ + scripts.script.content }}</pre> + <pre + v-if="scripts.afterScript.show" + class="gl-white-space-pre-wrap" + data-testid="ci-lint-after-script" + >{{ scripts.afterScript.content }}</pre + > <ul class="gl-list-style-none gl-pl-0 gl-mb-0"> - <li> + <li v-if="tagList"> <b>{{ __('Tag list:') }}</b> {{ tagList }} </li> <div v-if="!dryRun" data-testid="ci-lint-only-except"> - <li> + <li v-if="onlyPolicy"> <b>{{ __('Only policy:') }}</b> {{ onlyPolicy }} </li> - <li> + <li v-if="exceptPolicy"> <b>{{ __('Except policy:') }}</b> {{ exceptPolicy }} </li> </div> - <li> + <li v-if="item.environment"> <b>{{ __('Environment:') }}</b> {{ item.environment }} </li> - <li> + <li v-if="item.when"> <b>{{ __('When:') }}</b> {{ item.when }} <b v-if="item.allowFailure">{{ __('Allowed to fail') }}</b> diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue index 22f2a32c9ac..b8d49d77ea9 100644 --- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue +++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue @@ -1,14 +1,46 @@ <script> import EditorLite from '~/vue_shared/components/editor_lite.vue'; +import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext'; export default { components: { EditorLite, }, + inject: ['projectPath', 'projectNamespace'], + inheritAttrs: false, + props: { + ciConfigPath: { + type: String, + required: true, + }, + commitSha: { + type: String, + required: false, + default: null, + }, + }, + methods: { + onEditorReady() { + const editorInstance = this.$refs.editor.getEditor(); + + editorInstance.use(new CiSchemaExtension()); + editorInstance.registerCiSchema({ + projectPath: this.projectPath, + projectNamespace: this.projectNamespace, + ref: this.commitSha, + }); + }, + }, }; </script> <template> <div class="gl-border-solid gl-border-gray-100 gl-border-1"> - <editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" /> + <editor-lite + ref="editor" + :file-name="ciConfigPath" + v-bind="$attrs" + @editor-ready="onEditorReady" + v-on="$listeners" + /> </div> </template> diff --git a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue b/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue new file mode 100644 index 00000000000..b0acd3ca2ee --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue @@ -0,0 +1,68 @@ +<script> +import { GlTab } from '@gitlab/ui'; + +/** + * Wrapper of <gl-tab> to optionally lazily render this tab's content + * when its shown **without dismounting after its hidden**. + * + * Usage: + * + * API is the same as <gl-tab>, for example: + * + * <gl-tabs> + * <editor-tab title="Tab 1" :lazy="true"> + * lazily mounted content (gets mounted if this is first tab) + * </editor-tab> + * <editor-tab title="Tab 2" :lazy="true"> + * lazily mounted content + * </editor-tab> + * <editor-tab title="Tab 3"> + * eagerly mounted content + * </editor-tab> + * </gl-tabs> + * + * Once the tab is selected it is permanently set as "not-lazy" + * so it's contents are not dismounted. + * + * lazy is "false" by default, as in <gl-tab>. + */ + +export default { + components: { + GlTab, + // Use a small renderless component to know when the tab content mounts because: + // - gl-tab always gets mounted, even if lazy is `true`. See: + // https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/tabs/tab.js#L180 + // - we cannot listen to events on <slot /> + MountSpy: { + render: () => null, + }, + }, + inheritAttrs: false, + props: { + lazy: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isLazy: this.lazy, + }; + }, + methods: { + onContentMounted() { + // When a child is first mounted make the entire tab + // permanently mounted by setting 'lazy' to false. + this.isLazy = false; + }, + }, +}; +</script> +<template> + <gl-tab :lazy="isLazy" v-bind="$attrs" v-on="$listeners"> + <slot v-for="slot in Object.keys($slots)" :slot="slot" :name="slot"></slot> + <mount-spy @hook:mounted="onContentMounted" /> + </gl-tab> +</template> 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 11bca42fd69..0c58749a8b2 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 @@ -19,7 +19,7 @@ mutation commitCIFileMutation( } ) { commit { - id + sha } 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 index 496036f690f..5091d63111f 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql @@ -15,7 +15,7 @@ mutation lintCI($endpoint: String, $content: String, $dry: Boolean) { } afterScript stage - tagList + tags 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 index d65d9892260..dfddb29701d 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql @@ -1,7 +1,7 @@ -#import "~/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql" +#import "~/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql" -query getCiConfigData($content: String!) { - ciConfig(content: $content) { +query getCiConfigData($projectPath: ID!, $content: String!) { + ciConfig(projectPath: $projectPath, content: $content) { errors status stages { diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js index c1cdb5eb2ee..81e75c32846 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js +++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js @@ -18,7 +18,7 @@ export const resolvers = { valid: data.valid, errors: data.errors, warnings: data.warnings, - jobs: data.jobs.map(job => { + jobs: data.jobs.map((job) => { const only = job.only ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } : null; return { @@ -27,7 +27,7 @@ export const resolvers = { beforeScript: job.before_script, script: job.script, afterScript: job.after_script, - tagList: job.tag_list, + tags: job.tag_list, environment: job.environment, when: job.when, allowFailure: job.allow_failure, diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js index 8268a907a29..583ba555080 100644 --- a/app/assets/javascripts/pipeline_editor/index.js +++ b/app/assets/javascripts/pipeline_editor/index.js @@ -14,7 +14,20 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { return null; } - const { ciConfigPath, commitId, defaultBranch, newMergeRequestPath, projectPath } = el?.dataset; + const { + // props + ciConfigPath, + commitSha, + defaultBranch, + newMergeRequestPath, + + // `provide/inject` data + lintHelpPagePath, + projectFullPath, + projectPath, + projectNamespace, + ymlHelpPagePath, + } = el?.dataset; Vue.use(VueApollo); @@ -25,14 +38,20 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { return new Vue({ el, apolloProvider, + provide: { + lintHelpPagePath, + projectFullPath, + projectPath, + projectNamespace, + ymlHelpPagePath, + }, render(h) { return h(PipelineEditorApp, { props: { ciConfigPath, - commitId, + commitSha, 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 96dc782964b..21993e2120a 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -1,12 +1,16 @@ <script> -import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; -import { mergeUrlParams, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; +import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import httpStatusCodes from '~/lib/utils/http_status'; 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 getBlobContent from './graphql/queries/blob_content.graphql'; @@ -17,33 +21,33 @@ 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_NO_REF = 'LOAD_FAILURE_NO_REF'; const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; export default { components: { + CiLint, CommitForm, + EditorTab, GlAlert, GlLoadingIcon, - GlTab, GlTabs, + GlTab, PipelineGraph, TextEditor, + ValidationSegment, }, mixins: [glFeatureFlagsMixin()], + inject: ['projectFullPath'], props: { - projectPath: { - type: String, - required: true, - }, defaultBranch: { type: String, required: false, default: null, }, - commitId: { + commitSha: { type: String, required: false, default: null, @@ -62,12 +66,15 @@ export default { ciConfigData: {}, content: '', contentModel: '', - currentTabIndex: 0, - editorIsReady: false, - failureType: null, - failureReasons: [], + lastCommitSha: this.commitSha, isSaving: false, + + // Success and failure state + failureType: null, showFailureAlert: false, + failureReasons: [], + successType: null, + showSuccessAlert: false, }; }, apollo: { @@ -75,7 +82,7 @@ export default { query: getBlobContent, variables() { return { - projectPath: this.projectPath, + projectPath: this.projectFullPath, path: this.ciConfigPath, ref: this.defaultBranch, }; @@ -98,15 +105,16 @@ export default { }, variables() { return { + projectPath: this.projectFullPath, content: this.contentModel, }; }, update(data) { - const { ciConfigData } = data || {}; - const stageNodes = ciConfigData?.stages?.nodes || []; + const { ciConfig } = data || {}; + const stageNodes = ciConfig?.stages?.nodes || []; const stages = unwrapStagesWithNeeds(stageNodes); - return { ...ciConfigData, stages }; + return { ...ciConfig, stages }; }, error() { this.reportFailure(LOAD_FAILURE_UNKNOWN); @@ -117,40 +125,48 @@ export default { isBlobContentLoading() { return this.$apollo.queries.content.loading; }, - isVisualizationTabLoading() { - return this.$apollo.queries.ciConfigData.loading; + isBlobContentError() { + return this.failureType === LOAD_FAILURE_NO_FILE; }, - isVisualizeTabActive() { - return this.currentTabIndex === 1; + isCiConfigDataLoading() { + return this.$apollo.queries.ciConfigData.loading; }, defaultCommitMessage() { return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath }); }, - failure() { - switch (this.failureType) { - case LOAD_FAILURE_NO_REF: + success() { + switch (this.successType) { + case COMMIT_SUCCESS: return { - text: this.$options.errorTexts[LOAD_FAILURE_NO_REF], - variant: 'danger', + text: this.$options.alertTexts[COMMIT_SUCCESS], + variant: 'info', }; + default: + return null; + } + }, + failure() { + switch (this.failureType) { case LOAD_FAILURE_NO_FILE: return { - text: this.$options.errorTexts[LOAD_FAILURE_NO_FILE], + text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], { + filePath: this.ciConfigPath, + }), variant: 'danger', }; case LOAD_FAILURE_UNKNOWN: return { - text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN], + text: this.$options.alertTexts[LOAD_FAILURE_UNKNOWN], variant: 'danger', }; case COMMIT_FAILURE: return { - text: this.$options.errorTexts[COMMIT_FAILURE], + text: this.$options.alertTexts[COMMIT_FAILURE], variant: 'danger', }; default: return { - text: this.$options.errorTexts[DEFAULT_FAILURE], + text: this.$options.alertTexts[DEFAULT_FAILURE], variant: 'danger', }; } @@ -160,30 +176,34 @@ export default { defaultCommitMessage: __('Update %{sourcePath} file'), tabEdit: s__('Pipelines|Write pipeline configuration'), tabGraph: s__('Pipelines|Visualize'), + tabLint: s__('Pipelines|Lint'), }, - errorTexts: { - [LOAD_FAILURE_NO_REF]: s__( - 'Pipelines|Repository does not have a default branch, please set one.', + alertTexts: { + [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_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 + // 404 for missing CI file + // 400 for blank projects with no repository + if ( + response?.status === httpStatusCodes.NOT_FOUND || + response?.status === httpStatusCodes.BAD_REQUEST + ) { 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; }, @@ -192,6 +212,14 @@ export default { this.failureType = type; this.failureReasons = reasons; }, + dismissSuccess() { + this.showSuccessAlert = false; + }, + reportSuccess(type) { + this.showSuccessAlert = true; + this.successType = type; + }, + redirectToNewMergeRequest(sourceBranch) { const url = mergeUrlParams( { @@ -209,18 +237,18 @@ export default { try { const { data: { - commitCreate: { errors }, + commitCreate: { errors, commit }, }, } = await this.$apollo.mutate({ mutation: commitCiFileMutation, variables: { - projectPath: this.projectPath, + projectPath: this.projectFullPath, branch, startBranch: this.defaultBranch, message, filePath: this.ciConfigPath, content: this.contentModel, - lastCommitId: this.commitId, + lastCommitId: this.lastCommitSha, }, }); @@ -232,8 +260,10 @@ export default { if (openMergeRequest) { this.redirectToNewMergeRequest(branch); } else { - // Refresh the page to ensure commit is updated - refreshCurrentPage(); + this.reportSuccess(COMMIT_SUCCESS); + + // Update latest commit + this.lastCommitSha = commit.sha; } } catch (error) { this.reportFailure(COMMIT_FAILURE, [error?.message]); @@ -251,6 +281,14 @@ export default { <template> <div class="gl-mt-4"> <gl-alert + v-if="showSuccessAlert" + :variant="success.variant" + :dismissible="true" + @dismiss="dismissSuccess" + > + {{ success.text }} + </gl-alert> + <gl-alert v-if="showFailureAlert" :variant="failure.variant" :dismissible="true" @@ -261,25 +299,39 @@ export default { <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li> </ul> </gl-alert> - <div class="gl-mt-4"> - <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="contentModel" @editor-ready="editorIsReady = true" /> - </gl-tab> + <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" - :lazy="!isVisualizeTabActive" data-testid="visualization-tab" > - <gl-loading-icon v-if="isVisualizationTabLoading" size="lg" class="gl-m-3" /> + <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 |