Commit 247edd66 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'pipeline-editor-global-state' into 'master'

Adds a global status to the pipeline editor section

See merge request gitlab-org/gitlab!55787
parents a043e506 dd287fad
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
return { return {
content: '', content: '',
loading: false, loading: false,
valid: false, isValid: false,
errors: null, errors: null,
warnings: null, warnings: null,
jobs: [], jobs: [],
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
}); });
this.showingResults = true; this.showingResults = true;
this.valid = valid; this.isValid = valid;
this.errors = errors; this.errors = errors;
this.warnings = warnings; this.warnings = warnings;
this.jobs = jobs; this.jobs = jobs;
...@@ -120,7 +120,7 @@ export default { ...@@ -120,7 +120,7 @@ export default {
<ci-lint-results <ci-lint-results
v-if="showingResults" v-if="showingResults"
class="col-sm-12 gl-mt-5" class="col-sm-12 gl-mt-5"
:valid="valid" :is-valid="isValid"
:jobs="jobs" :jobs="jobs"
:errors="errors" :errors="errors"
:warnings="warnings" :warnings="warnings"
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import { GlAlert, GlIcon } from '@gitlab/ui'; import { GlAlert, GlIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { DEFAULT, INVALID_CI_CONFIG } from '~/pipelines/constants'; import { DEFAULT, INVALID_CI_CONFIG } from '~/pipelines/constants';
import EditorLite from '~/vue_shared/components/editor_lite.vue'; import EditorLite from '~/vue_shared/components/editor_lite.vue';
...@@ -21,6 +20,10 @@ export default { ...@@ -21,6 +20,10 @@ export default {
}, },
inject: ['ciConfigPath'], inject: ['ciConfigPath'],
props: { props: {
isValid: {
type: Boolean,
required: true,
},
ciConfigData: { ciConfigData: {
type: Object, type: Object,
required: true, required: true,
...@@ -46,9 +49,6 @@ export default { ...@@ -46,9 +49,6 @@ export default {
hasError() { hasError() {
return this.failureType; return this.failureType;
}, },
isInvalidConfiguration() {
return this.ciConfigData.status === CI_CONFIG_STATUS_INVALID;
},
mergedYaml() { mergedYaml() {
return this.ciConfigData.mergedYaml; return this.ciConfigData.mergedYaml;
}, },
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
ciConfigData: { ciConfigData: {
immediate: true, immediate: true,
handler() { handler() {
if (this.isInvalidConfiguration) { if (!this.isValid) {
this.reportFailure(INVALID_CI_CONFIG); this.reportFailure(INVALID_CI_CONFIG);
} else if (this.hasError) { } else if (this.hasError) {
this.resetFailure(); this.resetFailure();
......
...@@ -31,18 +31,10 @@ export default { ...@@ -31,18 +31,10 @@ export default {
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
ciFileContent: {
type: String,
required: true,
},
ciConfigData: { ciConfigData: {
type: Object, type: Object,
required: true, required: true,
}, },
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
showPipelineStatus() { showPipelineStatus() {
...@@ -61,11 +53,6 @@ export default { ...@@ -61,11 +53,6 @@ export default {
<template> <template>
<div class="gl-mb-5"> <div class="gl-mb-5">
<pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" /> <pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
<validation-segment <validation-segment :class="validationStyling" :ci-config="ciConfigData" />
:class="validationStyling"
:loading="isCiConfigDataLoading"
:ci-file-content="ciFileContent"
:ci-config="ciConfigData"
/>
</div> </div>
</template> </template>
<script> <script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { CI_CONFIG_STATUS_VALID } from '../../constants'; import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '../../constants';
export const i18n = { export const i18n = {
empty: __( empty: __(
...@@ -29,47 +34,51 @@ export default { ...@@ -29,47 +34,51 @@ export default {
}, },
}, },
props: { props: {
ciFileContent: {
type: String,
required: true,
},
ciConfig: { ciConfig: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
loading: { },
type: Boolean, apollo: {
required: false, appStatus: {
default: false, query: getAppStatus,
}, },
}, },
computed: { computed: {
isEmpty() { isEmpty() {
return !this.ciFileContent; return this.appStatus === EDITOR_APP_STATUS_EMPTY;
},
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
}, },
isValid() { isValid() {
return this.ciConfig?.status === CI_CONFIG_STATUS_VALID; return this.appStatus === EDITOR_APP_STATUS_VALID;
}, },
icon() { icon() {
if (this.isValid || this.isEmpty) { switch (this.appStatus) {
return 'check'; case EDITOR_APP_STATUS_EMPTY:
return 'check';
case EDITOR_APP_STATUS_VALID:
return 'check';
default:
return 'warning-solid';
} }
return 'warning-solid';
}, },
message() { message() {
if (this.isEmpty) {
return this.$options.i18n.empty;
} else if (this.isValid) {
return this.$options.i18n.valid;
}
// Only display first error as a reason
const [reason] = this.ciConfig?.errors || []; const [reason] = this.ciConfig?.errors || [];
if (reason) {
return sprintf(this.$options.i18n.invalidWithReason, { reason }, false); switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return this.$options.i18n.empty;
case EDITOR_APP_STATUS_VALID:
return this.$options.i18n.valid;
default:
// Only display first error as a reason
return this.ciConfig?.errors.length > 0
? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
: this.$options.i18n.invalid;
} }
return this.$options.i18n.invalid;
}, },
}, },
}; };
...@@ -77,7 +86,7 @@ export default { ...@@ -77,7 +86,7 @@ export default {
<template> <template>
<div> <div>
<template v-if="loading"> <template v-if="isLoading">
<gl-loading-icon inline /> <gl-loading-icon inline />
{{ $options.i18n.loading }} {{ $options.i18n.loading }}
</template> </template>
......
<script> <script>
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { CI_CONFIG_STATUS_VALID } from '../../constants';
import CiLintResults from './ci_lint_results.vue'; import CiLintResults from './ci_lint_results.vue';
export default { export default {
...@@ -13,15 +12,16 @@ export default { ...@@ -13,15 +12,16 @@ export default {
}, },
}, },
props: { props: {
isValid: {
type: Boolean,
required: true,
},
ciConfig: { ciConfig: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
computed: { computed: {
isValid() {
return this.ciConfig?.status === CI_CONFIG_STATUS_VALID;
},
stages() { stages() {
return this.ciConfig?.stages || []; return this.ciConfig?.stages || [];
}, },
...@@ -45,9 +45,9 @@ export default { ...@@ -45,9 +45,9 @@ export default {
<template> <template>
<ci-lint-results <ci-lint-results
:valid="isValid"
:jobs="jobs"
:errors="ciConfig.errors" :errors="ciConfig.errors"
:is-valid="isValid"
:jobs="jobs"
:lint-help-page-path="lintHelpPagePath" :lint-help-page-path="lintHelpPagePath"
/> />
</template> </template>
...@@ -42,34 +42,34 @@ export default { ...@@ -42,34 +42,34 @@ export default {
CiLintResultsParam, CiLintResultsParam,
}, },
props: { props: {
valid: {
type: Boolean,
required: true,
},
jobs: {
type: Array,
required: false,
default: () => [],
},
errors: { errors: {
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
}, },
warnings: {
type: Array,
required: false,
default: () => [],
},
dryRun: { dryRun: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
isValid: {
type: Boolean,
required: true,
},
jobs: {
type: Array,
required: false,
default: () => [],
},
lintHelpPagePath: { lintHelpPagePath: {
type: String, type: String,
required: true, required: true,
}, },
warnings: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
...@@ -78,7 +78,7 @@ export default { ...@@ -78,7 +78,7 @@ export default {
}, },
computed: { computed: {
status() { status() {
return this.valid ? this.$options.correct : this.$options.incorrect; return this.isValid ? this.$options.correct : this.$options.incorrect;
}, },
shouldShowTable() { shouldShowTable() {
return this.errors.length === 0; return this.errors.length === 0;
......
...@@ -4,12 +4,15 @@ import { s__ } from '~/locale'; ...@@ -4,12 +4,15 @@ import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { import {
CI_CONFIG_STATUS_INVALID,
CREATE_TAB, CREATE_TAB,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
LINT_TAB, LINT_TAB,
MERGED_TAB, MERGED_TAB,
VISUALIZE_TAB, VISUALIZE_TAB,
} from '../constants'; } from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue'; import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
import TextEditor from './editor/text_editor.vue'; import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue'; import CiLint from './lint/ci_lint.vue';
...@@ -52,17 +55,22 @@ export default { ...@@ -52,17 +55,22 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isCiConfigDataLoading: { },
type: Boolean, apollo: {
required: false, appStatus: {
default: false, query: getAppStatus,
}, },
}, },
computed: { computed: {
hasMergedYamlLoadError() { hasAppError() {
return ( // Not an invalid config and with `mergedYaml` data missing
!this.ciConfigData?.mergedYaml && this.ciConfigData.status !== CI_CONFIG_STATUS_INVALID return this.appStatus === EDITOR_APP_STATUS_ERROR;
); },
isValid() {
return this.appStatus === EDITOR_APP_STATUS_VALID;
},
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
}, },
}, },
methods: { methods: {
...@@ -91,7 +99,7 @@ export default { ...@@ -91,7 +99,7 @@ export default {
data-testid="visualization-tab" data-testid="visualization-tab"
@click="setCurrentTab($options.tabConstants.VISUALIZE_TAB)" @click="setCurrentTab($options.tabConstants.VISUALIZE_TAB)"
> >
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<pipeline-graph v-else :pipeline-data="ciConfigData" /> <pipeline-graph v-else :pipeline-data="ciConfigData" />
</gl-tab> </gl-tab>
<editor-tab <editor-tab
...@@ -100,8 +108,8 @@ export default { ...@@ -100,8 +108,8 @@ export default {
data-testid="lint-tab" data-testid="lint-tab"
@click="setCurrentTab($options.tabConstants.LINT_TAB)" @click="setCurrentTab($options.tabConstants.LINT_TAB)"
> >
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<ci-lint v-else :ci-config="ciConfigData" /> <ci-lint v-else :is-valid="isValid" :ci-config="ciConfigData" />
</editor-tab> </editor-tab>
<gl-tab <gl-tab
v-if="glFeatures.ciConfigMergedTab" v-if="glFeatures.ciConfigMergedTab"
...@@ -111,11 +119,16 @@ export default { ...@@ -111,11 +119,16 @@ export default {
data-testid="merged-tab" data-testid="merged-tab"
@click="setCurrentTab($options.tabConstants.MERGED_TAB)" @click="setCurrentTab($options.tabConstants.MERGED_TAB)"
> >
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<gl-alert v-else-if="hasMergedYamlLoadError" variant="danger" :dismissible="false"> <gl-alert v-else-if="hasAppError" variant="danger" :dismissible="false">
{{ $options.errorTexts.loadMergedYaml }} {{ $options.errorTexts.loadMergedYaml }}
</gl-alert> </gl-alert>
<ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" /> <ci-config-merged-preview
v-else
:is-valid="isValid"
:ci-config-data="ciConfigData"
v-on="$listeners"
/>
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
</template> </template>
export const CI_CONFIG_STATUS_VALID = 'VALID'; // Values for CI_CONFIG_STATUS_* comes from lint graphQL
export const CI_CONFIG_STATUS_INVALID = 'INVALID'; export const CI_CONFIG_STATUS_INVALID = 'INVALID';
export const CI_CONFIG_STATUS_VALID = 'VALID';
// Values for EDITOR_APP_STATUS_* are frontend specifics and
// represent the global state of the pipeline editor app.
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
export const EDITOR_APP_STATUS_ERROR = 'ERROR';
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
export const COMMIT_FAILURE = 'COMMIT_FAILURE'; export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS'; export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
......
...@@ -3,6 +3,7 @@ import Vue from 'vue'; ...@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { EDITOR_APP_STATUS_LOADING } from './constants';
import { resolvers } from './graphql/resolvers'; import { resolvers } from './graphql/resolvers';
import typeDefs from './graphql/typedefs.graphql'; import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue'; import PipelineEditorApp from './pipeline_editor_app.vue';
...@@ -45,6 +46,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -45,6 +46,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
data: { data: {
currentBranch: initialBranchName || defaultBranch, currentBranch: initialBranchName || defaultBranch,
commitSha, commitSha,
status: EDITOR_APP_STATUS_LOADING,
}, },
}); });
......
...@@ -6,9 +6,18 @@ import { __, s__ } from '~/locale'; ...@@ -6,9 +6,18 @@ import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils'; import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue'; import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_FAILURE, COMMIT_SUCCESS, DEFAULT_FAILURE, LOAD_FAILURE_UNKNOWN } from './constants'; import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
LOAD_FAILURE_UNKNOWN,
} from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql'; import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql'; import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql'; import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql'; import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue'; import PipelineEditorHome from './pipeline_editor_home.vue';
...@@ -32,7 +41,6 @@ export default { ...@@ -32,7 +41,6 @@ export default {
data() { data() {
return { return {
ciConfigData: {}, ciConfigData: {},
// Success and failure state
failureType: null, failureType: null,
failureReasons: [], failureReasons: [],
showStartScreen: false, showStartScreen: false,
...@@ -77,8 +85,7 @@ export default { ...@@ -77,8 +85,7 @@ export default {
}, },
ciConfigData: { ciConfigData: {
query: getCiConfigData, query: getCiConfigData,
// If content is not loaded, we can't lint the data skip({ currentCiFileContent }) {
skip: ({ currentCiFileContent }) => {
return !currentCiFileContent; return !currentCiFileContent;
}, },
variables() { variables() {
...@@ -94,9 +101,20 @@ export default { ...@@ -94,9 +101,20 @@ export default {
return { ...ciConfig, stages }; return { ...ciConfig, stages };
}, },
result({ data }) {
this.setAppStatus(data?.ciConfig?.status || EDITOR_APP_STATUS_ERROR);
},
error() { error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN); this.reportFailure(LOAD_FAILURE_UNKNOWN);
}, },
watchLoading(isLoading) {
if (isLoading) {
this.setAppStatus(EDITOR_APP_STATUS_LOADING);
}
},
},
appStatus: {
query: getAppStatus,
}, },
currentBranch: { currentBranch: {
query: getCurrentBranch, query: getCurrentBranch,
...@@ -115,6 +133,9 @@ export default { ...@@ -115,6 +133,9 @@ export default {
isCiConfigDataLoading() { isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading; return this.$apollo.queries.ciConfigData.loading;
}, },
isEmpty() {
return this.currentCiFileContent === '';
},
failure() { failure() {
switch (this.failureType) { switch (this.failureType) {
case LOAD_FAILURE_UNKNOWN: case LOAD_FAILURE_UNKNOWN:
...@@ -159,6 +180,13 @@ export default { ...@@ -159,6 +180,13 @@ export default {
successTexts: { successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'), [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
}, },
watch: {
isEmpty(flag) {
if (flag) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
},
},
methods: { methods: {
handleBlobContentError(error = {}) { handleBlobContentError(error = {}) {
const { networkError } = error; const { networkError } = error;
...@@ -170,6 +198,7 @@ export default { ...@@ -170,6 +198,7 @@ export default {
response?.status === httpStatusCodes.NOT_FOUND || response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST response?.status === httpStatusCodes.BAD_REQUEST
) { ) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
this.showStartScreen = true; this.showStartScreen = true;
} else { } else {
this.reportFailure(LOAD_FAILURE_UNKNOWN); this.reportFailure(LOAD_FAILURE_UNKNOWN);
...@@ -183,6 +212,8 @@ export default { ...@@ -183,6 +212,8 @@ export default {
this.showSuccessAlert = false; this.showSuccessAlert = false;
}, },
reportFailure(type, reasons = []) { reportFailure(type, reasons = []) {
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
this.showFailureAlert = true; this.showFailureAlert = true;
this.failureType = type; this.failureType = type;
...@@ -196,6 +227,9 @@ export default { ...@@ -196,6 +227,9 @@ export default {
resetContent() { resetContent() {
this.currentCiFileContent = this.lastCommittedContent; this.currentCiFileContent = this.lastCommittedContent;
}, },
setAppStatus(appStatus) {
this.$apollo.getClient().writeQuery({ query: getAppStatus, data: { appStatus } });
},
setNewEmptyCiConfigFile() { setNewEmptyCiConfigFile() {
this.$apollo this.$apollo
.getClient() .getClient()
...@@ -242,7 +276,6 @@ export default { ...@@ -242,7 +276,6 @@ export default {
</ul> </ul>
</gl-alert> </gl-alert>
<pipeline-editor-home <pipeline-editor-home
:is-ci-config-data-loading="isCiConfigDataLoading"
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent" :ci-file-content="currentCiFileContent"
@commit="updateOnCommit" @commit="updateOnCommit"
......
...@@ -19,10 +19,6 @@ export default { ...@@ -19,10 +19,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -44,15 +40,10 @@ export default { ...@@ -44,15 +40,10 @@ export default {
<template> <template>
<div> <div>
<pipeline-editor-header <pipeline-editor-header :ci-config-data="ciConfigData" />
:ci-file-content="ciFileContent"
:ci-config-data="ciConfigData"
:is-ci-config-data-loading="isCiConfigDataLoading"
/>
<pipeline-editor-tabs <pipeline-editor-tabs
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
:ci-file-content="ciFileContent" :ci-file-content="ciFileContent"
:is-ci-config-data-loading="isCiConfigDataLoading"
v-on="$listeners" v-on="$listeners"
@set-current-tab="setCurrentTab" @set-current-tab="setCurrentTab"
/> />
......
...@@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants'; import { EDITOR_READY_EVENT } from '~/editor/constants';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { INVALID_CI_CONFIG } from '~/pipelines/constants'; import { INVALID_CI_CONFIG } from '~/pipelines/constants';
import { mockLintResponse, mockCiConfigPath } from '../../mock_data'; import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
...@@ -39,12 +38,11 @@ describe('Text editor component', () => { ...@@ -39,12 +38,11 @@ describe('Text editor component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('when status is invalid', () => { describe('when status is invalid', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { ciConfigData: { status: CI_CONFIG_STATUS_INVALID } } }); createComponent({ props: { isValid: false } });
}); });
it('show an error message', () => { it('show an error message', () => {
...@@ -59,7 +57,7 @@ describe('Text editor component', () => { ...@@ -59,7 +57,7 @@ describe('Text editor component', () => {
describe('when status is valid', () => { describe('when status is valid', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ props: { isValid: true } });
}); });
it('shows an information message that the section is not editable', () => { it('shows an information message that the section is not editable', () => {
......
...@@ -6,13 +6,19 @@ import { sprintf } from '~/locale'; ...@@ -6,13 +6,19 @@ import { sprintf } from '~/locale';
import ValidationSegment, { import ValidationSegment, {
i18n, i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue'; } from '~/pipeline_editor/components/header/validation_segment.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data'; import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
describe('Validation segment component', () => { describe('Validation segment component', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = ({ props = {}, appStatus }) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(ValidationSegment, { shallowMount(ValidationSegment, {
provide: { provide: {
...@@ -21,9 +27,14 @@ describe('Validation segment component', () => { ...@@ -21,9 +27,14 @@ describe('Validation segment component', () => {
propsData: { propsData: {
ciConfig: mergeUnwrappedCiConfig(), ciConfig: mergeUnwrappedCiConfig(),
ciFileContent: mockCiYml, ciFileContent: mockCiYml,
loading: false,
...props, ...props,
}, },
// Simulate graphQL client query result
data() {
return {
appStatus,
};
},
}), }),
); );
}; };
...@@ -34,18 +45,17 @@ describe('Validation segment component', () => { ...@@ -34,18 +45,17 @@ describe('Validation segment component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('shows the loading state', () => { it('shows the loading state', () => {
createComponent({ loading: true }); createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
expect(wrapper.text()).toBe(i18n.loading); expect(wrapper.text()).toBe(i18n.loading);
}); });
describe('when config is empty', () => { describe('when config is empty', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ ciFileContent: '' }); createComponent({ appStatus: EDITOR_APP_STATUS_EMPTY });
}); });
it('has check icon', () => { it('has check icon', () => {
...@@ -59,7 +69,7 @@ describe('Validation segment component', () => { ...@@ -59,7 +69,7 @@ describe('Validation segment component', () => {
describe('when config is valid', () => { describe('when config is valid', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}); createComponent({ appStatus: EDITOR_APP_STATUS_VALID });
}); });
it('has check icon', () => { it('has check icon', () => {
...@@ -79,12 +89,9 @@ describe('Validation segment component', () => { ...@@ -79,12 +89,9 @@ describe('Validation segment component', () => {
describe('when config is invalid', () => { describe('when config is invalid', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
ciConfig: mergeUnwrappedCiConfig({ appStatus: EDITOR_APP_STATUS_INVALID,
status: CI_CONFIG_STATUS_INVALID,
}),
}); });
}); });
it('has warning icon', () => { it('has warning icon', () => {
expect(findIcon().props('name')).toBe('warning-solid'); expect(findIcon().props('name')).toBe('warning-solid');
}); });
...@@ -93,43 +100,53 @@ describe('Validation segment component', () => { ...@@ -93,43 +100,53 @@ describe('Validation segment component', () => {
expect(findValidationMsg().text()).toBe(i18n.invalid); expect(findValidationMsg().text()).toBe(i18n.invalid);
}); });
it('shows an invalid state with an error', () => { it('shows the learn more link', () => {
expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
expect(findLearnMoreLink().text()).toBe('Learn more');
});
describe('with multiple errors', () => {
const firstError = 'First Error'; const firstError = 'First Error';
const secondError = 'Second Error'; const secondError = 'Second Error';
createComponent({ beforeEach(() => {
ciConfig: mergeUnwrappedCiConfig({ createComponent({
status: CI_CONFIG_STATUS_INVALID, props: {
errors: [firstError, secondError], ciConfig: mergeUnwrappedCiConfig({
}), status: CI_CONFIG_STATUS_INVALID,
errors: [firstError, secondError],
}),
},
});
});
it('shows an invalid state with an error', () => {
// Test the error is shown _and_ the string matches
expect(findValidationMsg().text()).toContain(firstError);
expect(findValidationMsg().text()).toBe(
sprintf(i18n.invalidWithReason, { reason: firstError }),
);
}); });
// Test the error is shown _and_ the string matches
expect(findValidationMsg().text()).toContain(firstError);
expect(findValidationMsg().text()).toBe(
sprintf(i18n.invalidWithReason, { reason: firstError }),
);
}); });
it('shows an invalid state with an error while preventing XSS', () => { describe('with XSS inside the error', () => {
const evilError = '<script>evil();</script>'; const evilError = '<script>evil();</script>';
createComponent({ beforeEach(() => {
ciConfig: mergeUnwrappedCiConfig({ createComponent({
status: CI_CONFIG_STATUS_INVALID, props: {
errors: [evilError], ciConfig: mergeUnwrappedCiConfig({
}), status: CI_CONFIG_STATUS_INVALID,
errors: [evilError],
}),
},
});
}); });
it('shows an invalid state with an error while preventing XSS', () => {
const { innerHTML } = findValidationMsg().element;
const { innerHTML } = findValidationMsg().element; expect(innerHTML).not.toContain(evilError);
expect(innerHTML).toContain(escape(evilError));
expect(innerHTML).not.toContain(evilError); });
expect(innerHTML).toContain(escape(evilError));
});
it('shows the learn more link', () => {
expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
expect(findLearnMoreLink().text()).toBe('Learn more');
}); });
}); });
}); });
...@@ -7,7 +7,7 @@ import { mockJobs, mockErrors, mockWarnings } from '../../mock_data'; ...@@ -7,7 +7,7 @@ import { mockJobs, mockErrors, mockWarnings } from '../../mock_data';
describe('CI Lint Results', () => { describe('CI Lint Results', () => {
let wrapper; let wrapper;
const defaultProps = { const defaultProps = {
valid: true, isValid: true,
jobs: mockJobs, jobs: mockJobs,
errors: [], errors: [],
warnings: [], warnings: [],
...@@ -42,7 +42,6 @@ describe('CI Lint Results', () => { ...@@ -42,7 +42,6 @@ describe('CI Lint Results', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('Empty results', () => { describe('Empty results', () => {
...@@ -72,7 +71,7 @@ describe('CI Lint Results', () => { ...@@ -72,7 +71,7 @@ describe('CI Lint Results', () => {
describe('Invalid results', () => { describe('Invalid results', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ valid: false, errors: mockErrors, warnings: mockWarnings }, mount); createComponent({ isValid: false, errors: mockErrors, warnings: mockWarnings }, mount);
}); });
it('does not display the table', () => { it('does not display the table', () => {
......
import { GlAlert, GlLink } from '@gitlab/ui'; import { GlAlert, GlLink } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data'; import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data';
describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
let wrapper; let wrapper;
const createComponent = (props = {}, mountFn = shallowMount) => { const createComponent = ({ props, mountFn = shallowMount } = {}) => {
wrapper = mountFn(CiLint, { wrapper = mountFn(CiLint, {
provide: { provide: {
lintHelpPagePath: mockLintHelpPagePath, lintHelpPagePath: mockLintHelpPagePath,
...@@ -27,12 +26,11 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { ...@@ -27,12 +26,11 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('Valid Results', () => { describe('Valid Results', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}, mount); createComponent({ props: { isValid: true }, mountFn: mount });
}); });
it('displays valid results', () => { it('displays valid results', () => {
...@@ -66,14 +64,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { ...@@ -66,14 +64,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
}); });
it('displays invalid results', () => { it('displays invalid results', () => {
createComponent( createComponent({ props: { isValid: false }, mountFn: mount });
{
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
}),
},
mount,
);
expect(findAlert().text()).toMatch('Status: Syntax is incorrect.'); expect(findAlert().text()).toMatch('Status: Syntax is incorrect.');
}); });
......
...@@ -4,8 +4,12 @@ import { nextTick } from 'vue'; ...@@ -4,8 +4,12 @@ import { nextTick } from 'vue';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import {
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockCiYml } from '../mock_data'; import { mockLintResponse, mockCiYml } from '../mock_data';
describe('Pipeline editor tabs component', () => { describe('Pipeline editor tabs component', () => {
...@@ -20,14 +24,23 @@ describe('Pipeline editor tabs component', () => { ...@@ -20,14 +24,23 @@ describe('Pipeline editor tabs component', () => {
}, },
}; };
const createComponent = ({ props = {}, provide = {}, mountFn = shallowMount } = {}) => { const createComponent = ({
props = {},
provide = {},
appStatus = EDITOR_APP_STATUS_VALID,
mountFn = shallowMount,
} = {}) => {
wrapper = mountFn(PipelineEditorTabs, { wrapper = mountFn(PipelineEditorTabs, {
propsData: { propsData: {
ciConfigData: mockLintResponse, ciConfigData: mockLintResponse,
ciFileContent: mockCiYml, ciFileContent: mockCiYml,
isCiConfigDataLoading: false,
...props, ...props,
}, },
data() {
return {
appStatus,
};
},
provide: { ...mockProvide, ...provide }, provide: { ...mockProvide, ...provide },
stubs: { stubs: {
TextEditor: MockTextEditor, TextEditor: MockTextEditor,
...@@ -49,7 +62,6 @@ describe('Pipeline editor tabs component', () => { ...@@ -49,7 +62,6 @@ describe('Pipeline editor tabs component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('editor tab', () => { describe('editor tab', () => {
...@@ -69,7 +81,7 @@ describe('Pipeline editor tabs component', () => { ...@@ -69,7 +81,7 @@ describe('Pipeline editor tabs component', () => {
describe('with feature flag on', () => { describe('with feature flag on', () => {
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { isCiConfigDataLoading: true } }); createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
}); });
it('displays a loading icon if the lint query is loading', () => { it('displays a loading icon if the lint query is loading', () => {
...@@ -108,7 +120,7 @@ describe('Pipeline editor tabs component', () => { ...@@ -108,7 +120,7 @@ describe('Pipeline editor tabs component', () => {
describe('lint tab', () => { describe('lint tab', () => {
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { isCiConfigDataLoading: true } }); createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
}); });
it('displays a loading icon if the lint query is loading', () => { it('displays a loading icon if the lint query is loading', () => {
...@@ -135,7 +147,7 @@ describe('Pipeline editor tabs component', () => { ...@@ -135,7 +147,7 @@ describe('Pipeline editor tabs component', () => {
describe('with feature flag on', () => { describe('with feature flag on', () => {
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { isCiConfigDataLoading: true } }); createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
}); });
it('displays a loading icon if the lint query is loading', () => { it('displays a loading icon if the lint query is loading', () => {
...@@ -143,9 +155,9 @@ describe('Pipeline editor tabs component', () => { ...@@ -143,9 +155,9 @@ describe('Pipeline editor tabs component', () => {
}); });
}); });
describe('when `mergedYaml` is undefined', () => { describe('when there is a fetch error', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { ciConfigData: {} } }); createComponent({ appStatus: EDITOR_APP_STATUS_ERROR });
}); });
it('show an error message', () => { it('show an error message', () => {
......
...@@ -72,7 +72,7 @@ describe('Pipeline editor app component', () => { ...@@ -72,7 +72,7 @@ describe('Pipeline editor app component', () => {
}); });
}; };
const createComponentWithApollo = ({ props = {}, provide = {} } = {}) => { const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]]; const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = { const resolvers = {
Query: { Query: {
...@@ -94,6 +94,8 @@ describe('Pipeline editor app component', () => { ...@@ -94,6 +94,8 @@ describe('Pipeline editor app component', () => {
}; };
createComponent({ props, provide, options }); createComponent({ props, provide, options });
return waitForPromises();
}; };
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
...@@ -116,11 +118,13 @@ describe('Pipeline editor app component', () => { ...@@ -116,11 +118,13 @@ describe('Pipeline editor app component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('displays a loading icon if the blob query is loading', () => { describe('loading state', () => {
createComponent({ blobLoading: true }); it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false); expect(findTextEditor().exists()).toBe(false);
});
}); });
describe('when queries are called', () => { describe('when queries are called', () => {
...@@ -131,9 +135,7 @@ describe('Pipeline editor app component', () => { ...@@ -131,9 +135,7 @@ describe('Pipeline editor app component', () => {
describe('when file exists', () => { describe('when file exists', () => {
beforeEach(async () => { beforeEach(async () => {
createComponentWithApollo(); await createComponentWithApollo();
await waitForPromises();
}); });
it('shows pipeline editor home component', () => { it('shows pipeline editor home component', () => {
...@@ -145,10 +147,6 @@ describe('Pipeline editor app component', () => { ...@@ -145,10 +147,6 @@ describe('Pipeline editor app component', () => {
}); });
it('ci config query is called with correct variables', async () => { it('ci config query is called with correct variables', async () => {
createComponentWithApollo();
await waitForPromises();
expect(mockCiConfigData).toHaveBeenCalledWith({ expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml, content: mockCiYml,
projectPath: mockProjectFullPath, projectPath: mockProjectFullPath,
...@@ -164,9 +162,7 @@ describe('Pipeline editor app component', () => { ...@@ -164,9 +162,7 @@ describe('Pipeline editor app component', () => {
status: httpStatusCodes.BAD_REQUEST, status: httpStatusCodes.BAD_REQUEST,
}, },
}); });
createComponentWithApollo(); await createComponentWithApollo();
await waitForPromises();
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
...@@ -181,9 +177,7 @@ describe('Pipeline editor app component', () => { ...@@ -181,9 +177,7 @@ describe('Pipeline editor app component', () => {
status: httpStatusCodes.NOT_FOUND, status: httpStatusCodes.NOT_FOUND,
}, },
}); });
createComponentWithApollo(); await createComponentWithApollo();
await waitForPromises();
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
...@@ -194,8 +188,7 @@ describe('Pipeline editor app component', () => { ...@@ -194,8 +188,7 @@ describe('Pipeline editor app component', () => {
describe('because of a fetching error', () => { describe('because of a fetching error', () => {
it('shows a unkown error message', async () => { it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo(); await createComponentWithApollo();
await waitForPromises();
expect(findEmptyState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(false);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]); expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
...@@ -212,7 +205,7 @@ describe('Pipeline editor app component', () => { ...@@ -212,7 +205,7 @@ describe('Pipeline editor app component', () => {
}, },
}); });
createComponentWithApollo({ await createComponentWithApollo({
provide: { provide: {
glFeatures: { glFeatures: {
pipelineEditorEmptyStateAction: true, pipelineEditorEmptyStateAction: true,
...@@ -220,8 +213,6 @@ describe('Pipeline editor app component', () => { ...@@ -220,8 +213,6 @@ describe('Pipeline editor app component', () => {
}, },
}); });
await waitForPromises();
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false); expect(findTextEditor().exists()).toBe(false);
...@@ -254,9 +245,9 @@ describe('Pipeline editor app component', () => { ...@@ -254,9 +245,9 @@ describe('Pipeline editor app component', () => {
describe('and the commit mutation fails', () => { describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed']; const commitFailedReasons = ['Commit failed'];
beforeEach(() => { beforeEach(async () => {
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
createComponent(); await createComponentWithApollo();
findEditorHome().vm.$emit('showError', { findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE, type: COMMIT_FAILURE,
...@@ -278,9 +269,9 @@ describe('Pipeline editor app component', () => { ...@@ -278,9 +269,9 @@ describe('Pipeline editor app component', () => {
describe('when an unknown error occurs', () => { describe('when an unknown error occurs', () => {
const unknownReasons = ['Commit failed']; const unknownReasons = ['Commit failed'];
beforeEach(() => { beforeEach(async () => {
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
createComponent(); await createComponentWithApollo();
findEditorHome().vm.$emit('showError', { findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE, type: COMMIT_FAILURE,
......
...@@ -59,7 +59,10 @@ describe('pipeline graph component', () => { ...@@ -59,7 +59,10 @@ describe('pipeline graph component', () => {
describe('with `VALID` status', () => { describe('with `VALID` status', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
pipelineData: { status: CI_CONFIG_STATUS_VALID, stages: [{ name: 'hello', groups: [] }] }, pipelineData: {
status: CI_CONFIG_STATUS_VALID,
stages: [{ name: 'hello', groups: [] }],
},
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment