Commit fdab40dd authored by Mark Florian's avatar Mark Florian

Merge branch '34088-deplist-page-improve-message-when-job-succeeds' into 'master'

Improve error messages in the Dependency List

See merge request gitlab-org/gitlab!25369
parents ada7968c cad76c98
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; 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 { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DependenciesActions from './dependencies_actions.vue'; import DependenciesActions from './dependencies_actions.vue';
...@@ -8,6 +8,7 @@ import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vu ...@@ -8,6 +8,7 @@ import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vu
import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue'; import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue';
import PaginatedDependenciesTable from './paginated_dependencies_table.vue'; import PaginatedDependenciesTable from './paginated_dependencies_table.vue';
import { DEPENDENCY_LIST_TYPES } from '../store/constants'; import { DEPENDENCY_LIST_TYPES } from '../store/constants';
import { REPORT_STATUS } from '../store/modules/list/constants';
export default { export default {
name: 'DependenciesApp', name: 'DependenciesApp',
...@@ -19,6 +20,7 @@ export default { ...@@ -19,6 +20,7 @@ export default {
GlTab, GlTab,
GlTabs, GlTabs,
GlLink, GlLink,
GlButton,
DependencyListIncompleteAlert, DependencyListIncompleteAlert,
DependencyListJobFailedAlert, DependencyListJobFailedAlert,
Icon, Icon,
...@@ -37,6 +39,10 @@ export default { ...@@ -37,6 +39,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
supportDocumentationPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -52,6 +58,7 @@ export default { ...@@ -52,6 +58,7 @@ export default {
'isJobNotSetUp', 'isJobNotSetUp',
'isJobFailed', 'isJobFailed',
'isIncomplete', 'isIncomplete',
'hasNoDependencies',
'reportInfo', 'reportInfo',
'totals', 'totals',
]), ]),
...@@ -77,6 +84,30 @@ export default { ...@@ -77,6 +84,30 @@ export default {
return sprintf(body, { linkStart, linkEnd }, false); 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() { created() {
this.setDependenciesEndpoint(this.endpoint); this.setDependenciesEndpoint(this.endpoint);
...@@ -104,15 +135,17 @@ export default { ...@@ -104,15 +135,17 @@ export default {
<gl-loading-icon v-if="!isInitialized" size="md" class="mt-4" /> <gl-loading-icon v-if="!isInitialized" size="md" class="mt-4" />
<gl-empty-state <gl-empty-state
v-else-if="isJobNotSetUp" v-else-if="showEmptyState"
:title="__('View dependency details for your project')" :title="emptyStateOptions.title"
:description=" :description="emptyStateOptions.description"
__('The dependency list details information about the components used within your project.')
"
:svg-path="emptyStateSvgPath" :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> <section v-else>
<dependency-list-incomplete-alert <dependency-list-incomplete-alert
...@@ -133,15 +166,16 @@ export default { ...@@ -133,15 +166,16 @@ export default {
target="_blank" target="_blank"
:href="documentationPath" :href="documentationPath"
:aria-label="__('Dependencies help page link')" :aria-label="__('Dependencies help page link')"
><icon name="question" >
/></gl-link> <icon name="question" />
</gl-link>
</h2> </h2>
<p class="mb-0"> <p class="mb-0">
<span v-html="subHeadingText"></span> <span v-html="subHeadingText"></span>
<span v-if="generatedAtTimeAgo" <span v-if="generatedAtTimeAgo">
><span aria-hidden="true">&bull;</span> <span aria-hidden="true">&bull;</span>
<span class="text-secondary"> {{ generatedAtTimeAgo }}</span></span <span class="text-secondary">{{ generatedAtTimeAgo }}</span>
> </span>
</p> </p>
</header> </header>
...@@ -153,9 +187,9 @@ export default { ...@@ -153,9 +187,9 @@ export default {
> >
<template #title> <template #title>
{{ listType.label }} {{ listType.label }}
<gl-badge pill :data-qa-selector="qaCountSelector(listType.label)">{{ <gl-badge pill :data-qa-selector="qaCountSelector(listType.label)">
totals[listType.namespace] {{ totals[listType.namespace] }}
}}</gl-badge> </gl-badge>
</template> </template>
<paginated-dependencies-table :namespace="listType.namespace" /> <paginated-dependencies-table :namespace="listType.namespace" />
</gl-tab> </gl-tab>
......
...@@ -6,7 +6,7 @@ import { addListType } from './store/utils'; ...@@ -6,7 +6,7 @@ import { addListType } from './store/utils';
export default () => { export default () => {
const el = document.querySelector('#js-dependencies-app'); const el = document.querySelector('#js-dependencies-app');
const { endpoint, emptyStateSvgPath, documentationPath } = el.dataset; const { endpoint, emptyStateSvgPath, documentationPath, supportDocumentationPath } = el.dataset;
const store = createStore(); const store = createStore();
addListType(store, DEPENDENCY_LIST_TYPES.vulnerable); addListType(store, DEPENDENCY_LIST_TYPES.vulnerable);
...@@ -23,6 +23,7 @@ export default () => { ...@@ -23,6 +23,7 @@ export default () => {
endpoint, endpoint,
emptyStateSvgPath, emptyStateSvgPath,
documentationPath, documentationPath,
supportDocumentationPath,
}, },
}); });
}, },
......
...@@ -7,6 +7,8 @@ export const generatedAtTimeAgo = ({ currentList }, getters) => ...@@ -7,6 +7,8 @@ export const generatedAtTimeAgo = ({ currentList }, getters) =>
export const isJobNotSetUp = ({ currentList }, getters) => getters[`${currentList}/isJobNotSetUp`]; export const isJobNotSetUp = ({ currentList }, getters) => getters[`${currentList}/isJobNotSetUp`];
export const isJobFailed = ({ currentList }, getters) => getters[`${currentList}/isJobFailed`]; export const isJobFailed = ({ currentList }, getters) => getters[`${currentList}/isJobFailed`];
export const isIncomplete = ({ currentList }, getters) => getters[`${currentList}/isIncomplete`]; export const isIncomplete = ({ currentList }, getters) => getters[`${currentList}/isIncomplete`];
export const hasNoDependencies = ({ currentList }, getters) =>
getters[`${currentList}/hasNoDependencies`];
export const totals = state => export const totals = state =>
state.listTypes.reduce( state.listTypes.reduce(
......
...@@ -5,8 +5,8 @@ export const generatedAtTimeAgo = ({ reportInfo: { generatedAt } }) => ...@@ -5,8 +5,8 @@ export const generatedAtTimeAgo = ({ reportInfo: { generatedAt } }) =>
generatedAt ? getTimeago().format(generatedAt) : ''; generatedAt ? getTimeago().format(generatedAt) : '';
export const isJobNotSetUp = state => state.reportInfo.status === REPORT_STATUS.jobNotSetUp; export const isJobNotSetUp = state => state.reportInfo.status === REPORT_STATUS.jobNotSetUp;
export const isJobFailed = state => export const isJobFailed = state => state.reportInfo.status === REPORT_STATUS.jobFailed;
[REPORT_STATUS.jobFailed, REPORT_STATUS.noDependencies].includes(state.reportInfo.status);
export const isIncomplete = state => state.reportInfo.status === REPORT_STATUS.incomplete; 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; export const downloadEndpoint = ({ endpoint }) => endpoint;
- breadcrumb_title _('Dependency List') - breadcrumb_title _('Dependency List')
- page_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', () => { ...@@ -22,6 +22,7 @@ describe('DependenciesApp component', () => {
endpoint: '/foo', endpoint: '/foo',
emptyStateSvgPath: '/bar.svg', emptyStateSvgPath: '/bar.svg',
documentationPath: TEST_HOST, documentationPath: TEST_HOST,
supportDocumentationPath: `${TEST_HOST}/dependency_scanning#supported-languages`,
}; };
const factory = (props = basicAppProps) => { const factory = (props = basicAppProps) => {
...@@ -87,6 +88,16 @@ describe('DependenciesApp component', () => { ...@@ -87,6 +88,16 @@ describe('DependenciesApp component', () => {
store.state[allNamespace].reportInfo.status = REPORT_STATUS.incomplete; 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 findJobFailedAlert = () => wrapper.find(DependencyListJobFailedAlert);
const findIncompleteListAlert = () => wrapper.find(DependencyListIncompleteAlert); const findIncompleteListAlert = () => wrapper.find(DependencyListIncompleteAlert);
const findDependenciesTables = () => wrapper.findAll(PaginatedDependenciesTable); const findDependenciesTables = () => wrapper.findAll(PaginatedDependenciesTable);
...@@ -104,6 +115,11 @@ describe('DependenciesApp component', () => { ...@@ -104,6 +115,11 @@ describe('DependenciesApp component', () => {
expect(componentWrapper.props()).toEqual(expect.objectContaining(props)); 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 expectNoDependenciesTables = () => expect(findDependenciesTables()).toHaveLength(0);
const expectNoHeader = () => expect(findHeader().exists()).toBe(false); const expectNoHeader = () => expect(findHeader().exists()).toBe(false);
...@@ -149,6 +165,7 @@ describe('DependenciesApp component', () => { ...@@ -149,6 +165,7 @@ describe('DependenciesApp component', () => {
it('shows only the empty state', () => { it('shows only the empty state', () => {
expectComponentWithProps(GlEmptyState, { svgPath: basicAppProps.emptyStateSvgPath }); expectComponentWithProps(GlEmptyState, { svgPath: basicAppProps.emptyStateSvgPath });
expectComponentPropsToMatchSnapshot(GlEmptyState);
expectNoHeader(); expectNoHeader();
expectNoDependenciesTables(); expectNoDependenciesTables();
}); });
...@@ -324,5 +341,18 @@ describe('DependenciesApp component', () => { ...@@ -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', () => { ...@@ -24,6 +24,7 @@ describe('Dependencies getters', () => {
${'isJobNotSetUp'} ${'isJobNotSetUp'}
${'isJobFailed'} ${'isJobFailed'}
${'isIncomplete'} ${'isIncomplete'}
${'hasNoDependencies'}
${'generatedAtTimeAgo'} ${'generatedAtTimeAgo'}
`('$getterName', ({ getterName }) => { `('$getterName', ({ getterName }) => {
it(`delegates to the current list module's ${getterName} getter`, () => { it(`delegates to the current list module's ${getterName} getter`, () => {
......
...@@ -5,14 +5,16 @@ import { getDateInPast } from '~/lib/utils/datetime_utility'; ...@@ -5,14 +5,16 @@ import { getDateInPast } from '~/lib/utils/datetime_utility';
describe('Dependencies getters', () => { describe('Dependencies getters', () => {
describe.each` describe.each`
getterName | reportStatus | outcome getterName | reportStatus | outcome
${'isJobNotSetUp'} | ${REPORT_STATUS.jobNotSetUp} | ${true} ${'isJobNotSetUp'} | ${REPORT_STATUS.jobNotSetUp} | ${true}
${'isJobNotSetUp'} | ${REPORT_STATUS.ok} | ${false} ${'isJobNotSetUp'} | ${REPORT_STATUS.ok} | ${false}
${'isJobFailed'} | ${REPORT_STATUS.jobFailed} | ${true} ${'isJobFailed'} | ${REPORT_STATUS.jobFailed} | ${true}
${'isJobFailed'} | ${REPORT_STATUS.noDependencies} | ${true} ${'isJobFailed'} | ${REPORT_STATUS.noDependencies} | ${false}
${'isJobFailed'} | ${REPORT_STATUS.ok} | ${false} ${'isJobFailed'} | ${REPORT_STATUS.ok} | ${false}
${'isIncomplete'} | ${REPORT_STATUS.incomplete} | ${true} ${'hasNoDependencies'} | ${REPORT_STATUS.ok} | ${false}
${'isIncomplete'} | ${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 }) => { `('$getterName when report status is $reportStatus', ({ getterName, reportStatus, outcome }) => {
it(`returns ${outcome}`, () => { it(`returns ${outcome}`, () => {
expect( expect(
......
...@@ -6303,6 +6303,9 @@ msgstr "" ...@@ -6303,6 +6303,9 @@ msgstr ""
msgid "Dependency List" msgid "Dependency List"
msgstr "" msgstr ""
msgid "Dependency List has no entries"
msgstr ""
msgid "Dependency Proxy" msgid "Dependency Proxy"
msgstr "" msgstr ""
...@@ -10822,6 +10825,9 @@ 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." 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 "" 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" msgid "It's you"
msgstr "" msgstr ""
...@@ -21516,6 +21522,9 @@ msgstr "" ...@@ -21516,6 +21522,9 @@ msgstr ""
msgid "View replaced file @ " msgid "View replaced file @ "
msgstr "" msgstr ""
msgid "View supported languages and frameworks"
msgstr ""
msgid "View the documentation" msgid "View the documentation"
msgstr "" 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