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'; ...@@ -9,6 +9,7 @@ import SecurityDashboardLayout from './security_dashboard_layout.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue'; import VulnerabilitiesCountList from './vulnerability_count_list.vue';
import Filters from './first_class_vulnerability_filters.vue'; import Filters from './first_class_vulnerability_filters.vue';
import CsvExportButton from './csv_export_button.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'; export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner';
...@@ -24,6 +25,22 @@ export default { ...@@ -24,6 +25,22 @@ export default {
Filters, Filters,
}, },
mixins: [glFeatureFlagsMixin()], 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: { props: {
securityDashboardHelpPath: { securityDashboardHelpPath: {
type: String, type: String,
...@@ -85,7 +102,7 @@ export default { ...@@ -85,7 +102,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 :pipeline="pipeline" /> <project-pipeline-status :pipeline="pipeline" :auto-fix-mrs-count="autoFixMrsCount" />
<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> <script>
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale'; import { __, s__ } 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'; import PipelineStatusBadge from './pipeline_status_badge.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
...@@ -10,8 +11,15 @@ export default { ...@@ -10,8 +11,15 @@ export default {
TimeAgoTooltip, TimeAgoTooltip,
PipelineStatusBadge, PipelineStatusBadge,
}, },
mixins: [glFeatureFlagsMixin()],
inject: ['autoFixMrsPath'],
props: { props: {
pipeline: { type: Object, required: true }, pipeline: { type: Object, required: true },
autoFixMrsCount: {
type: Number,
required: false,
default: 0,
},
}, },
computed: { computed: {
shouldShowPipelineStatus() { shouldShowPipelineStatus() {
...@@ -22,7 +30,9 @@ export default { ...@@ -22,7 +30,9 @@ export default {
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.',
), ),
label: __('Last updated'), lastUpdated: __('Last updated'),
autoFixSolutions: s__('AutoRemediation|Auto-fix solutions'),
autoFixMrsLink: s__('AutoRemediation|%{mrsCount} ready for review'),
}, },
}; };
</script> </script>
...@@ -33,10 +43,20 @@ export default { ...@@ -33,10 +43,20 @@ export default {
<div <div
class="gl-display-flex gl-align-items-center gl-border-solid gl-border-1 gl-border-gray-100 gl-p-6" 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> <div class="gl-mr-6">
<time-ago-tooltip class="gl-px-3" :time="pipeline.createdAt" /> <span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.lastUpdated }}</span>
<gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link> <span class="gl-white-space-nowrap">
<pipeline-status-badge :pipeline="pipeline" class="gl-ml-3" /> <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>
</div> </div>
</template> </template>
...@@ -57,6 +57,7 @@ export default (el, dashboardType) => { ...@@ -57,6 +57,7 @@ export default (el, dashboardType) => {
}; };
props.projectFullPath = el.dataset.projectFullPath; props.projectFullPath = el.dataset.projectFullPath;
provide.autoFixDocumentation = el.dataset.autoFixDocumentation; provide.autoFixDocumentation = el.dataset.autoFixDocumentation;
provide.autoFixMrsPath = el.dataset.autoFixMrsPath;
} 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;
......
query autoFixMrsCount($fullPath: ID!) {
project(fullPath: $fullPath) {
mergeRequests(labels: "GitLab-auto-fix", state: opened) {
count
}
}
}
...@@ -208,7 +208,8 @@ module EE ...@@ -208,7 +208,8 @@ module EE
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'), 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), no_pipeline_run_scanners_help_path: new_project_pipeline_path(project),
security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'), 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)) }.merge!(security_dashboard_pipeline_data(project))
end end
end end
......
...@@ -46,7 +46,7 @@ describe('First class Project Security Dashboard component', () => { ...@@ -46,7 +46,7 @@ describe('First class Project Security Dashboard component', () => {
const findCsvExportButton = () => wrapper.find(CsvExportButton); const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findAutoFixUserCallout = () => wrapper.find(AutoFixUserCallout); const findAutoFixUserCallout = () => wrapper.find(AutoFixUserCallout);
const createComponent = options => { const createComponent = (options, data = {}) => {
wrapper = shallowMount(FirstClassProjectSecurityDashboard, { wrapper = shallowMount(FirstClassProjectSecurityDashboard, {
propsData: { propsData: {
...props, ...props,
...@@ -54,6 +54,12 @@ describe('First class Project Security Dashboard component', () => { ...@@ -54,6 +54,12 @@ describe('First class Project Security Dashboard component', () => {
}, },
provide, provide,
stubs: { SecurityDashboardLayout, GlBanner }, stubs: { SecurityDashboardLayout, GlBanner },
data() {
return {
autoFixMrsCount: 0,
...data,
};
},
...options, ...options,
}); });
}; };
...@@ -65,10 +71,12 @@ describe('First class Project Security Dashboard component', () => { ...@@ -65,10 +71,12 @@ describe('First class Project Security Dashboard component', () => {
describe('on render when there are vulnerabilities', () => { describe('on render when there are vulnerabilities', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent(
props: { hasVulnerabilities: true }, {
data: () => ({ filters }), props: { hasVulnerabilities: true },
}); },
{ filters },
);
}); });
it('should render the vulnerabilities', () => { it('should render the vulnerabilities', () => {
...@@ -165,14 +173,14 @@ describe('First class Project Security Dashboard component', () => { ...@@ -165,14 +173,14 @@ describe('First class Project Security Dashboard component', () => {
describe('with filter data', () => { describe('with filter data', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent(
props: { {
hasVulnerabilities: true, props: {
}, hasVulnerabilities: true,
data() { },
return { filters };
}, },
}); { filters },
);
}); });
it('should pass the filter data down to the vulnerabilities', () => { 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 { 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';
...@@ -18,12 +20,25 @@ describe('Project Pipeline Status Component', () => { ...@@ -18,12 +20,25 @@ describe('Project Pipeline Status Component', () => {
const findPipelineStatusBadge = () => wrapper.find(PipelineStatusBadge); const findPipelineStatusBadge = () => wrapper.find(PipelineStatusBadge);
const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip); const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip);
const findLink = () => wrapper.find(GlLink); const findLink = () => wrapper.find(GlLink);
const findAutoFixMrsLink = () => wrapper.findByTestId('auto-fix-mrs-link');
const createWrapper = ({ props = {}, options = {} } = {}) => { const createWrapper = (options = {}) => {
return shallowMount(ProjectPipelineStatus, { return extendedWrapper(
propsData: { ...DEFAULT_PROPS, ...props }, shallowMount(
...options, ProjectPipelineStatus,
}); merge(
{},
{
propsData: DEFAULT_PROPS,
provide: {
glFeatures: { securityAutoFix: true },
autoFixMrsPath: '/merge_requests?label_name=GitLab-auto-fix',
},
},
options,
),
),
);
}; };
afterEach(() => { afterEach(() => {
...@@ -56,7 +71,7 @@ describe('Project Pipeline Status Component', () => { ...@@ -56,7 +71,7 @@ describe('Project Pipeline Status Component', () => {
describe('when no pipeline has run', () => { describe('when no pipeline has run', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({ props: { pipeline: { path: '' } } }); wrapper = createWrapper({ propsData: { pipeline: { path: '' } } });
}); });
it('should not show the project_pipeline_status component', () => { it('should not show the project_pipeline_status component', () => {
...@@ -65,4 +80,36 @@ describe('Project Pipeline Status Component', () => { ...@@ -65,4 +80,36 @@ describe('Project Pipeline Status Component', () => {
expect(findPipelineStatusBadge().exists()).toBe(false); 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 ...@@ -155,7 +155,8 @@ RSpec.describe ProjectsHelper do
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index', 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'), 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", 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 end
......
...@@ -3931,6 +3931,12 @@ msgstr "" ...@@ -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." msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found."
msgstr "" 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." 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 "" 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