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.
## 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