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
to **Security & Compliance > Security Dashboard**. By default, the Security Dashboard displays all
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,
and description. Clicking a vulnerability takes you to its [Vulnerability Details](../vulnerabilities)
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:
......
......@@ -2,6 +2,7 @@
import Cookies from 'js-cookie';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AutoFixUserCallout from './auto_fix_user_callout.vue';
import ProjectPipelineStatus from './project_pipeline_status.vue';
import ProjectVulnerabilitiesApp from './project_vulnerabilities.vue';
import ReportsNotConfigured from './empty_states/reports_not_configured.vue';
import SecurityDashboardLayout from './security_dashboard_layout.vue';
......@@ -14,6 +15,7 @@ export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner';
export default {
components: {
AutoFixUserCallout,
ProjectPipelineStatus,
ProjectVulnerabilitiesApp,
ReportsNotConfigured,
SecurityDashboardLayout,
......@@ -27,6 +29,11 @@ export default {
type: String,
required: true,
},
pipeline: {
type: Object,
required: false,
default: () => ({}),
},
projectFullPath: {
type: String,
required: false,
......@@ -52,6 +59,11 @@ export default {
};
},
inject: ['dashboardDocumentation', 'autoFixDocumentation'],
computed: {
shouldShowPipelineStatus() {
return Object.values(this.pipeline).every(Boolean);
},
},
methods: {
handleFilterChange(filters) {
this.filters = filters;
......@@ -78,6 +90,7 @@ export default {
<h4 class="flex-grow mt-0 mb-0">{{ __('Vulnerabilities') }}</h4>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" />
</div>
<project-pipeline-status v-if="shouldShowPipelineStatus" :pipeline="pipeline" />
<vulnerabilities-count-list :project-full-path="projectFullPath" :filters="filters" />
</template>
<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) => {
if (dashboardType === DASHBOARD_TYPES.PROJECT) {
component = FirstClassProjectSecurityDashboard;
const { pipelineCreatedAt: createdAt, pipelineId: id, pipelinePath: path } = el.dataset;
props.pipeline = { createdAt, id, path };
props.projectFullPath = el.dataset.projectFullPath;
provide.autoFixDocumentation = el.dataset.autoFixDocumentation;
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
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 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 VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.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.
const props = {
notEnabledScannersHelpPath: '/help/docs/',
noPipelineRunScannersHelpPath: '/new/pipeline',
pipeline: {
createdAt: '2020-10-06T20:08:07Z',
id: '214',
path: '/mixed-vulnerabilities/dependency-list-test-01/-/pipelines/214',
},
projectFullPath: '/group/project',
securityDashboardHelpPath: '/security/dashboard/help-path',
vulnerabilitiesExportEndpoint: '/vulnerabilities/exports',
......@@ -33,6 +39,7 @@ describe('First class Project Security Dashboard component', () => {
let wrapper;
const findFilters = () => wrapper.find(Filters);
const findProjectPipelineStatus = () => wrapper.find(ProjectPipelineStatus);
const findVulnerabilities = () => wrapper.find(ProjectVulnerabilitiesApp);
const findVulnerabilityCountList = () => wrapper.find(VulnerabilityCountList);
const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured);
......@@ -95,6 +102,10 @@ describe('First class Project Security Dashboard component', () => {
props.vulnerabilitiesExportEndpoint,
);
});
it('should display the project pipeline status', () => {
expect(findProjectPipelineStatus()).toExist();
});
});
describe('auto-fix user callout', () => {
......@@ -157,6 +168,7 @@ describe('First class Project Security Dashboard component', () => {
createComponent({
props: {
hasVulnerabilities: true,
pipeline: { id: '214' },
},
data() {
return { filters };
......@@ -181,5 +193,23 @@ describe('First class Project Security Dashboard component', () => {
it('displays the unconfigured state', () => {
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 ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
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."
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