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

Add empty state inside pipeline editor section

This adds an empty section with no CTA inside the pipeline
editor if there is no file to edit. At a later date, we
are going to add the option to start working even without an
existing file.
parent 15e5c966
<script>
import { GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlSprintf,
},
i18n: {
title: __('Optimize your workflow with CI/CD Pipelines'),
body: __(
'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.',
),
},
inject: {
emptyStateIllustrationPath: {
default: '',
},
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
<img :src="emptyStateIllustrationPath" />
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
<p>
<gl-sprintf :message="$options.i18n.body">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</div>
</template>
......@@ -5,7 +5,6 @@ export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const CREATE_TAB = 'CREATE_TAB';
......
......@@ -25,6 +25,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
// Add to provide/inject API for static values
ciConfigPath,
defaultBranch,
emptyStateIllustrationPath,
lintHelpPagePath,
newMergeRequestPath,
projectFullPath,
......@@ -51,6 +52,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
provide: {
ciConfigPath,
defaultBranch,
emptyStateIllustrationPath,
lintHelpPagePath,
newMergeRequestPath,
projectFullPath,
......
<script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
LOAD_FAILURE_NO_FILE,
LOAD_FAILURE_UNKNOWN,
} from './constants';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_FAILURE, COMMIT_SUCCESS, DEFAULT_FAILURE, LOAD_FAILURE_UNKNOWN } from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
......@@ -21,6 +16,7 @@ export default {
ConfirmUnsavedChangesDialog,
GlAlert,
GlLoadingIcon,
PipelineEditorEmptyState,
PipelineEditorHome,
},
inject: {
......@@ -40,6 +36,7 @@ export default {
// Success and failure state
failureType: null,
failureReasons: [],
hasNoCiConfigFile: false,
initialCiFileContent: '',
lastCommittedContent: '',
currentCiFileContent: '',
......@@ -102,21 +99,11 @@ export default {
isBlobContentLoading() {
return this.$apollo.queries.initialCiFileContent.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE;
},
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_FILE:
return {
text: sprintf(this.$options.errorTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
return {
text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
......@@ -154,9 +141,6 @@ export default {
errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
successTexts: {
......@@ -173,7 +157,7 @@ export default {
response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST
) {
this.reportFailure(LOAD_FAILURE_NO_FILE);
this.hasNoCiConfigFile = true;
} else {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
......@@ -216,18 +200,19 @@ export default {
</script>
<template>
<div class="gl-mt-4">
<gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
</gl-alert>
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="dismissFailure">
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
</ul>
</gl-alert>
<div class="gl-mt-4 gl-relative">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
<pipeline-editor-empty-state v-else-if="hasNoCiConfigFile" />
<div v-else>
<gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
</gl-alert>
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="dismissFailure">
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
</ul>
</gl-alert>
<pipeline-editor-home
:is-ci-config-data-loading="isCiConfigDataLoading"
:ci-config-data="ciConfigData"
......@@ -237,7 +222,7 @@ export default {
@showError="showErrorAlert"
@updateCiConfig="updateCiConfig"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</template>
- page_title s_('Pipelines|Pipeline Editor')
#js-pipeline-editor{ data: { "ci-config-path": @project.ci_config_path_or_default,
"commit-sha" => @project.commit ? @project.commit.sha : '',
"default-branch" => @project.default_branch,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
"project-path" => @project.path,
"project-full-path" => @project.full_path,
"project-namespace" => @project.namespace.full_path,
"default-branch" => @project.default_branch,
"commit-sha" => @project.commit ? @project.commit.sha : '',
"new-merge-request-path" => namespace_project_new_merge_request_path,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"yml-help-page-path" => help_page_path('ci/yaml/README'),
} }
---
title: Add empty state to pipeline editor section
merge_request: 55227
author:
type: changed
......@@ -8551,6 +8551,9 @@ msgstr ""
msgid "Create a merge request"
msgstr ""
msgid "Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started."
msgstr ""
msgid "Create a new branch"
msgstr ""
......@@ -21220,6 +21223,9 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr ""
msgid "Optimize your workflow with CI/CD Pipelines"
msgstr ""
msgid "Optional"
msgstr ""
......@@ -22093,9 +22099,6 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again."
msgstr ""
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr ""
......
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
describe('Pipeline editor empty state', () => {
let wrapper;
const defaultProvide = {
emptyStateIllustrationPath: 'my/svg/path',
};
const createComponent = () => {
wrapper = shallowMount(PipelineEditorEmptyState, {
provide: defaultProvide,
});
};
const findSvgImage = () => wrapper.find('img');
const findTitle = () => wrapper.find('h1');
const findDescription = () => wrapper.findComponent(GlSprintf);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders an svg image', () => {
expect(findSvgImage().exists()).toBe(true);
});
it('renders a title', () => {
expect(findTitle().exists()).toBe(true);
expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
});
it('renders a description', () => {
expect(findDescription().exists()).toBe(true);
expect(findDescription().html()).toContain(wrapper.vm.$options.i18n.body);
});
});
......@@ -7,6 +7,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
......@@ -92,6 +93,7 @@ describe('Pipeline editor app component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
beforeEach(() => {
mockBlobContentData = jest.fn();
......@@ -146,45 +148,51 @@ describe('Pipeline editor app component', () => {
});
});
describe('when no file exists', () => {
const noFileAlertMsg =
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
it('shows a 404 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
createComponentWithApollo();
describe('when no CI config file exists', () => {
describe('in a project without a repository', () => {
it('shows an empty state and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
},
});
createComponentWithApollo();
await waitForPromises();
await waitForPromises();
expect(findAlert().text()).toBe(noFileAlertMsg);
expect(findEditorHome().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
});
it('shows a 400 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
},
});
createComponentWithApollo();
describe('in a project with a repository', () => {
it('shows an empty state and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
createComponentWithApollo();
await waitForPromises();
await waitForPromises();
expect(findAlert().text()).toBe(noFileAlertMsg);
expect(findEditorHome().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
});
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
expect(findEditorHome().exists()).toBe(true);
expect(findEmptyState().exists()).toBe(false);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
expect(findEditorHome().exists()).toBe(true);
});
});
});
......
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