Commit 8bd8ea37 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '241061-integrate-pipeline-failed-widget' into 'master'

Integrate the status badge

See merge request gitlab-org/gitlab!45987
parents 74512a7b be352be2
...@@ -59,11 +59,6 @@ export default { ...@@ -59,11 +59,6 @@ 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;
...@@ -90,7 +85,7 @@ export default { ...@@ -90,7 +85,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" /> <project-pipeline-status :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>
......
...@@ -3,18 +3,29 @@ import { GlBadge, GlIcon } from '@gitlab/ui'; ...@@ -3,18 +3,29 @@ import { GlBadge, GlIcon } from '@gitlab/ui';
export default { export default {
components: { GlBadge, GlIcon }, components: { GlBadge, GlIcon },
inject: { props: {
pipelineSecurityBuildsFailedCount: { default: 0 }, pipeline: {
pipelineSecurityBuildsFailedPath: { default: '' }, type: Object,
required: true,
},
},
computed: {
failedCount() {
return this.pipeline.securityBuildsFailedCount || 0;
},
failedPath() {
return this.pipeline.securityBuildsFailedPath || '';
},
shouldShow() {
return this.failedCount > 0;
},
}, },
}; };
</script> </script>
<template> <template>
<gl-badge variant="danger" :href="pipelineSecurityBuildsFailedPath"> <gl-badge v-if="shouldShow" variant="danger" :href="failedPath">
<gl-icon name="status_failed" class="gl-mr-2" /> <gl-icon name="status_failed" class="gl-mr-2" />
{{ {{ n__('%d failed security job', '%d failed security jobs', failedCount) }}
n__('%d failed security job', '%d failed security jobs', pipelineSecurityBuildsFailedCount)
}}
</gl-badge> </gl-badge>
</template> </template>
...@@ -2,15 +2,22 @@ ...@@ -2,15 +2,22 @@
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import PipelineStatusBadge from './pipeline_status_badge.vue';
export default { export default {
components: { components: {
GlLink, GlLink,
TimeAgoTooltip, TimeAgoTooltip,
PipelineStatusBadge,
}, },
props: { props: {
pipeline: { type: Object, required: true }, pipeline: { type: Object, required: true },
}, },
computed: {
shouldShowPipelineStatus() {
return this.pipeline.createdAt && this.pipeline.id && this.pipeline.path;
},
},
i18n: { i18n: {
title: __( title: __(
'The Security Dashboard shows the results of the last successful pipeline run on the default branch.', 'The Security Dashboard shows the results of the last successful pipeline run on the default branch.',
...@@ -21,12 +28,15 @@ export default { ...@@ -21,12 +28,15 @@ export default {
</script> </script>
<template> <template>
<div> <div v-if="shouldShowPipelineStatus">
<h6 class="gl-font-weight-normal">{{ $options.i18n.title }}</h6> <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"> <div
class="gl-display-flex gl-align-items-center gl-border-solid gl-border-1 gl-border-gray-100 gl-p-6"
>
<span class="gl-font-weight-bold">{{ $options.i18n.label }}</span> <span class="gl-font-weight-bold">{{ $options.i18n.label }}</span>
<time-ago-tooltip class="gl-px-3" :time="pipeline.createdAt" /> <time-ago-tooltip class="gl-px-3" :time="pipeline.createdAt" />
<gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link> <gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link>
<pipeline-status-badge :pipeline="pipeline" class="gl-ml-3" />
</div> </div>
</div> </div>
</template> </template>
...@@ -41,12 +41,22 @@ export default (el, dashboardType) => { ...@@ -41,12 +41,22 @@ 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; const {
props.pipeline = { createdAt, id, path }; pipelineCreatedAt: createdAt,
pipelineId: id,
pipelinePath: path,
pipelineSecurityBuildsFailedCount: securityBuildsFailedCount,
pipelineSecurityBuildsFailedPath: securityBuildsFailedPath,
} = el.dataset;
props.pipeline = {
createdAt,
id,
path,
securityBuildsFailedCount: Number(securityBuildsFailedCount),
securityBuildsFailedPath,
};
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.pipelineSecurityBuildsFailedPath = el.dataset.pipelineSecurityBuildsFailedPath;
} else if (dashboardType === DASHBOARD_TYPES.GROUP) { } else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard; component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath; props.groupFullPath = el.dataset.groupFullPath;
......
---
title: Add security status badge to the project pipeline widget
merge_request: 45987
author:
type: added
...@@ -168,7 +168,6 @@ describe('First class Project Security Dashboard component', () => { ...@@ -168,7 +168,6 @@ describe('First class Project Security Dashboard component', () => {
createComponent({ createComponent({
props: { props: {
hasVulnerabilities: true, hasVulnerabilities: true,
pipeline: { id: '214' },
}, },
data() { data() {
return { filters }; return { filters };
...@@ -193,23 +192,5 @@ describe('First class Project Security Dashboard component', () => { ...@@ -193,23 +192,5 @@ 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 { GlBadge } from '@gitlab/ui'; import { merge } from 'lodash';
import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PipelineStatusBadge from 'ee/security_dashboard/components/pipeline_status_badge.vue'; import PipelineStatusBadge from 'ee/security_dashboard/components/pipeline_status_badge.vue';
describe('Pipeline status badge', () => { describe('Pipeline status badge', () => {
const pipelineSecurityBuildsFailedPath = '/some/path/to/failed/jobs';
let wrapper; let wrapper;
const createWrapper = ({ pipelineSecurityBuildsFailedCount }) => { const securityBuildsFailedPath = '/some/path/to/failed/jobs';
const findGlBadge = () => wrapper.find(GlBadge);
const findGlIcon = () => wrapper.find(GlIcon);
const createProps = securityBuildsFailedCount => ({ pipeline: { securityBuildsFailedCount } });
const createWrapper = (props = {}) => {
wrapper = shallowMount(PipelineStatusBadge, { wrapper = shallowMount(PipelineStatusBadge, {
provide: { propsData: merge({ pipeline: { securityBuildsFailedPath } }, props),
pipelineSecurityBuildsFailedCount,
pipelineSecurityBuildsFailedPath,
},
stubs: { GlBadge },
}); });
}; };
...@@ -22,18 +24,35 @@ describe('Pipeline status badge', () => { ...@@ -22,18 +24,35 @@ describe('Pipeline status badge', () => {
wrapper = null; wrapper = null;
}); });
it('displays correct message for 5 failed jobs', () => { describe.each`
createWrapper({ pipelineSecurityBuildsFailedCount: 5 }); failedCount | expectedMessage
expect(wrapper.text()).toBe('5 failed security jobs'); ${7} | ${'7 failed security jobs'}
}); ${1} | ${'1 failed security job'}
`('when there are failed jobs ($failedCount)', ({ failedCount, expectedMessage }) => {
beforeEach(() => {
createWrapper(createProps(failedCount));
});
it('displays correct message for 1 failed job', () => { it('displays correct message', () => {
createWrapper({ pipelineSecurityBuildsFailedCount: 1 }); expect(wrapper.text()).toBe(expectedMessage);
expect(wrapper.text()).toBe('1 failed security job'); });
it('links to the correct path', () => {
expect(findGlBadge().attributes('href')).toBe(securityBuildsFailedPath);
});
}); });
it('links to the correct path', () => { describe('when there are not more than 0 failed jobs', () => {
createWrapper({ pipelineSecurityBuildsFailedCount: 5 }); it('does not display when there are 0 failed jobs', () => {
expect(wrapper.find(GlBadge).attributes('href')).toBe(pipelineSecurityBuildsFailedPath); createWrapper(createProps(0));
expect(findGlBadge().exists()).toBe(false);
expect(findGlIcon().exists()).toBe(false);
});
it('does not display when there is no failed jobs count', () => {
createWrapper();
expect(findGlBadge().exists()).toBe(false);
expect(findGlIcon().exists()).toBe(false);
});
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import ProjectPipelineStatus from 'ee/security_dashboard/components/project_pipeline_status.vue'; import ProjectPipelineStatus from 'ee/security_dashboard/components/project_pipeline_status.vue';
import PipelineStatusBadge from 'ee/security_dashboard/components/pipeline_status_badge.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Project Pipeline Status Component', () => { describe('Project Pipeline Status Component', () => {
let wrapper; let wrapper;
const propsData = { const DEFAULT_PROPS = {
pipeline: { pipeline: {
createdAt: '2020-10-06T20:08:07Z', createdAt: '2020-10-06T20:08:07Z',
id: '214', id: '214',
...@@ -14,12 +15,14 @@ describe('Project Pipeline Status Component', () => { ...@@ -14,12 +15,14 @@ describe('Project Pipeline Status Component', () => {
}, },
}; };
const findLink = () => wrapper.find(GlLink); const findPipelineStatusBadge = () => wrapper.find(PipelineStatusBadge);
const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip); const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip);
const findLink = () => wrapper.find(GlLink);
const createWrapper = () => { const createWrapper = ({ props = {}, options = {} } = {}) => {
return shallowMount(ProjectPipelineStatus, { return shallowMount(ProjectPipelineStatus, {
propsData, propsData: { ...DEFAULT_PROPS, ...props },
...options,
}); });
}; };
...@@ -37,7 +40,7 @@ describe('Project Pipeline Status Component', () => { ...@@ -37,7 +40,7 @@ describe('Project Pipeline Status Component', () => {
const TimeComponent = findTimeAgoTooltip(); const TimeComponent = findTimeAgoTooltip();
expect(TimeComponent.exists()).toBeTruthy(); expect(TimeComponent.exists()).toBeTruthy();
expect(TimeComponent.props()).toStrictEqual({ expect(TimeComponent.props()).toStrictEqual({
time: propsData.pipeline.createdAt, time: DEFAULT_PROPS.pipeline.createdAt,
cssClass: '', cssClass: '',
tooltipPlacement: 'top', tooltipPlacement: 'top',
}); });
...@@ -46,8 +49,20 @@ describe('Project Pipeline Status Component', () => { ...@@ -46,8 +49,20 @@ describe('Project Pipeline Status Component', () => {
it('should show the link component', () => { it('should show the link component', () => {
const GlLinkComponent = findLink(); const GlLinkComponent = findLink();
expect(GlLinkComponent.exists()).toBeTruthy(); expect(GlLinkComponent.exists()).toBeTruthy();
expect(GlLinkComponent.text()).toBe(`#${propsData.pipeline.id}`); expect(GlLinkComponent.text()).toBe(`#${DEFAULT_PROPS.pipeline.id}`);
expect(GlLinkComponent.attributes('href')).toBe(propsData.pipeline.path); expect(GlLinkComponent.attributes('href')).toBe(DEFAULT_PROPS.pipeline.path);
});
});
describe('when no pipeline has run', () => {
beforeEach(() => {
wrapper = createWrapper({ props: { pipeline: { path: '' } } });
});
it('should not show the project_pipeline_status component', () => {
expect(findLink().exists()).toBe(false);
expect(findTimeAgoTooltip().exists()).toBe(false);
expect(findPipelineStatusBadge().exists()).toBe(false);
}); });
}); });
}); });
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