Commit 469fc9e1 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

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

Add empty state inside pipeline editor section

See merge request gitlab-org/gitlab!55227
parents 83af89ed c3deeb50
<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'; ...@@ -5,7 +5,6 @@ export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS'; export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; 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 LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const CREATE_TAB = 'CREATE_TAB'; export const CREATE_TAB = 'CREATE_TAB';
......
...@@ -25,6 +25,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -25,6 +25,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
// Add to provide/inject API for static values // Add to provide/inject API for static values
ciConfigPath, ciConfigPath,
defaultBranch, defaultBranch,
emptyStateIllustrationPath,
lintHelpPagePath, lintHelpPagePath,
newMergeRequestPath, newMergeRequestPath,
projectFullPath, projectFullPath,
...@@ -51,6 +52,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -51,6 +52,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
provide: { provide: {
ciConfigPath, ciConfigPath,
defaultBranch, defaultBranch,
emptyStateIllustrationPath,
lintHelpPagePath, lintHelpPagePath,
newMergeRequestPath, newMergeRequestPath,
projectFullPath, projectFullPath,
......
<script> <script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale'; 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 { import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
COMMIT_FAILURE, import { COMMIT_FAILURE, COMMIT_SUCCESS, DEFAULT_FAILURE, LOAD_FAILURE_UNKNOWN } from './constants';
COMMIT_SUCCESS,
DEFAULT_FAILURE,
LOAD_FAILURE_NO_FILE,
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 PipelineEditorHome from './pipeline_editor_home.vue'; import PipelineEditorHome from './pipeline_editor_home.vue';
...@@ -21,6 +16,7 @@ export default { ...@@ -21,6 +16,7 @@ export default {
ConfirmUnsavedChangesDialog, ConfirmUnsavedChangesDialog,
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
PipelineEditorEmptyState,
PipelineEditorHome, PipelineEditorHome,
}, },
inject: { inject: {
...@@ -40,6 +36,7 @@ export default { ...@@ -40,6 +36,7 @@ export default {
// Success and failure state // Success and failure state
failureType: null, failureType: null,
failureReasons: [], failureReasons: [],
hasNoCiConfigFile: false,
initialCiFileContent: '', initialCiFileContent: '',
lastCommittedContent: '', lastCommittedContent: '',
currentCiFileContent: '', currentCiFileContent: '',
...@@ -102,21 +99,11 @@ export default { ...@@ -102,21 +99,11 @@ export default {
isBlobContentLoading() { isBlobContentLoading() {
return this.$apollo.queries.initialCiFileContent.loading; return this.$apollo.queries.initialCiFileContent.loading;
}, },
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE;
},
isCiConfigDataLoading() { isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading; return this.$apollo.queries.ciConfigData.loading;
}, },
failure() { failure() {
switch (this.failureType) { 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: case LOAD_FAILURE_UNKNOWN:
return { return {
text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN], text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
...@@ -154,9 +141,6 @@ export default { ...@@ -154,9 +141,6 @@ export default {
errorTexts: { errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'), [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.'), [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
}, },
successTexts: { successTexts: {
...@@ -173,7 +157,7 @@ export default { ...@@ -173,7 +157,7 @@ export default {
response?.status === httpStatusCodes.NOT_FOUND || response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST response?.status === httpStatusCodes.BAD_REQUEST
) { ) {
this.reportFailure(LOAD_FAILURE_NO_FILE); this.hasNoCiConfigFile = true;
} else { } else {
this.reportFailure(LOAD_FAILURE_UNKNOWN); this.reportFailure(LOAD_FAILURE_UNKNOWN);
} }
...@@ -216,18 +200,19 @@ export default { ...@@ -216,18 +200,19 @@ export default {
</script> </script>
<template> <template>
<div class="gl-mt-4"> <div class="gl-mt-4 gl-relative">
<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>
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" /> <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 <pipeline-editor-home
:is-ci-config-data-loading="isCiConfigDataLoading" :is-ci-config-data-loading="isCiConfigDataLoading"
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
...@@ -237,7 +222,7 @@ export default { ...@@ -237,7 +222,7 @@ export default {
@showError="showErrorAlert" @showError="showErrorAlert"
@updateCiConfig="updateCiConfig" @updateCiConfig="updateCiConfig"
/> />
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div> </div>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div> </div>
</template> </template>
- page_title s_('Pipelines|Pipeline Editor') - page_title s_('Pipelines|Pipeline Editor')
#js-pipeline-editor{ data: { "ci-config-path": @project.ci_config_path_or_default, #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-path" => @project.path,
"project-full-path" => @project.full_path, "project-full-path" => @project.full_path,
"project-namespace" => @project.namespace.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'), "yml-help-page-path" => help_page_path('ci/yaml/README'),
} } } }
---
title: Add empty state to pipeline editor section
merge_request: 55227
author:
type: changed
...@@ -8560,6 +8560,9 @@ msgstr "" ...@@ -8560,6 +8560,9 @@ msgstr ""
msgid "Create a merge request" msgid "Create a merge request"
msgstr "" 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" msgid "Create a new branch"
msgstr "" msgstr ""
...@@ -21229,6 +21232,9 @@ msgstr "" ...@@ -21229,6 +21232,9 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses." msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr "" msgstr ""
msgid "Optimize your workflow with CI/CD Pipelines"
msgstr ""
msgid "Optional" msgid "Optional"
msgstr "" msgstr ""
...@@ -22102,9 +22108,6 @@ msgstr "" ...@@ -22102,9 +22108,6 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines." msgid "Pipelines|There are currently no pipelines."
msgstr "" 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." msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr "" 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'; ...@@ -7,6 +7,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.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 { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
...@@ -92,6 +93,7 @@ describe('Pipeline editor app component', () => { ...@@ -92,6 +93,7 @@ describe('Pipeline editor app component', () => {
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome); const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor); const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
beforeEach(() => { beforeEach(() => {
mockBlobContentData = jest.fn(); mockBlobContentData = jest.fn();
...@@ -146,45 +148,51 @@ describe('Pipeline editor app component', () => { ...@@ -146,45 +148,51 @@ describe('Pipeline editor app component', () => {
}); });
}); });
describe('when no file exists', () => { describe('when no CI config file exists', () => {
const noFileAlertMsg = describe('in a project without a repository', () => {
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.'; it('shows an empty state and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
it('shows a 404 error message and does not show editor home component', async () => { response: {
mockBlobContentData.mockRejectedValueOnce({ status: httpStatusCodes.BAD_REQUEST,
response: { },
status: httpStatusCodes.NOT_FOUND, });
}, createComponentWithApollo();
});
createComponentWithApollo();
await waitForPromises(); await waitForPromises();
expect(findAlert().text()).toBe(noFileAlertMsg); expect(findEmptyState().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
}); });
it('shows a 400 error message and does not show editor home component', async () => { describe('in a project with a repository', () => {
mockBlobContentData.mockRejectedValueOnce({ it('shows an empty state and does not show editor home component', async () => {
response: { mockBlobContentData.mockRejectedValueOnce({
status: httpStatusCodes.BAD_REQUEST, response: {
}, status: httpStatusCodes.NOT_FOUND,
}); },
createComponentWithApollo(); });
createComponentWithApollo();
await waitForPromises(); await waitForPromises();
expect(findAlert().text()).toBe(noFileAlertMsg); expect(findEmptyState().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
}); });
it('shows a unkown error message', async () => { describe('because of a fetching error', () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); it('shows a unkown error message', async () => {
createComponentWithApollo(); mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
await waitForPromises(); createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]); expect(findEmptyState().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(true); 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