Commit dd287fad authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Sarah Groff Hennigh-Palermo

Adds a global state to the pipeline editor section

Instead of relying on passing props data down to know
all about what is happening on a global level, we
write to apollo cache the global state of the app
that can then be consumed byy its children at any level
without needing extra layer of complexity.
parent fb9c1c99
...@@ -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) {
case EDITOR_APP_STATUS_EMPTY:
return 'check'; return 'check';
} case EDITOR_APP_STATUS_VALID:
return 'check';
default:
return 'warning-solid'; return 'warning-solid';
}
}, },
message() { message() {
if (this.isEmpty) { const [reason] = this.ciConfig?.errors || [];
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return this.$options.i18n.empty; return this.$options.i18n.empty;
} else if (this.isValid) { case EDITOR_APP_STATUS_VALID:
return this.$options.i18n.valid; return this.$options.i18n.valid;
} default:
// Only display first error as a reason // Only display first error as a reason
const [reason] = this.ciConfig?.errors || []; return this.ciConfig?.errors.length > 0
if (reason) { ? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
return 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';
beforeEach(() => {
createComponent({ createComponent({
props: {
ciConfig: mergeUnwrappedCiConfig({ ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID, status: CI_CONFIG_STATUS_INVALID,
errors: [firstError, secondError], errors: [firstError, secondError],
}), }),
},
}); });
});
it('shows an invalid state with an error', () => {
// Test the error is shown _and_ the string matches // Test the error is shown _and_ the string matches
expect(findValidationMsg().text()).toContain(firstError); expect(findValidationMsg().text()).toContain(firstError);
expect(findValidationMsg().text()).toBe( expect(findValidationMsg().text()).toBe(
sprintf(i18n.invalidWithReason, { reason: firstError }), 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>';
beforeEach(() => {
createComponent({ createComponent({
props: {
ciConfig: mergeUnwrappedCiConfig({ ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID, status: CI_CONFIG_STATUS_INVALID,
errors: [evilError], 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).not.toContain(evilError);
expect(innerHTML).toContain(escape(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,12 +118,14 @@ describe('Pipeline editor app component', () => { ...@@ -116,12 +118,14 @@ describe('Pipeline editor app component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => { it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true }); 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', () => {
beforeEach(() => { beforeEach(() => {
...@@ -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