Commit 3dbb2de4 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '34855-dependency-list-is-not-up-to-date-frontend' into 'master'

Resolve "Dependency List is not up-to-date - frontend"

See merge request gitlab-org/gitlab!19352
parents afb3cff3 f3af6a58
---
title: Add pipeline information to dependency list header
merge_request: 19352
author:
type: added
...@@ -17,7 +17,7 @@ sidebar. ...@@ -17,7 +17,7 @@ sidebar.
## Viewing dependencies ## 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: Dependencies are displayed with the following information:
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; 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 DependenciesActions from './dependencies_actions.vue';
import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vue'; import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vue';
import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue'; import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue';
...@@ -16,8 +18,10 @@ export default { ...@@ -16,8 +18,10 @@ export default {
GlLoadingIcon, GlLoadingIcon,
GlTab, GlTab,
GlTabs, GlTabs,
GlLink,
DependencyListIncompleteAlert, DependencyListIncompleteAlert,
DependencyListJobFailedAlert, DependencyListJobFailedAlert,
Icon,
PaginatedDependenciesTable, PaginatedDependenciesTable,
}, },
props: { props: {
...@@ -43,6 +47,7 @@ export default { ...@@ -43,6 +47,7 @@ export default {
computed: { computed: {
...mapState(['currentList', 'listTypes']), ...mapState(['currentList', 'listTypes']),
...mapGetters([ ...mapGetters([
'generatedAtTimeAgo',
'isInitialized', 'isInitialized',
'isJobNotSetUp', 'isJobNotSetUp',
'isJobFailed', 'isJobFailed',
...@@ -60,6 +65,18 @@ export default { ...@@ -60,6 +65,18 @@ export default {
this.setCurrentList(namespace); 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() { created() {
this.setDependenciesEndpoint(this.endpoint); this.setDependenciesEndpoint(this.endpoint);
...@@ -97,7 +114,7 @@ export default { ...@@ -97,7 +114,7 @@ export default {
:primary-button-text="__('Learn more about the dependency list')" :primary-button-text="__('Learn more about the dependency list')"
/> />
<div v-else> <section v-else>
<dependency-list-incomplete-alert <dependency-list-incomplete-alert
v-if="isIncomplete && !isIncompleteAlertDismissed" v-if="isIncomplete && !isIncompleteAlertDismissed"
@close="dismissIncompleteListAlert" @close="dismissIncompleteListAlert"
...@@ -109,7 +126,24 @@ export default { ...@@ -109,7 +126,24 @@ export default {
@close="dismissJobFailedAlert" @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-tabs v-model="currentListIndex" content-class="pt-0">
<gl-tab <gl-tab
...@@ -131,5 +165,5 @@ export default { ...@@ -131,5 +165,5 @@ export default {
</li> </li>
</template> </template>
</gl-tabs> </gl-tabs>
</div> </section>
</template> </template>
export const isInitialized = ({ currentList, ...state }) => state[currentList].initialized; export const isInitialized = ({ currentList, ...state }) => state[currentList].initialized;
export const reportInfo = ({ currentList, ...state }) => state[currentList].reportInfo; 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 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`];
......
import { REPORT_STATUS } from './constants'; 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 isJobNotSetUp = state => state.reportInfo.status === REPORT_STATUS.jobNotSetUp;
export const isJobFailed = state => export const isJobFailed = state =>
......
...@@ -19,6 +19,7 @@ export default { ...@@ -19,6 +19,7 @@ export default {
state.errorLoading = false; state.errorLoading = false;
state.reportInfo.status = reportInfo.status; state.reportInfo.status = reportInfo.status;
state.reportInfo.jobPath = reportInfo.job_path; state.reportInfo.jobPath = reportInfo.job_path;
state.reportInfo.generatedAt = reportInfo.generated_at;
state.initialized = true; state.initialized = true;
}, },
[types.RECEIVE_DEPENDENCIES_ERROR](state) { [types.RECEIVE_DEPENDENCIES_ERROR](state) {
...@@ -29,6 +30,7 @@ export default { ...@@ -29,6 +30,7 @@ export default {
state.reportInfo = { state.reportInfo = {
status: REPORT_STATUS.ok, status: REPORT_STATUS.ok,
jobPath: '', jobPath: '',
generatedAt: '',
}; };
state.initialized = true; state.initialized = true;
}, },
......
...@@ -12,6 +12,7 @@ export default () => ({ ...@@ -12,6 +12,7 @@ export default () => ({
reportInfo: { reportInfo: {
status: REPORT_STATUS.ok, status: REPORT_STATUS.ok,
jobPath: '', jobPath: '',
generatedAt: '',
}, },
filter: FILTER.all, filter: FILTER.all,
sortField: 'name', 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 { createLocalVue, mount } from '@vue/test-utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import createStore from 'ee/dependencies/store'; import createStore from 'ee/dependencies/store';
import { addListType } from 'ee/dependencies/store/utils'; import { addListType } from 'ee/dependencies/store/utils';
...@@ -64,6 +65,8 @@ describe('DependenciesApp component', () => { ...@@ -64,6 +65,8 @@ describe('DependenciesApp component', () => {
}); });
store.state[namespace].pageInfo.total = total; store.state[namespace].pageInfo.total = total;
store.state[namespace].reportInfo.status = REPORT_STATUS.ok; 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', () => { ...@@ -95,6 +98,10 @@ describe('DependenciesApp component', () => {
const findVulnerableTabControl = () => findTabControls().at(1); const findVulnerableTabControl = () => findTabControls().at(1);
const findVulnerableTabComponent = () => wrapper.findAll(GlTab).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 expectComponentWithProps = (Component, props = {}) => {
const componentWrapper = wrapper.find(Component); const componentWrapper = wrapper.find(Component);
expect(componentWrapper.isVisible()).toBe(true); expect(componentWrapper.isVisible()).toBe(true);
...@@ -102,6 +109,7 @@ describe('DependenciesApp component', () => { ...@@ -102,6 +109,7 @@ describe('DependenciesApp component', () => {
}; };
const expectNoDependenciesTables = () => expect(findDependenciesTables()).toHaveLength(0); const expectNoDependenciesTables = () => expect(findDependenciesTables()).toHaveLength(0);
const expectNoHeader = () => expect(findHeader().exists()).toBe(false);
const expectDependenciesTables = () => { const expectDependenciesTables = () => {
const { wrappers } = findDependenciesTables(); const { wrappers } = findDependenciesTables();
...@@ -110,6 +118,10 @@ describe('DependenciesApp component', () => { ...@@ -110,6 +118,10 @@ describe('DependenciesApp component', () => {
expect(wrappers[1].props()).toEqual({ namespace: vulnerableNamespace }); expect(wrappers[1].props()).toEqual({ namespace: vulnerableNamespace });
}; };
const expectHeader = () => {
expect(findHeader().exists()).toBe(true);
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -128,6 +140,7 @@ describe('DependenciesApp component', () => { ...@@ -128,6 +140,7 @@ describe('DependenciesApp component', () => {
it('shows only the loading icon', () => { it('shows only the loading icon', () => {
expectComponentWithProps(GlLoadingIcon); expectComponentWithProps(GlLoadingIcon);
expectNoHeader();
expectNoDependenciesTables(); expectNoDependenciesTables();
}); });
...@@ -140,6 +153,7 @@ describe('DependenciesApp component', () => { ...@@ -140,6 +153,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 });
expectNoHeader();
expectNoDependenciesTables(); expectNoDependenciesTables();
}); });
}); });
...@@ -152,9 +166,22 @@ describe('DependenciesApp component', () => { ...@@ -152,9 +166,22 @@ describe('DependenciesApp component', () => {
}); });
it('shows both dependencies tables with the correct props', () => { it('shows both dependencies tables with the correct props', () => {
expectHeader();
expectDependenciesTables(); 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', () => { it('displays the tabs correctly', () => {
const expected = [ const expected = [
{ {
...@@ -225,6 +252,27 @@ describe('DependenciesApp component', () => { ...@@ -225,6 +252,27 @@ describe('DependenciesApp component', () => {
expect(findVulnerableTabComponent().classes('disabled')).toBe(true); 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', () => { describe('given the dependency list job failed', () => {
......
...@@ -24,6 +24,7 @@ describe('Dependencies getters', () => { ...@@ -24,6 +24,7 @@ describe('Dependencies getters', () => {
${'isJobNotSetUp'} ${'isJobNotSetUp'}
${'isJobFailed'} ${'isJobFailed'}
${'isIncomplete'} ${'isIncomplete'}
${'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`, () => {
const mockValue = {}; const mockValue = {};
......
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import * as getters from 'ee/dependencies/store/modules/list/getters'; import * as getters from 'ee/dependencies/store/modules/list/getters';
import { REPORT_STATUS } from 'ee/dependencies/store/modules/list/constants'; import { REPORT_STATUS } from 'ee/dependencies/store/modules/list/constants';
...@@ -30,4 +31,24 @@ describe('Dependencies getters', () => { ...@@ -30,4 +31,24 @@ describe('Dependencies getters', () => {
expect(getters.downloadEndpoint({ endpoint })).toBe(`${TEST_HOST}/dependencies.json`); 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', () => { ...@@ -45,6 +45,7 @@ describe('Dependencies mutations', () => {
const reportInfo = { const reportInfo = {
status: REPORT_STATUS.jobFailed, status: REPORT_STATUS.jobFailed,
job_path: 'foo', job_path: 'foo',
generated_at: 'foo',
}; };
beforeEach(() => { beforeEach(() => {
...@@ -60,6 +61,7 @@ describe('Dependencies mutations', () => { ...@@ -60,6 +61,7 @@ describe('Dependencies mutations', () => {
expect(state.reportInfo).toEqual({ expect(state.reportInfo).toEqual({
status: REPORT_STATUS.jobFailed, status: REPORT_STATUS.jobFailed,
jobPath: 'foo', jobPath: 'foo',
generatedAt: 'foo',
}); });
}); });
}); });
...@@ -78,6 +80,7 @@ describe('Dependencies mutations', () => { ...@@ -78,6 +80,7 @@ describe('Dependencies mutations', () => {
expect(state.reportInfo).toEqual({ expect(state.reportInfo).toEqual({
status: REPORT_STATUS.ok, status: REPORT_STATUS.ok,
jobPath: '', jobPath: '',
generatedAt: '',
}); });
}); });
}); });
......
...@@ -5293,6 +5293,9 @@ msgstr "" ...@@ -5293,6 +5293,9 @@ msgstr ""
msgid "Dependencies" msgid "Dependencies"
msgstr "" msgstr ""
msgid "Dependencies help page link"
msgstr ""
msgid "Dependencies|%d additional vulnerability not shown" msgid "Dependencies|%d additional vulnerability not shown"
msgid_plural "Dependencies|%d additional vulnerabilities not shown" msgid_plural "Dependencies|%d additional vulnerabilities not shown"
msgstr[0] "" msgstr[0] ""
...@@ -5789,6 +5792,9 @@ msgstr "" ...@@ -5789,6 +5792,9 @@ msgstr ""
msgid "Display name" msgid "Display name"
msgstr "" 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" msgid "Do not display offers from third parties within GitLab"
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