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 {
};
},
inject: ['dashboardDocumentation', 'autoFixDocumentation'],
computed: {
shouldShowPipelineStatus() {
return Object.values(this.pipeline).every(Boolean);
},
},
methods: {
handleFilterChange(filters) {
this.filters = filters;
......@@ -90,7 +85,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" />
<project-pipeline-status :pipeline="pipeline" />
<vulnerabilities-count-list :project-full-path="projectFullPath" :filters="filters" />
</template>
<template #sticky>
......
......@@ -3,18 +3,29 @@ import { GlBadge, GlIcon } from '@gitlab/ui';
export default {
components: { GlBadge, GlIcon },
inject: {
pipelineSecurityBuildsFailedCount: { default: 0 },
pipelineSecurityBuildsFailedPath: { default: '' },
props: {
pipeline: {
type: Object,
required: true,
},
},
computed: {
failedCount() {
return this.pipeline.securityBuildsFailedCount || 0;
},
failedPath() {
return this.pipeline.securityBuildsFailedPath || '';
},
shouldShow() {
return this.failedCount > 0;
},
},
};
</script>
<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" />
{{
n__('%d failed security job', '%d failed security jobs', pipelineSecurityBuildsFailedCount)
}}
{{ n__('%d failed security job', '%d failed security jobs', failedCount) }}
</gl-badge>
</template>
......@@ -2,15 +2,22 @@
import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import PipelineStatusBadge from './pipeline_status_badge.vue';
export default {
components: {
GlLink,
TimeAgoTooltip,
PipelineStatusBadge,
},
props: {
pipeline: { type: Object, required: true },
},
computed: {
shouldShowPipelineStatus() {
return this.pipeline.createdAt && this.pipeline.id && this.pipeline.path;
},
},
i18n: {
title: __(
'The Security Dashboard shows the results of the last successful pipeline run on the default branch.',
......@@ -21,12 +28,15 @@ export default {
</script>
<template>
<div>
<div v-if="shouldShowPipelineStatus">
<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>
<time-ago-tooltip class="gl-px-3" :time="pipeline.createdAt" />
<gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link>
<pipeline-status-badge :pipeline="pipeline" class="gl-ml-3" />
</div>
</div>
</template>
......@@ -41,12 +41,22 @@ 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 };
const {
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;
provide.autoFixDocumentation = el.dataset.autoFixDocumentation;
provide.pipelineSecurityBuildsFailedCount = el.dataset.pipelineSecurityBuildsFailedCount;
provide.pipelineSecurityBuildsFailedPath = el.dataset.pipelineSecurityBuildsFailedPath;
} else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard;
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', () => {
createComponent({
props: {
hasVulnerabilities: true,
pipeline: { id: '214' },
},
data() {
return { filters };
......@@ -193,23 +192,5 @@ 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 { GlBadge } from '@gitlab/ui';
import { merge } from 'lodash';
import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineStatusBadge from 'ee/security_dashboard/components/pipeline_status_badge.vue';
describe('Pipeline status badge', () => {
const pipelineSecurityBuildsFailedPath = '/some/path/to/failed/jobs';
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, {
provide: {
pipelineSecurityBuildsFailedCount,
pipelineSecurityBuildsFailedPath,
},
stubs: { GlBadge },
propsData: merge({ pipeline: { securityBuildsFailedPath } }, props),
});
};
......@@ -22,18 +24,35 @@ describe('Pipeline status badge', () => {
wrapper = null;
});
it('displays correct message for 5 failed jobs', () => {
createWrapper({ pipelineSecurityBuildsFailedCount: 5 });
expect(wrapper.text()).toBe('5 failed security jobs');
});
describe.each`
failedCount | expectedMessage
${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', () => {
createWrapper({ pipelineSecurityBuildsFailedCount: 1 });
expect(wrapper.text()).toBe('1 failed security job');
it('displays correct message', () => {
expect(wrapper.text()).toBe(expectedMessage);
});
it('links to the correct path', () => {
expect(findGlBadge().attributes('href')).toBe(securityBuildsFailedPath);
});
});
it('links to the correct path', () => {
createWrapper({ pipelineSecurityBuildsFailedCount: 5 });
expect(wrapper.find(GlBadge).attributes('href')).toBe(pipelineSecurityBuildsFailedPath);
describe('when there are not more than 0 failed jobs', () => {
it('does not display when there are 0 failed jobs', () => {
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 { GlLink } from '@gitlab/ui';
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';
describe('Project Pipeline Status Component', () => {
let wrapper;
const propsData = {
const DEFAULT_PROPS = {
pipeline: {
createdAt: '2020-10-06T20:08:07Z',
id: '214',
......@@ -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 findLink = () => wrapper.find(GlLink);
const createWrapper = () => {
const createWrapper = ({ props = {}, options = {} } = {}) => {
return shallowMount(ProjectPipelineStatus, {
propsData,
propsData: { ...DEFAULT_PROPS, ...props },
...options,
});
};
......@@ -37,7 +40,7 @@ describe('Project Pipeline Status Component', () => {
const TimeComponent = findTimeAgoTooltip();
expect(TimeComponent.exists()).toBeTruthy();
expect(TimeComponent.props()).toStrictEqual({
time: propsData.pipeline.createdAt,
time: DEFAULT_PROPS.pipeline.createdAt,
cssClass: '',
tooltipPlacement: 'top',
});
......@@ -46,8 +49,20 @@ describe('Project Pipeline Status Component', () => {
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);
expect(GlLinkComponent.text()).toBe(`#${DEFAULT_PROPS.pipeline.id}`);
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