Commit 842e902d authored by Savas Vedova's avatar Savas Vedova

Merge branch '337805-wrap-vulnerability-report-in-tabs' into 'master'

Update non-pipeline vulnerability reports

See merge request gitlab-org/gitlab!70732
parents acfa4824 831bfe8f
---
name: operational_vulnerabilities
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70732
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341423
milestone: '14.4'
type: development
group: group::container security
default_enabled: false
......@@ -39,9 +39,6 @@ export default {
},
},
i18n: {
title: __(
'The Vulnerability Report shows the results of the last successful pipeline run on the default branch.',
),
lastUpdated: __('Last updated'),
autoFixSolutions: s__('AutoRemediation|Auto-fix solutions'),
autoFixMrsLink: s__('AutoRemediation|%{mrsCount} ready for review'),
......@@ -51,7 +48,6 @@ export default {
<template>
<div v-if="shouldShowPipelineStatus">
<h6 class="gl-font-weight-normal">{{ $options.i18n.title }}</h6>
<div
class="gl-display-flex gl-align-items-center gl-border-solid gl-border-1 gl-border-gray-100 gl-p-6"
>
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { PortalTarget } from 'portal-vue';
import groupProjectsQuery from 'ee/security_dashboard/graphql/queries/group_projects.query.graphql';
......@@ -38,7 +38,9 @@ export default {
DashboardNotConfiguredProject,
PortalTarget,
ProjectPipelineStatus,
GlLink,
GlLoadingIcon,
GlSprintf,
VulnerabilitiesCountList,
},
mixins: [glFeatureFlagsMixin()],
......@@ -51,6 +53,7 @@ export default {
dashboardType: {},
groupFullPath: { default: undefined },
autoFixDocumentation: { default: undefined },
dashboardDocumentation: { default: undefined },
pipeline: { default: undefined },
},
apollo: {
......@@ -120,6 +123,9 @@ export default {
autoFixUserCalloutCookieName: 'auto_fix_user_callout_dismissed',
i18n: {
title: s__('SecurityReports|Vulnerability Report'),
description: s__(
"SecurityReports|The Vulnerability Report shows the results of the lastest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}",
),
},
};
</script>
......@@ -143,12 +149,21 @@ export default {
<vulnerability-report-layout>
<template v-if="!isPipeline" #header>
<survey-request-banner class="gl-mt-5" />
<header class="gl-my-6 gl-display-flex gl-align-items-center">
<header class="gl-mt-6 gl-mb-3 gl-display-flex gl-align-items-center">
<h2 class="gl-flex-grow-1 gl-my-0">
{{ $options.i18n.title }}
</h2>
<csv-export-button />
</header>
<gl-sprintf :message="$options.i18n.description">
<template #link="{ content }">
<gl-link :href="dashboardDocumentation" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</template>
<template #summary>
<project-pipeline-status v-if="isProject" class="gl-mb-6" :pipeline="pipeline" />
<vulnerabilities-count-list :filters="filters" />
</template>
......
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
export default {
i18n: {
developmentTab: s__('SecurityReports|Development vulnerabilities'),
},
components: {
GlTabs,
GlTab,
},
mixins: [glFeatureFlagMixin()],
inject: ['dashboardType'],
computed: {
hasHeaderSlot() {
return Boolean(this.$slots.header);
......@@ -7,6 +21,18 @@ export default {
hasStickySlot() {
return Boolean(this.$slots.sticky);
},
hasSummarySlot() {
return Boolean(this.$slots.summary);
},
isProject() {
return this.dashboardType === DASHBOARD_TYPES.PROJECT;
},
shouldShowTabs() {
return (
this.dashboardType !== DASHBOARD_TYPES.PIPELINE &&
this.glFeatures.operationalVulnerabilities
);
},
},
};
</script>
......@@ -17,18 +43,54 @@ export default {
<slot name="header"></slot>
</header>
<section
v-if="hasStickySlot"
data-testid="sticky-section"
class="position-sticky gl-z-index-3 security-dashboard-filters"
<gl-tabs
v-if="shouldShowTabs"
:content-class="{ 'gl-pt-0': isProject, 'gl-pt-7': !isProject }"
nav-class="gl-mt-3"
>
<slot name="sticky"></slot>
</section>
<div class="row mt-4">
<article class="col">
<slot></slot>
</article>
</div>
<gl-tab>
<template #title>
<span>{{ $options.i18n.developmentTab }}</span>
</template>
<section v-if="hasSummarySlot" data-testid="summary-section">
<slot name="summary"></slot>
</section>
<section
v-if="hasStickySlot"
data-testid="sticky-section"
class="position-sticky gl-z-index-3 security-dashboard-filters"
>
<slot name="sticky"></slot>
</section>
<div class="row mt-4">
<article class="col">
<slot></slot>
</article>
</div>
</gl-tab>
</gl-tabs>
<template v-else>
<section v-if="hasSummarySlot" data-testid="summary-section" class="gl-pt-7">
<slot name="summary"></slot>
</section>
<section
v-if="hasStickySlot"
data-testid="sticky-section"
class="position-sticky gl-z-index-3 security-dashboard-filters"
>
<slot name="sticky"></slot>
</section>
<div class="row mt-4">
<article class="col">
<slot></slot>
</article>
</div>
</template>
</section>
</template>
......@@ -8,6 +8,7 @@ module Groups
before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:vuln_report_new_project_filter, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:operational_vulnerabilities, @project, default_enabled: :yaml)
end
feature_category :vulnerability_management
......
......@@ -8,6 +8,7 @@ module Projects
before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:operational_vulnerabilities, @project, default_enabled: :yaml)
end
feature_category :vulnerability_management
......
......@@ -7,6 +7,7 @@ module Security
before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:vuln_report_new_project_filter, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:operational_vulnerabilities, @project, default_enabled: :yaml)
end
end
end
import { mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import Filters from 'ee/security_dashboard/components/pipeline/filters.vue';
import LoadingError from 'ee/security_dashboard/components/pipeline/loading_error.vue';
import SecurityDashboardTable from 'ee/security_dashboard/components/pipeline/security_dashboard_table.vue';
import SecurityDashboard from 'ee/security_dashboard/components/pipeline/security_dashboard_vuex.vue';
......@@ -45,7 +44,7 @@ describe('Security Dashboard component', () => {
}),
);
wrapper = mount(SecurityDashboard, {
wrapper = shallowMount(SecurityDashboard, {
store,
propsData: {
dashboardDocumentation: '',
......@@ -73,10 +72,6 @@ describe('Security Dashboard component', () => {
createComponent();
});
it('renders the filters', () => {
expect(wrapper.find(Filters).exists()).toBe(true);
});
it('renders the security dashboard table ', () => {
expect(wrapper.find(SecurityDashboardTable).exists()).toBe(true);
});
......
import { GlLink } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import PipelineStatusBadge from 'ee/security_dashboard/components/shared/pipeline_status_badge.vue';
......@@ -56,15 +55,6 @@ describe('Project Pipeline Status Component', () => {
wrapper = createWrapper();
});
it('should display the help message properly', () => {
expect(
within(wrapper.element).getByRole('heading', {
name:
'The Vulnerability Report shows the results of the last successful pipeline run on the default branch.',
}),
).not.toBe(null);
});
it('should show the timeAgoTooltip component', () => {
const TimeComponent = findTimeAgoTooltip();
expect(TimeComponent.exists()).toBeTruthy();
......
import { shallowMount } from '@vue/test-utils';
import { GlTabs } from '@gitlab/ui';
import VulnerabilityReportLayout from 'ee/security_dashboard/components/shared/vulnerability_report_layout.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
describe('Vulnerability Report Layout component', () => {
let wrapper;
const SMALLER_SECTION_CLASS = 'col-xl-7';
const STICKY_SECTION_SELECTOR = '[data-testid="sticky-section"]';
const DummyComponent = {
name: 'dummy-component',
template: '<p>dummy component</p>',
};
const createWrapper = (slots) => {
wrapper = shallowMount(VulnerabilityReportLayout, { slots });
const createWrapper = ({ slots, provide } = {}) => {
wrapper = shallowMountExtended(VulnerabilityReportLayout, {
slots,
provide: {
dashboardType: DASHBOARD_TYPES.PROJECT,
glFeatures: { operationalVulnerabilities: false },
...provide,
},
});
};
const findArticle = () => wrapper.find('article');
const findHeader = () => wrapper.find('header');
const findStickySection = () => wrapper.find(STICKY_SECTION_SELECTOR);
const findStickySection = () => wrapper.findByTestId('sticky-section');
const findSummarySection = () => wrapper.findByTestId('summary-section');
const findTabs = () => wrapper.find(GlTabs);
afterEach(() => {
wrapper.destroy();
});
describe('with the main slot only', () => {
beforeEach(() => {
createWrapper({
default: DummyComponent,
});
});
it.each`
element | exists
${'article'} | ${true}
${'header'} | ${false}
${STICKY_SECTION_SELECTOR} | ${false}
`('should find that $element exists is $exists', ({ element, exists }) => {
expect(wrapper.find(element).exists()).toBe(exists);
});
it('should render the dummy component in the main section', () => {
const article = wrapper.find('article');
describe('template slots', () => {
it('should not render any slot contents by default', () => {
createWrapper();
expect(article.find(DummyComponent).exists()).toBe(true);
expect(findHeader().exists()).toBe(false);
expect(findStickySection().exists()).toBe(false);
expect(findSummarySection().exists()).toBe(false);
expect(findArticle().classes()).not.toContain(SMALLER_SECTION_CLASS);
});
it('should not make the main section smaller', () => {
const article = wrapper.find('article');
expect(article.classes()).not.toContain(SMALLER_SECTION_CLASS);
});
});
describe('with the header and main slots', () => {
beforeEach(() => {
it.each`
slotName | findFn
${'default'} | ${findArticle}
${'header'} | ${findHeader}
${'sticky'} | ${findStickySection}
${'summary'} | ${findSummarySection}
`('should render the template contents in the correct slot', ({ slotName, findFn }) => {
createWrapper({
default: DummyComponent,
header: DummyComponent,
slots: {
[slotName]: DummyComponent,
},
});
});
it.each`
element | exists
${'article'} | ${true}
${'header'} | ${true}
${STICKY_SECTION_SELECTOR} | ${false}
`('should find that $element exists is $exists', ({ element, exists }) => {
expect(wrapper.find(element).exists()).toBe(exists);
});
it('should render the dummy component in the main section', () => {
const article = findArticle();
expect(article.find(DummyComponent).exists()).toBe(true);
});
it('should render the dummy component in the header section', () => {
const header = findHeader();
expect(header.find(DummyComponent).exists()).toBe(true);
expect(findFn().find(DummyComponent).exists()).toBe(true);
});
});
describe('with the sticky section and main slots', () => {
describe('with the "operationalVulnerabilities" feature flag on', () => {
beforeEach(() => {
createWrapper({
default: DummyComponent,
sticky: DummyComponent,
slots: {
default: DummyComponent,
header: DummyComponent,
sticky: DummyComponent,
summary: DummyComponent,
},
provide: {
glFeatures: { operationalVulnerabilities: true },
},
});
});
it.each`
element | exists
${'article'} | ${true}
${'header'} | ${false}
${STICKY_SECTION_SELECTOR} | ${true}
`('should find that $element exists is $exists', ({ element, exists }) => {
expect(wrapper.find(element).exists()).toBe(exists);
element | findFn | exists
${'main'} | ${findArticle} | ${true}
${'header'} | ${findHeader} | ${true}
${'sticky section'} | ${findStickySection} | ${true}
${'summary section'} | ${findSummarySection} | ${true}
`('should find that $element exists is $exists', ({ exists, findFn }) => {
expect(findFn().find(DummyComponent).exists()).toBe(exists);
});
});
it('should render the dummy component in the main section', () => {
const article = findArticle();
expect(article.find(DummyComponent).exists()).toBe(true);
describe('tabs', () => {
const createOptions = (dashboardType, operationalVulnerabilities) => ({
provide: { dashboardType, glFeatures: { operationalVulnerabilities } },
});
it('should render the dummy component in the sticky section', () => {
const section = findStickySection();
expect(section.find(DummyComponent).exists()).toBe(true);
it.each`
test | wrapperOptions | exists
${'should not find the tabs for the pipeline-level report with the feature flag off'} | ${createOptions(DASHBOARD_TYPES.PIPELINE, false)} | ${false}
${'should not find the tabs for the pipeline-level report with the feature flag on'} | ${createOptions(DASHBOARD_TYPES.PIPELINE, true)} | ${false}
${'should not find the tabs for the project-level report with the feature flag off'} | ${createOptions(DASHBOARD_TYPES.PROJECT, false)} | ${false}
${'should find the tabs for the project-level report with the feature flag on'} | ${createOptions(DASHBOARD_TYPES.PROJECT, true)} | ${true}
${'should not find the tabs for the group-level report with the feature flag off'} | ${createOptions(DASHBOARD_TYPES.GROUP, false)} | ${false}
${'should find the tabs for the group-level report with the feature flag on'} | ${createOptions(DASHBOARD_TYPES.GROUP, true)} | ${true}
${'should not find the tabs for the instance-level report with the feature flag off'} | ${createOptions(DASHBOARD_TYPES.INSTANCE, false)} | ${false}
${'should find the tabs for the instance-level report with the feature flag on'} | ${createOptions(DASHBOARD_TYPES.INSTANCE, true)} | ${true}
`('$test', ({ exists, wrapperOptions }) => {
createWrapper(wrapperOptions);
expect(findTabs().exists()).toBe(exists);
});
});
});
......@@ -30253,6 +30253,9 @@ msgstr ""
msgid "SecurityReports|Create issue"
msgstr ""
msgid "SecurityReports|Development vulnerabilities"
msgstr ""
msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
......@@ -30394,6 +30397,9 @@ msgstr ""
msgid "SecurityReports|Take survey"
msgstr ""
msgid "SecurityReports|The Vulnerability Report shows the results of the lastest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgstr ""
......@@ -33669,9 +33675,6 @@ msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr ""
msgid "The Vulnerability Report shows the results of the last successful pipeline run on the default branch."
msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
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