Commit 892c3b9a authored by Alexander Turinske's avatar Alexander Turinske Committed by Kushal Pandya

Create pipeline status widget

- add time from update and link
- add tests
parent dea7f319
...@@ -69,12 +69,15 @@ At the project level, the Security Dashboard displays the vulnerabilities merged ...@@ -69,12 +69,15 @@ At the project level, the Security Dashboard displays the vulnerabilities merged
to **Security & Compliance > Security Dashboard**. By default, the Security Dashboard displays all to **Security & Compliance > Security Dashboard**. By default, the Security Dashboard displays all
detected and confirmed vulnerabilities. detected and confirmed vulnerabilities.
The Security Dashboard first displays the total number of vulnerabilities by severity (for example, The Security Dashboard first displays the time at which the last pipeline completed on the project's
default branch. There's also a link to view this in more detail.
The Security Dashboard next displays the total number of vulnerabilities by severity (for example,
Critical, High, Medium, Low, Info, Unknown). Below this, a table shows each vulnerability's status, severity, Critical, High, Medium, Low, Info, Unknown). Below this, a table shows each vulnerability's status, severity,
and description. Clicking a vulnerability takes you to its [Vulnerability Details](../vulnerabilities) and description. Clicking a vulnerability takes you to its [Vulnerability Details](../vulnerabilities)
page to view more information about that vulnerability. page to view more information about that vulnerability.
![Project Security Dashboard](img/project_security_dashboard_v13_4.png) ![Project Security Dashboard](img/project_security_dashboard_v13_5.png)
You can filter the vulnerabilities by one or more of the following: You can filter the vulnerabilities by one or more of the following:
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AutoFixUserCallout from './auto_fix_user_callout.vue'; import AutoFixUserCallout from './auto_fix_user_callout.vue';
import ProjectPipelineStatus from './project_pipeline_status.vue';
import ProjectVulnerabilitiesApp from './project_vulnerabilities.vue'; import ProjectVulnerabilitiesApp from './project_vulnerabilities.vue';
import ReportsNotConfigured from './empty_states/reports_not_configured.vue'; import ReportsNotConfigured from './empty_states/reports_not_configured.vue';
import SecurityDashboardLayout from './security_dashboard_layout.vue'; import SecurityDashboardLayout from './security_dashboard_layout.vue';
...@@ -14,6 +15,7 @@ export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner'; ...@@ -14,6 +15,7 @@ export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner';
export default { export default {
components: { components: {
AutoFixUserCallout, AutoFixUserCallout,
ProjectPipelineStatus,
ProjectVulnerabilitiesApp, ProjectVulnerabilitiesApp,
ReportsNotConfigured, ReportsNotConfigured,
SecurityDashboardLayout, SecurityDashboardLayout,
...@@ -27,6 +29,11 @@ export default { ...@@ -27,6 +29,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
pipeline: {
type: Object,
required: false,
default: () => ({}),
},
projectFullPath: { projectFullPath: {
type: String, type: String,
required: false, required: false,
...@@ -52,6 +59,11 @@ export default { ...@@ -52,6 +59,11 @@ export default {
}; };
}, },
inject: ['dashboardDocumentation', 'autoFixDocumentation'], inject: ['dashboardDocumentation', 'autoFixDocumentation'],
computed: {
shouldShowPipelineStatus() {
return Object.values(this.pipeline).every(Boolean);
},
},
methods: { methods: {
handleFilterChange(filters) { handleFilterChange(filters) {
this.filters = filters; this.filters = filters;
...@@ -78,6 +90,7 @@ export default { ...@@ -78,6 +90,7 @@ export default {
<h4 class="flex-grow mt-0 mb-0">{{ __('Vulnerabilities') }}</h4> <h4 class="flex-grow mt-0 mb-0">{{ __('Vulnerabilities') }}</h4>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" /> <csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" />
</div> </div>
<project-pipeline-status v-if="shouldShowPipelineStatus" :pipeline="pipeline" />
<vulnerabilities-count-list :project-full-path="projectFullPath" :filters="filters" /> <vulnerabilities-count-list :project-full-path="projectFullPath" :filters="filters" />
</template> </template>
<template #sticky> <template #sticky>
......
<script>
import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
GlLink,
TimeAgoTooltip,
},
props: {
pipeline: { type: Object, required: true },
},
i18n: {
title: __(
'The Security Dashboard shows the results of the last successful pipeline run on the default branch.',
),
label: __('Last updated'),
},
};
</script>
<template>
<div>
<h6 class="gl-font-weight-normal">{{ $options.i18n.title }}</h6>
<div class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-6">
<span class="gl-font-weight-bold">{{ $options.i18n.label }}</span>
<time-ago-tooltip class="gl-px-3" :time="pipeline.createdAt" />
<gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link>
</div>
</div>
</template>
...@@ -41,6 +41,8 @@ export default (el, dashboardType) => { ...@@ -41,6 +41,8 @@ export default (el, dashboardType) => {
if (dashboardType === DASHBOARD_TYPES.PROJECT) { if (dashboardType === DASHBOARD_TYPES.PROJECT) {
component = FirstClassProjectSecurityDashboard; component = FirstClassProjectSecurityDashboard;
const { pipelineCreatedAt: createdAt, pipelineId: id, pipelinePath: path } = el.dataset;
props.pipeline = { createdAt, id, path };
props.projectFullPath = el.dataset.projectFullPath; props.projectFullPath = el.dataset.projectFullPath;
provide.autoFixDocumentation = el.dataset.autoFixDocumentation; provide.autoFixDocumentation = el.dataset.autoFixDocumentation;
provide.pipelineSecurityBuildsFailedCount = el.dataset.pipelineSecurityBuildsFailedCount; provide.pipelineSecurityBuildsFailedCount = el.dataset.pipelineSecurityBuildsFailedCount;
......
---
title: Create pipeline status widget
merge_request: 44521
author:
type: added
...@@ -5,6 +5,7 @@ import FirstClassProjectSecurityDashboard from 'ee/security_dashboard/components ...@@ -5,6 +5,7 @@ import FirstClassProjectSecurityDashboard from 'ee/security_dashboard/components
import AutoFixUserCallout from 'ee/security_dashboard/components/auto_fix_user_callout.vue'; import AutoFixUserCallout from 'ee/security_dashboard/components/auto_fix_user_callout.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue'; import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import ProjectPipelineStatus from 'ee/security_dashboard/components/project_pipeline_status.vue';
import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue'; import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue';
import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue'; import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.vue'; import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.vue';
...@@ -13,6 +14,11 @@ import CsvExportButton from 'ee/security_dashboard/components/csv_export_button. ...@@ -13,6 +14,11 @@ import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.
const props = { const props = {
notEnabledScannersHelpPath: '/help/docs/', notEnabledScannersHelpPath: '/help/docs/',
noPipelineRunScannersHelpPath: '/new/pipeline', noPipelineRunScannersHelpPath: '/new/pipeline',
pipeline: {
createdAt: '2020-10-06T20:08:07Z',
id: '214',
path: '/mixed-vulnerabilities/dependency-list-test-01/-/pipelines/214',
},
projectFullPath: '/group/project', projectFullPath: '/group/project',
securityDashboardHelpPath: '/security/dashboard/help-path', securityDashboardHelpPath: '/security/dashboard/help-path',
vulnerabilitiesExportEndpoint: '/vulnerabilities/exports', vulnerabilitiesExportEndpoint: '/vulnerabilities/exports',
...@@ -33,6 +39,7 @@ describe('First class Project Security Dashboard component', () => { ...@@ -33,6 +39,7 @@ describe('First class Project Security Dashboard component', () => {
let wrapper; let wrapper;
const findFilters = () => wrapper.find(Filters); const findFilters = () => wrapper.find(Filters);
const findProjectPipelineStatus = () => wrapper.find(ProjectPipelineStatus);
const findVulnerabilities = () => wrapper.find(ProjectVulnerabilitiesApp); const findVulnerabilities = () => wrapper.find(ProjectVulnerabilitiesApp);
const findVulnerabilityCountList = () => wrapper.find(VulnerabilityCountList); const findVulnerabilityCountList = () => wrapper.find(VulnerabilityCountList);
const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured); const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured);
...@@ -95,6 +102,10 @@ describe('First class Project Security Dashboard component', () => { ...@@ -95,6 +102,10 @@ describe('First class Project Security Dashboard component', () => {
props.vulnerabilitiesExportEndpoint, props.vulnerabilitiesExportEndpoint,
); );
}); });
it('should display the project pipeline status', () => {
expect(findProjectPipelineStatus()).toExist();
});
}); });
describe('auto-fix user callout', () => { describe('auto-fix user callout', () => {
...@@ -157,6 +168,7 @@ describe('First class Project Security Dashboard component', () => { ...@@ -157,6 +168,7 @@ describe('First class Project Security Dashboard component', () => {
createComponent({ createComponent({
props: { props: {
hasVulnerabilities: true, hasVulnerabilities: true,
pipeline: { id: '214' },
}, },
data() { data() {
return { filters }; return { filters };
...@@ -181,5 +193,23 @@ describe('First class Project Security Dashboard component', () => { ...@@ -181,5 +193,23 @@ describe('First class Project Security Dashboard component', () => {
it('displays the unconfigured state', () => { it('displays the unconfigured state', () => {
expect(findUnconfiguredState().exists()).toBe(true); expect(findUnconfiguredState().exists()).toBe(true);
}); });
it('does not display the project pipeline status', () => {
expect(findProjectPipelineStatus().exists()).toBe(false);
});
});
describe('when there is no pipeline data', () => {
beforeEach(() => {
createComponent({
props: {
pipeline: undefined,
},
});
});
it('does not display the project pipeline status', () => {
expect(findProjectPipelineStatus().exists()).toBe(false);
});
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import ProjectPipelineStatus from 'ee/security_dashboard/components/project_pipeline_status.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Project Pipeline Status Component', () => {
let wrapper;
const propsData = {
pipeline: {
createdAt: '2020-10-06T20:08:07Z',
id: '214',
path: '/mixed-vulnerabilities/dependency-list-test-01/-/pipelines/214',
},
};
const findLink = () => wrapper.find(GlLink);
const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip);
const createWrapper = () => {
return shallowMount(ProjectPipelineStatus, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('default state', () => {
beforeEach(() => {
wrapper = createWrapper();
});
it('should show the timeAgoTooltip component', () => {
const TimeComponent = findTimeAgoTooltip();
expect(TimeComponent.exists()).toBeTruthy();
expect(TimeComponent.props()).toStrictEqual({
time: propsData.pipeline.createdAt,
cssClass: '',
tooltipPlacement: 'top',
});
});
it('should show the link component', () => {
const GlLinkComponent = findLink();
expect(GlLinkComponent.exists()).toBeTruthy();
expect(GlLinkComponent.text()).toBe(`#${propsData.pipeline.id}`);
expect(GlLinkComponent.attributes('href')).toBe(propsData.pipeline.path);
});
});
});
...@@ -25920,6 +25920,9 @@ msgstr "" ...@@ -25920,6 +25920,9 @@ msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}" msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr "" msgstr ""
msgid "The Security Dashboard shows the results of the last successful pipeline run on the default branch."
msgstr ""
msgid "The URL defined on the primary node that secondary nodes should use to contact it." msgid "The URL defined on the primary node that secondary nodes should use to contact it."
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