Commit cad76c98 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Mark Florian

Improve error messages in the Dependency List

- Use info variant for documentation links
- Handle case where there are no dependencies: show an empty state with
a link to supported languages and framework information
- Update and add tests
parent 9d990969
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab, GlTabs, GlLink, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import DependenciesActions from './dependencies_actions.vue';
......@@ -8,6 +8,7 @@ import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vu
import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue';
import PaginatedDependenciesTable from './paginated_dependencies_table.vue';
import { DEPENDENCY_LIST_TYPES } from '../store/constants';
import { REPORT_STATUS } from '../store/modules/list/constants';
export default {
name: 'DependenciesApp',
......@@ -19,6 +20,7 @@ export default {
GlTab,
GlTabs,
GlLink,
GlButton,
DependencyListIncompleteAlert,
DependencyListJobFailedAlert,
Icon,
......@@ -37,6 +39,10 @@ export default {
type: String,
required: true,
},
supportDocumentationPath: {
type: String,
required: true,
},
},
data() {
return {
......@@ -52,6 +58,7 @@ export default {
'isJobNotSetUp',
'isJobFailed',
'isIncomplete',
'hasNoDependencies',
'reportInfo',
'totals',
]),
......@@ -77,6 +84,30 @@ export default {
return sprintf(body, { linkStart, linkEnd }, false);
},
showEmptyState() {
return this.isJobNotSetUp || this.hasNoDependencies;
},
emptyStateOptions() {
const map = {
[REPORT_STATUS.jobNotSetUp]: {
title: __('View dependency details for your project'),
description: __(
'The dependency list details information about the components used within your project.',
),
buttonLabel: __('Learn more about the dependency list'),
link: this.documentationPath,
},
[REPORT_STATUS.noDependencies]: {
title: __('Dependency List has no entries'),
description: __(
'It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project.',
),
buttonLabel: __('View supported languages and frameworks'),
link: this.supportDocumentationPath,
},
};
return map[this.reportInfo.status];
},
},
created() {
this.setDependenciesEndpoint(this.endpoint);
......@@ -104,15 +135,17 @@ export default {
<gl-loading-icon v-if="!isInitialized" size="md" class="mt-4" />
<gl-empty-state
v-else-if="isJobNotSetUp"
:title="__('View dependency details for your project')"
:description="
__('The dependency list details information about the components used within your project.')
"
v-else-if="showEmptyState"
:title="emptyStateOptions.title"
:description="emptyStateOptions.description"
:svg-path="emptyStateSvgPath"
:primary-button-link="documentationPath"
:primary-button-text="__('Learn more about the dependency list')"
/>
>
<template #actions>
<gl-button variant="info" :href="emptyStateOptions.link">
{{ emptyStateOptions.buttonLabel }}
</gl-button>
</template>
</gl-empty-state>
<section v-else>
<dependency-list-incomplete-alert
......@@ -133,15 +166,16 @@ export default {
target="_blank"
:href="documentationPath"
:aria-label="__('Dependencies help page link')"
><icon name="question"
/></gl-link>
>
<icon name="question" />
</gl-link>
</h2>
<p class="mb-0">
<span v-html="subHeadingText"></span>
<span v-if="generatedAtTimeAgo"
><span aria-hidden="true">&bull;</span>
<span class="text-secondary"> {{ generatedAtTimeAgo }}</span></span
>
<span v-if="generatedAtTimeAgo">
<span aria-hidden="true">&bull;</span>
<span class="text-secondary">{{ generatedAtTimeAgo }}</span>
</span>
</p>
</header>
......@@ -153,9 +187,9 @@ export default {
>
<template #title>
{{ listType.label }}
<gl-badge pill :data-qa-selector="qaCountSelector(listType.label)">{{
totals[listType.namespace]
}}</gl-badge>
<gl-badge pill :data-qa-selector="qaCountSelector(listType.label)">
{{ totals[listType.namespace] }}
</gl-badge>
</template>
<paginated-dependencies-table :namespace="listType.namespace" />
</gl-tab>
......
......@@ -6,7 +6,7 @@ import { addListType } from './store/utils';
export default () => {
const el = document.querySelector('#js-dependencies-app');
const { endpoint, emptyStateSvgPath, documentationPath } = el.dataset;
const { endpoint, emptyStateSvgPath, documentationPath, supportDocumentationPath } = el.dataset;
const store = createStore();
addListType(store, DEPENDENCY_LIST_TYPES.vulnerable);
......@@ -23,6 +23,7 @@ export default () => {
endpoint,
emptyStateSvgPath,
documentationPath,
supportDocumentationPath,
},
});
},
......
......@@ -7,6 +7,8 @@ export const generatedAtTimeAgo = ({ currentList }, getters) =>
export const isJobNotSetUp = ({ currentList }, getters) => getters[`${currentList}/isJobNotSetUp`];
export const isJobFailed = ({ currentList }, getters) => getters[`${currentList}/isJobFailed`];
export const isIncomplete = ({ currentList }, getters) => getters[`${currentList}/isIncomplete`];
export const hasNoDependencies = ({ currentList }, getters) =>
getters[`${currentList}/hasNoDependencies`];
export const totals = state =>
state.listTypes.reduce(
......
......@@ -5,8 +5,8 @@ export const generatedAtTimeAgo = ({ reportInfo: { generatedAt } }) =>
generatedAt ? getTimeago().format(generatedAt) : '';
export const isJobNotSetUp = state => state.reportInfo.status === REPORT_STATUS.jobNotSetUp;
export const isJobFailed = state =>
[REPORT_STATUS.jobFailed, REPORT_STATUS.noDependencies].includes(state.reportInfo.status);
export const isJobFailed = state => state.reportInfo.status === REPORT_STATUS.jobFailed;
export const isIncomplete = state => state.reportInfo.status === REPORT_STATUS.incomplete;
export const hasNoDependencies = state => state.reportInfo.status === REPORT_STATUS.noDependencies;
export const downloadEndpoint = ({ endpoint }) => endpoint;
- breadcrumb_title _('Dependency List')
- page_title _('Dependency List')
#js-dependencies-app{ data: { endpoint: project_dependencies_path(@project, format: :json), documentation_path: help_page_path('user/application_security/dependency_list/index'), empty_state_svg_path: image_path('illustrations/Dependency-list-empty-state.svg') } }
#js-dependencies-app{ data: { endpoint: project_dependencies_path(@project, format: :json),
documentation_path: help_page_path('user/application_security/dependency_list/index'),
support_documentation_path: help_page_path('user/application_security/dependency_scanning/index', anchor: 'supported-languages-and-package-managers'),
empty_state_svg_path: image_path('illustrations/Dependency-list-empty-state.svg') } }
---
title: Improve error messages in the Dependency List
merge_request: 25369
author:
type: changed
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DependenciesApp component on creation given the dependency list job has not yet run shows only the empty state 1`] = `
Object {
"compact": false,
"description": "The dependency list details information about the components used within your project.",
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
"secondaryButtonText": null,
"svgPath": "/bar.svg",
"title": "View dependency details for your project",
}
`;
exports[`DependenciesApp component on creation given there are no dependencies detected shows only the empty state 1`] = `
Object {
"compact": false,
"description": "It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project.",
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
"secondaryButtonText": null,
"svgPath": "/bar.svg",
"title": "Dependency List has no entries",
}
`;
......@@ -22,6 +22,7 @@ describe('DependenciesApp component', () => {
endpoint: '/foo',
emptyStateSvgPath: '/bar.svg',
documentationPath: TEST_HOST,
supportDocumentationPath: `${TEST_HOST}/dependency_scanning#supported-languages`,
};
const factory = (props = basicAppProps) => {
......@@ -87,6 +88,16 @@ describe('DependenciesApp component', () => {
store.state[allNamespace].reportInfo.status = REPORT_STATUS.incomplete;
};
const setStateNoDependencies = () => {
Object.assign(store.state[allNamespace], {
initialized: true,
isLoading: false,
dependencies: [],
});
store.state[allNamespace].pageInfo.total = 0;
store.state[allNamespace].reportInfo.status = REPORT_STATUS.noDependencies;
};
const findJobFailedAlert = () => wrapper.find(DependencyListJobFailedAlert);
const findIncompleteListAlert = () => wrapper.find(DependencyListIncompleteAlert);
const findDependenciesTables = () => wrapper.findAll(PaginatedDependenciesTable);
......@@ -104,6 +115,11 @@ describe('DependenciesApp component', () => {
expect(componentWrapper.props()).toEqual(expect.objectContaining(props));
};
const expectComponentPropsToMatchSnapshot = Component => {
const componentWrapper = wrapper.find(Component);
expect(componentWrapper.props()).toMatchSnapshot();
};
const expectNoDependenciesTables = () => expect(findDependenciesTables()).toHaveLength(0);
const expectNoHeader = () => expect(findHeader().exists()).toBe(false);
......@@ -149,6 +165,7 @@ describe('DependenciesApp component', () => {
it('shows only the empty state', () => {
expectComponentWithProps(GlEmptyState, { svgPath: basicAppProps.emptyStateSvgPath });
expectComponentPropsToMatchSnapshot(GlEmptyState);
expectNoHeader();
expectNoDependenciesTables();
});
......@@ -324,5 +341,18 @@ describe('DependenciesApp component', () => {
});
});
});
describe('given there are no dependencies detected', () => {
beforeEach(() => {
setStateNoDependencies();
});
it('shows only the empty state', () => {
expectComponentWithProps(GlEmptyState, { svgPath: basicAppProps.emptyStateSvgPath });
expectComponentPropsToMatchSnapshot(GlEmptyState);
expectNoHeader();
expectNoDependenciesTables();
});
});
});
});
......@@ -24,6 +24,7 @@ describe('Dependencies getters', () => {
${'isJobNotSetUp'}
${'isJobFailed'}
${'isIncomplete'}
${'hasNoDependencies'}
${'generatedAtTimeAgo'}
`('$getterName', ({ getterName }) => {
it(`delegates to the current list module's ${getterName} getter`, () => {
......
......@@ -5,14 +5,16 @@ import { getDateInPast } from '~/lib/utils/datetime_utility';
describe('Dependencies getters', () => {
describe.each`
getterName | reportStatus | outcome
${'isJobNotSetUp'} | ${REPORT_STATUS.jobNotSetUp} | ${true}
${'isJobNotSetUp'} | ${REPORT_STATUS.ok} | ${false}
${'isJobFailed'} | ${REPORT_STATUS.jobFailed} | ${true}
${'isJobFailed'} | ${REPORT_STATUS.noDependencies} | ${true}
${'isJobFailed'} | ${REPORT_STATUS.ok} | ${false}
${'isIncomplete'} | ${REPORT_STATUS.incomplete} | ${true}
${'isIncomplete'} | ${REPORT_STATUS.ok} | ${false}
getterName | reportStatus | outcome
${'isJobNotSetUp'} | ${REPORT_STATUS.jobNotSetUp} | ${true}
${'isJobNotSetUp'} | ${REPORT_STATUS.ok} | ${false}
${'isJobFailed'} | ${REPORT_STATUS.jobFailed} | ${true}
${'isJobFailed'} | ${REPORT_STATUS.noDependencies} | ${false}
${'isJobFailed'} | ${REPORT_STATUS.ok} | ${false}
${'hasNoDependencies'} | ${REPORT_STATUS.ok} | ${false}
${'hasNoDependencies'} | ${REPORT_STATUS.noDependencies} | ${true}
${'isIncomplete'} | ${REPORT_STATUS.incomplete} | ${true}
${'isIncomplete'} | ${REPORT_STATUS.ok} | ${false}
`('$getterName when report status is $reportStatus', ({ getterName, reportStatus, outcome }) => {
it(`returns ${outcome}`, () => {
expect(
......
......@@ -6303,6 +6303,9 @@ msgstr ""
msgid "Dependency List"
msgstr ""
msgid "Dependency List has no entries"
msgstr ""
msgid "Dependency Proxy"
msgstr ""
......@@ -10822,6 +10825,9 @@ msgstr ""
msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected."
msgstr ""
msgid "It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project."
msgstr ""
msgid "It's you"
msgstr ""
......@@ -21516,6 +21522,9 @@ msgstr ""
msgid "View replaced file @ "
msgstr ""
msgid "View supported languages and frameworks"
msgstr ""
msgid "View the documentation"
msgstr ""
......
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