Commit 982ed637 authored by Phil Hughes's avatar Phil Hughes

Merge branch '217648-add-no-graph-empty-state-for-dag' into 'master'

Resolve "Add no graph empty state for DAG"

See merge request gitlab-org/gitlab!35053
parents e827638f 85ba7b45
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
......@@ -25,6 +25,8 @@ export default {
GlAlert,
GlLink,
GlSprintf,
GlEmptyState,
GlButton,
},
props: {
graphUrl: {
......@@ -32,6 +34,16 @@ export default {
required: false,
default: '',
},
emptySvgPath: {
type: String,
required: true,
default: '',
},
dagDocPath: {
type: String,
required: true,
default: '',
},
},
data() {
return {
......@@ -40,6 +52,7 @@ export default {
graphData: null,
showFailureAlert: false,
showBetaInfo: true,
hasNoDependentJobs: false,
};
},
errorTexts: {
......@@ -48,6 +61,16 @@ export default {
[UNSUPPORTED_DATA]: __('DAG visualization requires at least 3 dependent jobs.'),
[DEFAULT]: __('An unknown error occurred while loading this graph.'),
},
emptyStateTexts: {
title: __('Start using Directed Acyclic Graphs (DAG)'),
firstDescription: __(
"This pipeline does not use the %{codeStart}needs%{codeEnd} keyword and can't be represented as a directed acyclic graph.",
),
secondDescription: __(
'Using %{codeStart}needs%{codeEnd} allows jobs to run before their stage is reached, as soon as their individual dependencies are met, which speeds up your pipelines.',
),
button: __('Learn more about job dependencies'),
},
computed: {
betaMessage() {
return __(
......@@ -114,11 +137,18 @@ export default {
return;
}
if (parsed.links.length < 2) {
if (parsed.links.length === 1) {
this.reportFailure(UNSUPPORTED_DATA);
return;
}
// If there are no links, we don't report failure
// as it simply means the user does not use job dependencies
if (parsed.links.length === 0) {
this.hasNoDependentJobs = true;
return;
}
this.graphData = parsed;
},
hideAlert() {
......@@ -172,9 +202,38 @@ export default {
<dag-graph
v-if="shouldDisplayGraph"
:graph-data="graphData"
@on-failure="reportFailure"
@onFailure="reportFailure"
@update-annotation="updateAnnotation"
/>
<gl-empty-state
v-else-if="hasNoDependentJobs"
:svg-path="emptySvgPath"
:title="$options.emptyStateTexts.title"
>
<template #description>
<div class="gl-text-left">
<p>
<gl-sprintf :message="$options.emptyStateTexts.firstDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.emptyStateTexts.secondDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</div>
</template>
<template #actions>
<gl-button :href="dagDocPath" target="__blank" variant="success">
{{ $options.emptyStateTexts.button }}
</gl-button>
</template>
</gl-empty-state>
</div>
</div>
</template>
......@@ -151,7 +151,8 @@ const createDagApp = () => {
}
const el = document.querySelector('#js-pipeline-dag-vue');
const graphUrl = el?.dataset?.pipelineDataPath;
const { pipelineDataPath, emptySvgPath, dagDocPath } = el?.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
......@@ -161,7 +162,9 @@ const createDagApp = () => {
render(createElement) {
return createElement('dag', {
props: {
graphUrl,
graphUrl: pipelineDataPath,
emptySvgPath,
dagDocPath,
},
});
},
......
......@@ -83,7 +83,7 @@
- if dag_pipeline_tab_enabled
#js-tab-dag.tab-pane
#js-pipeline-dag-vue{ data: { pipeline_data_path: dag_project_pipeline_path(@project, @pipeline) } }
#js-pipeline-dag-vue{ data: { pipeline_data_path: dag_project_pipeline_path(@project, @pipeline), empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
#js-tab-tests.tab-pane
#js-pipeline-tests-detail
......
---
title: Resolve Add no graph empty state for DAG
merge_request: 35053
author:
type: changed
......@@ -13201,6 +13201,9 @@ msgstr ""
msgid "Learn more about group-level project templates"
msgstr ""
msgid "Learn more about job dependencies"
msgstr ""
msgid "Learn more about signing commits"
msgstr ""
......@@ -21646,6 +21649,9 @@ msgstr ""
msgid "Start thread & reopen %{noteable_name}"
msgstr ""
msgid "Start using Directed Acyclic Graphs (DAG)"
msgstr ""
msgid "Start your Free Gold Trial"
msgstr ""
......@@ -23395,6 +23401,9 @@ msgstr ""
msgid "This page will be removed in a future release."
msgstr ""
msgid "This pipeline does not use the %{codeStart}needs%{codeEnd} keyword and can't be represented as a directed acyclic graph."
msgstr ""
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}"
msgstr ""
......@@ -25169,6 +25178,9 @@ msgstr ""
msgid "UsersSelect|Unassigned"
msgstr ""
msgid "Using %{codeStart}needs%{codeEnd} allows jobs to run before their stage is reached, as soon as their individual dependencies are met, which speeds up your pipelines."
msgstr ""
msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}"
msgstr ""
......
......@@ -2,7 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
import { GlAlert } from '@gitlab/ui';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import Dag from '~/pipelines/components/dag/dag.vue';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
......@@ -16,7 +16,14 @@ import {
LOAD_FAILURE,
UNSUPPORTED_DATA,
} from '~/pipelines/components/dag//constants';
import { mockBaseData, tooSmallGraph, unparseableGraph, singleNote, multiNote } from './mock_data';
import {
mockBaseData,
tooSmallGraph,
unparseableGraph,
graphWithoutDependencies,
singleNote,
multiNote,
} from './mock_data';
describe('Pipeline DAG graph wrapper', () => {
let wrapper;
......@@ -26,6 +33,7 @@ describe('Pipeline DAG graph wrapper', () => {
const getGraph = () => wrapper.find(DagGraph);
const getNotes = () => wrapper.find(DagAnnotations);
const getErrorText = type => wrapper.vm.$options.errorTexts[type];
const getEmptyState = () => wrapper.find(GlEmptyState);
const dataPath = '/root/test/pipelines/90/dag.json';
......@@ -35,7 +43,11 @@ describe('Pipeline DAG graph wrapper', () => {
}
wrapper = method(Dag, {
propsData,
propsData: {
emptySvgPath: '/my-svg',
dagDocPath: '/my-doc',
...propsData,
},
data() {
return {
showFailureAlert: false,
......@@ -64,6 +76,10 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getAlert().text()).toBe(getErrorText(DEFAULT));
expect(getGraph().exists()).toBe(false);
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('when there is a dataUrl', () => {
......@@ -83,6 +99,10 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getGraph().exists()).toBe(false);
});
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('the data fetch succeeds but the parse fails', () => {
......@@ -101,6 +121,10 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getGraph().exists()).toBe(false);
});
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('and the data fetch and parse succeeds', () => {
......@@ -119,6 +143,15 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getGraph().exists()).toBe(true);
});
});
it('does not render the empty state', () => {
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(getEmptyState().exists()).toBe(false);
});
});
});
describe('the data fetch and parse succeeds, but the resulting graph is too small', () => {
......@@ -137,6 +170,42 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getGraph().exists()).toBe(false);
});
});
it('does not show the empty dag graph state', () => {
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(getEmptyState().exists()).toBe(false);
});
});
});
describe('the data fetch and parse succeeds, but the resulting graph is empty', () => {
beforeEach(() => {
mock.onGet(dataPath).replyOnce(200, graphWithoutDependencies);
createComponent({ graphUrl: dataPath }, mount);
});
it('does not render an error alert or the graph', () => {
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(getAllAlerts().length).toBe(1);
expect(getAlert().text()).toContain('This feature is currently in beta.');
expect(getGraph().exists()).toBe(false);
});
});
it('shows the empty dag graph state', () => {
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(getEmptyState().exists()).toBe(true);
});
});
});
});
......
......@@ -83,6 +83,46 @@ export const tooSmallGraph = {
],
};
export const graphWithoutDependencies = {
stages: [
{
name: 'test',
groups: [
{
name: 'jest',
size: 2,
jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
},
{
name: 'rspec',
size: 1,
jobs: [{ name: 'rspec' }],
},
],
},
{
name: 'fixtures',
groups: [
{
name: 'frontend fixtures',
size: 1,
jobs: [{ name: 'frontend fixtures' }],
},
],
},
{
name: 'un-needed',
groups: [
{
name: 'un-needed',
size: 1,
jobs: [{ name: 'un-needed' }],
},
],
},
],
};
export const unparseableGraph = [
{
name: 'test',
......
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