Commit f3af6a58 authored by David Pisek's avatar David Pisek Committed by Natalia Tepluhina

Add pipeline information to dependencies header

This commit adds additional information (link to latest pipeline
and it's time) to the header section of the dependency list page.

It also increases the main headers font size adds a link to the
relevant documentation page to it.
parent afb3cff3
---
title: Add pipeline information to dependency list header
merge_request: 19352
author:
type: added
......@@ -17,7 +17,7 @@ sidebar.
## Viewing dependencies
![Dependency List](img/dependency_list_v12_3.png)
![Dependency List](img/dependency_list_v12_4.png)
Dependencies are displayed with the following information:
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import DependenciesActions from './dependencies_actions.vue';
import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vue';
import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue';
......@@ -16,8 +18,10 @@ export default {
GlLoadingIcon,
GlTab,
GlTabs,
GlLink,
DependencyListIncompleteAlert,
DependencyListJobFailedAlert,
Icon,
PaginatedDependenciesTable,
},
props: {
......@@ -43,6 +47,7 @@ export default {
computed: {
...mapState(['currentList', 'listTypes']),
...mapGetters([
'generatedAtTimeAgo',
'isInitialized',
'isJobNotSetUp',
'isJobFailed',
......@@ -60,6 +65,18 @@ export default {
this.setCurrentList(namespace);
},
},
subHeadingText() {
const { jobPath } = this.reportInfo;
const body = __(
'Displays dependencies and known vulnerabilities, based on the %{linkStart}latest pipeline%{linkEnd} scan',
);
const linkStart = jobPath ? `<a href="${jobPath}">` : '';
const linkEnd = jobPath ? '</a>' : '';
return sprintf(body, { linkStart, linkEnd }, false);
},
},
created() {
this.setDependenciesEndpoint(this.endpoint);
......@@ -97,7 +114,7 @@ export default {
:primary-button-text="__('Learn more about the dependency list')"
/>
<div v-else>
<section v-else>
<dependency-list-incomplete-alert
v-if="isIncomplete && !isIncompleteAlertDismissed"
@close="dismissIncompleteListAlert"
......@@ -109,7 +126,24 @@ export default {
@close="dismissJobFailedAlert"
/>
<h3 class="h5">{{ __('Dependencies') }}</h3>
<header class="my-3">
<h2 class="h4 mb-1">
{{ __('Dependencies') }}
<gl-link
target="_blank"
:href="documentationPath"
:aria-label="__('Dependencies help page 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
>
</p>
</header>
<gl-tabs v-model="currentListIndex" content-class="pt-0">
<gl-tab
......@@ -131,5 +165,5 @@ export default {
</li>
</template>
</gl-tabs>
</div>
</section>
</template>
export const isInitialized = ({ currentList, ...state }) => state[currentList].initialized;
export const reportInfo = ({ currentList, ...state }) => state[currentList].reportInfo;
export const generatedAtTimeAgo = ({ currentList }, getters) =>
getters[`${currentList}/generatedAtTimeAgo`];
export const isJobNotSetUp = ({ currentList }, getters) => getters[`${currentList}/isJobNotSetUp`];
export const isJobFailed = ({ currentList }, getters) => getters[`${currentList}/isJobFailed`];
export const isIncomplete = ({ currentList }, getters) => getters[`${currentList}/isIncomplete`];
......
import { REPORT_STATUS } from './constants';
import { getTimeago } from '~/lib/utils/datetime_utility';
export const generatedAtTimeAgo = ({ reportInfo: { generatedAt } }) =>
generatedAt ? getTimeago().format(generatedAt) : '';
export const isJobNotSetUp = state => state.reportInfo.status === REPORT_STATUS.jobNotSetUp;
export const isJobFailed = state =>
......
......@@ -19,6 +19,7 @@ export default {
state.errorLoading = false;
state.reportInfo.status = reportInfo.status;
state.reportInfo.jobPath = reportInfo.job_path;
state.reportInfo.generatedAt = reportInfo.generated_at;
state.initialized = true;
},
[types.RECEIVE_DEPENDENCIES_ERROR](state) {
......@@ -29,6 +30,7 @@ export default {
state.reportInfo = {
status: REPORT_STATUS.ok,
jobPath: '',
generatedAt: '',
};
state.initialized = true;
},
......
......@@ -12,6 +12,7 @@ export default () => ({
reportInfo: {
status: REPORT_STATUS.ok,
jobPath: '',
generatedAt: '',
},
filter: FILTER.all,
sortField: 'name',
......
import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab } from '@gitlab/ui';
import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab, GlLink } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { TEST_HOST } from 'helpers/test_constants';
import createStore from 'ee/dependencies/store';
import { addListType } from 'ee/dependencies/store/utils';
......@@ -64,6 +65,8 @@ describe('DependenciesApp component', () => {
});
store.state[namespace].pageInfo.total = total;
store.state[namespace].reportInfo.status = REPORT_STATUS.ok;
store.state[namespace].reportInfo.generatedAt = getDateInPast(new Date(), 7);
store.state[namespace].reportInfo.jobPath = '/jobs/foo/321';
});
};
......@@ -95,6 +98,10 @@ describe('DependenciesApp component', () => {
const findVulnerableTabControl = () => findTabControls().at(1);
const findVulnerableTabComponent = () => wrapper.findAll(GlTab).at(1);
const findHeader = () => wrapper.find('section > header');
const findHeaderHelpLink = () => findHeader().find(GlLink);
const findHeaderJobLink = () => findHeader().find('a');
const expectComponentWithProps = (Component, props = {}) => {
const componentWrapper = wrapper.find(Component);
expect(componentWrapper.isVisible()).toBe(true);
......@@ -102,6 +109,7 @@ describe('DependenciesApp component', () => {
};
const expectNoDependenciesTables = () => expect(findDependenciesTables()).toHaveLength(0);
const expectNoHeader = () => expect(findHeader().exists()).toBe(false);
const expectDependenciesTables = () => {
const { wrappers } = findDependenciesTables();
......@@ -110,6 +118,10 @@ describe('DependenciesApp component', () => {
expect(wrappers[1].props()).toEqual({ namespace: vulnerableNamespace });
};
const expectHeader = () => {
expect(findHeader().exists()).toBe(true);
};
afterEach(() => {
wrapper.destroy();
});
......@@ -128,6 +140,7 @@ describe('DependenciesApp component', () => {
it('shows only the loading icon', () => {
expectComponentWithProps(GlLoadingIcon);
expectNoHeader();
expectNoDependenciesTables();
});
......@@ -140,6 +153,7 @@ describe('DependenciesApp component', () => {
it('shows only the empty state', () => {
expectComponentWithProps(GlEmptyState, { svgPath: basicAppProps.emptyStateSvgPath });
expectNoHeader();
expectNoDependenciesTables();
});
});
......@@ -152,9 +166,22 @@ describe('DependenciesApp component', () => {
});
it('shows both dependencies tables with the correct props', () => {
expectHeader();
expectDependenciesTables();
});
it('shows a link to the latest job', () => {
expect(findHeaderJobLink().attributes('href')).toBe('/jobs/foo/321');
});
it('shows when the last job ran', () => {
expect(findHeader().text()).toContain('1 week ago');
});
it('shows a link to the dependencies documentation page', () => {
expect(findHeaderHelpLink().attributes('href')).toBe(TEST_HOST);
});
it('displays the tabs correctly', () => {
const expected = [
{
......@@ -225,6 +252,27 @@ describe('DependenciesApp component', () => {
expect(findVulnerableTabComponent().classes('disabled')).toBe(true);
});
});
describe('given the user has public permissions', () => {
beforeEach(() => {
store.state[allNamespace].reportInfo.generatedAt = '';
store.state[allNamespace].reportInfo.jobPath = '';
return wrapper.vm.$nextTick();
});
it('shows the header', () => {
expectHeader();
});
it('does not show when the last job ran', () => {
expect(findHeader().text()).not.toContain('1 week ago');
});
it('does not show a link to the latest job', () => {
expect(findHeaderJobLink().exists()).toBe(false);
});
});
});
describe('given the dependency list job failed', () => {
......
......@@ -24,6 +24,7 @@ describe('Dependencies getters', () => {
${'isJobNotSetUp'}
${'isJobFailed'}
${'isIncomplete'}
${'generatedAtTimeAgo'}
`('$getterName', ({ getterName }) => {
it(`delegates to the current list module's ${getterName} getter`, () => {
const mockValue = {};
......
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { TEST_HOST } from 'helpers/test_constants';
import * as getters from 'ee/dependencies/store/modules/list/getters';
import { REPORT_STATUS } from 'ee/dependencies/store/modules/list/constants';
......@@ -30,4 +31,24 @@ describe('Dependencies getters', () => {
expect(getters.downloadEndpoint({ endpoint })).toBe(`${TEST_HOST}/dependencies.json`);
});
});
describe('generatedAtTimeAgo', () => {
it.each`
daysAgo | outcome
${1} | ${'1 day ago'}
${2} | ${'2 days ago'}
${7} | ${'1 week ago'}
`(
'should return "$outcome" when "generatedAt" was $daysAgo days ago',
({ daysAgo, outcome }) => {
const generatedAt = getDateInPast(new Date(), daysAgo);
expect(getters.generatedAtTimeAgo({ reportInfo: { generatedAt } })).toBe(outcome);
},
);
it('should return an empty string when "generatedAt" is not given', () => {
expect(getters.generatedAtTimeAgo({ reportInfo: {} })).toBe('');
});
});
});
......@@ -45,6 +45,7 @@ describe('Dependencies mutations', () => {
const reportInfo = {
status: REPORT_STATUS.jobFailed,
job_path: 'foo',
generated_at: 'foo',
};
beforeEach(() => {
......@@ -60,6 +61,7 @@ describe('Dependencies mutations', () => {
expect(state.reportInfo).toEqual({
status: REPORT_STATUS.jobFailed,
jobPath: 'foo',
generatedAt: 'foo',
});
});
});
......@@ -78,6 +80,7 @@ describe('Dependencies mutations', () => {
expect(state.reportInfo).toEqual({
status: REPORT_STATUS.ok,
jobPath: '',
generatedAt: '',
});
});
});
......
......@@ -5293,6 +5293,9 @@ msgstr ""
msgid "Dependencies"
msgstr ""
msgid "Dependencies help page link"
msgstr ""
msgid "Dependencies|%d additional vulnerability not shown"
msgid_plural "Dependencies|%d additional vulnerabilities not shown"
msgstr[0] ""
......@@ -5789,6 +5792,9 @@ msgstr ""
msgid "Display name"
msgstr ""
msgid "Displays dependencies and known vulnerabilities, based on the %{linkStart}latest pipeline%{linkEnd} scan"
msgstr ""
msgid "Do not display offers from third parties within GitLab"
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