Link to auto-fix MRs in PSD widget

Link to open auto-fix merge requests in the Project Security Dashboard
pipelines widget.
parent ebcc4595
......@@ -9,6 +9,7 @@ import SecurityDashboardLayout from './security_dashboard_layout.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue';
import Filters from './first_class_vulnerability_filters.vue';
import CsvExportButton from './csv_export_button.vue';
import projectAutoFixMrsCountQuery from '../graphql/project_auto_fix_mrs_count.query.graphql';
export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner';
......@@ -24,6 +25,22 @@ export default {
Filters,
},
mixins: [glFeatureFlagsMixin()],
apollo: {
autoFixMrsCount: {
query: projectAutoFixMrsCountQuery,
variables() {
return {
fullPath: this.projectFullPath,
};
},
update(data) {
return data?.project?.mergeRequests?.count || 0;
},
skip() {
return !this.glFeatures.securityAutoFix;
},
},
},
props: {
securityDashboardHelpPath: {
type: String,
......@@ -85,7 +102,7 @@ export default {
<h4 class="flex-grow mt-0 mb-0">{{ __('Vulnerabilities') }}</h4>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" />
</div>
<project-pipeline-status :pipeline="pipeline" />
<project-pipeline-status :pipeline="pipeline" :auto-fix-mrs-count="autoFixMrsCount" />
<vulnerabilities-count-list :project-full-path="projectFullPath" :filters="filters" />
</template>
<template #sticky>
......
<script>
import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import PipelineStatusBadge from './pipeline_status_badge.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
......@@ -10,8 +11,15 @@ export default {
TimeAgoTooltip,
PipelineStatusBadge,
},
mixins: [glFeatureFlagsMixin()],
inject: ['autoFixMrsPath'],
props: {
pipeline: { type: Object, required: true },
autoFixMrsCount: {
type: Number,
required: false,
default: 0,
},
},
computed: {
shouldShowPipelineStatus() {
......@@ -22,7 +30,9 @@ export default {
title: __(
'The Security Dashboard shows the results of the last successful pipeline run on the default branch.',
),
label: __('Last updated'),
lastUpdated: __('Last updated'),
autoFixSolutions: s__('AutoRemediation|Auto-fix solutions'),
autoFixMrsLink: s__('AutoRemediation|%{mrsCount} ready for review'),
},
};
</script>
......@@ -33,10 +43,20 @@ export default {
<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 class="gl-mr-6">
<span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.lastUpdated }}</span>
<span class="gl-white-space-nowrap">
<time-ago-tooltip class="gl-pr-3" :time="pipeline.createdAt" />
<gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link>
<pipeline-status-badge :pipeline="pipeline" class="gl-ml-3" />
</span>
</div>
<div v-if="autoFixMrsCount" data-testid="auto-fix-mrs-link">
<span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.autoFixSolutions }}</span>
<gl-link :href="autoFixMrsPath" target="_blank" class="gl-white-space-nowrap">{{
sprintf($options.i18n.autoFixMrsLink, { mrsCount: autoFixMrsCount })
}}</gl-link>
</div>
</div>
</div>
</template>
......@@ -57,6 +57,7 @@ export default (el, dashboardType) => {
};
props.projectFullPath = el.dataset.projectFullPath;
provide.autoFixDocumentation = el.dataset.autoFixDocumentation;
provide.autoFixMrsPath = el.dataset.autoFixMrsPath;
} else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath;
......
query autoFixMrsCount($fullPath: ID!) {
project(fullPath: $fullPath) {
mergeRequests(labels: "GitLab-auto-fix", state: opened) {
count
}
}
}
......@@ -208,7 +208,8 @@ module EE
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'),
no_pipeline_run_scanners_help_path: new_project_pipeline_path(project),
security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'),
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests')
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests'),
auto_fix_mrs_path: project_merge_requests_path(@project, label_name: 'GitLab-auto-fix')
}.merge!(security_dashboard_pipeline_data(project))
end
end
......
......@@ -46,7 +46,7 @@ describe('First class Project Security Dashboard component', () => {
const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findAutoFixUserCallout = () => wrapper.find(AutoFixUserCallout);
const createComponent = options => {
const createComponent = (options, data = {}) => {
wrapper = shallowMount(FirstClassProjectSecurityDashboard, {
propsData: {
...props,
......@@ -54,6 +54,12 @@ describe('First class Project Security Dashboard component', () => {
},
provide,
stubs: { SecurityDashboardLayout, GlBanner },
data() {
return {
autoFixMrsCount: 0,
...data,
};
},
...options,
});
};
......@@ -65,10 +71,12 @@ describe('First class Project Security Dashboard component', () => {
describe('on render when there are vulnerabilities', () => {
beforeEach(() => {
createComponent({
props: { hasVulnerabilities: true },
data: () => ({ filters }),
});
createComponent(
{
props: { hasVulnerabilities: true },
},
{ filters },
);
});
it('should render the vulnerabilities', () => {
......@@ -165,14 +173,14 @@ describe('First class Project Security Dashboard component', () => {
describe('with filter data', () => {
beforeEach(() => {
createComponent({
props: {
hasVulnerabilities: true,
},
data() {
return { filters };
createComponent(
{
props: {
hasVulnerabilities: true,
},
},
});
{ filters },
);
});
it('should pass the filter data down to the vulnerabilities', () => {
......
import { merge } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import ProjectPipelineStatus from 'ee/security_dashboard/components/project_pipeline_status.vue';
......@@ -18,12 +20,25 @@ describe('Project Pipeline Status Component', () => {
const findPipelineStatusBadge = () => wrapper.find(PipelineStatusBadge);
const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip);
const findLink = () => wrapper.find(GlLink);
const findAutoFixMrsLink = () => wrapper.findByTestId('auto-fix-mrs-link');
const createWrapper = ({ props = {}, options = {} } = {}) => {
return shallowMount(ProjectPipelineStatus, {
propsData: { ...DEFAULT_PROPS, ...props },
...options,
});
const createWrapper = (options = {}) => {
return extendedWrapper(
shallowMount(
ProjectPipelineStatus,
merge(
{},
{
propsData: DEFAULT_PROPS,
provide: {
glFeatures: { securityAutoFix: true },
autoFixMrsPath: '/merge_requests?label_name=GitLab-auto-fix',
},
},
options,
),
),
);
};
afterEach(() => {
......@@ -56,7 +71,7 @@ describe('Project Pipeline Status Component', () => {
describe('when no pipeline has run', () => {
beforeEach(() => {
wrapper = createWrapper({ props: { pipeline: { path: '' } } });
wrapper = createWrapper({ propsData: { pipeline: { path: '' } } });
});
it('should not show the project_pipeline_status component', () => {
......@@ -65,4 +80,36 @@ describe('Project Pipeline Status Component', () => {
expect(findPipelineStatusBadge().exists()).toBe(false);
});
});
describe('auto-fix MRs', () => {
describe('when there are auto-fix MRs', () => {
beforeEach(() => {
wrapper = createWrapper({
propsData: {
autoFixMrsCount: 12,
},
});
});
it('renders the auto-fix container', () => {
expect(findAutoFixMrsLink().exists()).toBe(true);
});
it('renders a link to open auto-fix MRs if any', () => {
const link = findAutoFixMrsLink().find(GlLink);
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('/merge_requests?label_name=GitLab-auto-fix');
});
});
it('does not render the link if there are no open auto-fix MRs', () => {
wrapper = createWrapper({
propsData: {
autoFixMrsCount: 0,
},
});
expect(findAutoFixMrsLink().exists()).toBe(false);
});
});
});
......@@ -155,7 +155,8 @@ RSpec.describe ProjectsHelper do
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'),
no_pipeline_run_scanners_help_path: "/#{project.full_path}/-/pipelines/new",
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests')
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests'),
auto_fix_mrs_path: end_with('/merge_requests?label_name=GitLab-auto-fix')
}
end
......
......@@ -3931,6 +3931,12 @@ msgstr ""
msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found."
msgstr ""
msgid "AutoRemediation|%{mrsCount} ready for review"
msgstr ""
msgid "AutoRemediation|Auto-fix solutions"
msgstr ""
msgid "AutoRemediation|If you're using dependency and/or container scanning, and auto-fix is enabled, auto-fix automatically creates merge requests with fixes to vulnerabilities."
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