Commit 39029393 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Clement Ho

Align group and project security dashboards UX

- Updated the project security dashboard so it uses group-level
  security dashboard component
- Updated project_security_dashboard_config helper to get
  required data
- Updated security dashboard component to support locked filters
- In project security dashboard: locked project filter on current
  project
- Removed obsolete code
parent 331e3c84
import Vue from 'vue';
import createStore from 'ee/vue_shared/security_reports/store';
import createStore from 'ee/security_dashboard/store';
import SecurityReportApp from 'ee/vue_shared/security_reports/card_security_reports_app.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
......@@ -18,16 +18,60 @@ document.addEventListener('DOMContentLoaded', () => {
refId,
refPath,
pipelineId,
createVulnerabilityFeedbackIssuePath,
createVulnerabilityFeedbackMergeRequestPath,
createVulnerabilityFeedbackDismissalPath,
...rest
projectId,
projectName,
dashboardDocumentation,
emptyStateSvgPath,
vulnerabilitiesEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilitiesSummaryEndpoint,
vulnerabilityFeedbackHelpPath,
} = securityTab.dataset;
const parsedPipelineId = parseInt(pipelineId, 10);
const parsedHasPipelineData = parseBoolean(hasPipelineData);
const store = createStore();
let props = {
hasPipelineData: parsedHasPipelineData,
dashboardDocumentation,
emptyStateSvgPath,
vulnerabilitiesEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilitiesSummaryEndpoint,
vulnerabilityFeedbackHelpPath,
securityDashboardHelpPath: dashboardDocumentation,
emptyStateIllustrationPath: emptyStateSvgPath,
};
if (parsedHasPipelineData) {
props = {
...props,
project: {
id: projectId,
name: projectName,
},
triggeredBy: {
avatarPath: userAvatarPath,
name: userName,
path: userPath,
},
pipeline: {
id: parsedPipelineId,
created: pipelineCreated,
path: pipelinePath,
},
commit: {
id: commitId,
path: commitPath,
},
branch: {
id: refId,
path: refPath,
},
};
}
return new Vue({
el: securityTab,
store,
......@@ -37,35 +81,7 @@ document.addEventListener('DOMContentLoaded', () => {
methods: {},
render(createElement) {
return createElement('security-report-app', {
props: {
pipelineId: parsedPipelineId,
hasPipelineData: parseBoolean(hasPipelineData),
canCreateIssue: Boolean(createVulnerabilityFeedbackIssuePath),
canCreateMergeRequest: Boolean(createVulnerabilityFeedbackMergeRequestPath),
canDismissVulnerability: Boolean(createVulnerabilityFeedbackDismissalPath),
createVulnerabilityFeedbackIssuePath,
createVulnerabilityFeedbackMergeRequestPath,
createVulnerabilityFeedbackDismissalPath,
triggeredBy: {
avatarPath: userAvatarPath,
name: userName,
path: userPath,
},
pipeline: {
id: parsedPipelineId,
created: pipelineCreated,
path: pipelinePath,
},
commit: {
id: commitId,
path: commitPath,
},
branch: {
id: refId,
path: refPath,
},
...rest,
},
props,
});
},
});
......
<script>
import { isUndefined } from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import Filters from './filters.vue';
......@@ -26,7 +27,8 @@ export default {
},
projectsEndpoint: {
type: String,
required: true,
required: false,
default: null,
},
vulnerabilitiesEndpoint: {
type: String,
......@@ -44,6 +46,12 @@ export default {
type: String,
required: true,
},
lockToProject: {
type: Object,
required: false,
default: null,
validator: project => !isUndefined(project.id) && !isUndefined(project.name),
},
},
computed: {
...mapState('vulnerabilities', ['modal', 'pageInfo']),
......@@ -64,8 +72,17 @@ export default {
vulnerability() {
return this.modal.vulnerability;
},
isLockedToProject() {
return this.lockToProject !== null;
},
},
created() {
if (this.isLockedToProject) {
this.lockFilter({
filterId: 'project_id',
optionId: this.lockToProject.id,
});
}
this.setProjectsEndpoint(this.projectsEndpoint);
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
......@@ -73,7 +90,9 @@ export default {
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page });
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
this.fetchProjects();
if (!this.isLockedToProject) {
this.fetchProjects();
}
},
methods: {
...mapActions('vulnerabilities', [
......@@ -91,6 +110,7 @@ export default {
'undoDismiss',
]),
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
...mapActions('filters', ['lockFilter']),
},
};
</script>
......@@ -98,9 +118,11 @@ export default {
<template>
<div>
<filters />
<vulnerability-count-list />
<vulnerability-count-list :class="{ 'mb-0': isLockedToProject }" />
<vulnerability-chart v-if="!isLockedToProject" />
<vulnerability-chart />
<h4 v-if="!isLockedToProject" class="my-4">{{ __('Vulnerability List') }}</h4>
<security-dashboard-table
:dashboard-documentation="dashboardDocumentation"
......
<script>
import { GlButton } from '@gitlab/ui';
export default {
name: 'EmptyState',
components: {
GlButton,
},
props: {
svgPath: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="empty-state row">
<div class="col-12">
<div class="svg-content svg-250">
<img :src="svgPath" :alt="s__('Security Reports|No Vulnerabilities')" />
</div>
<div class="text-content">
<h4>{{ s__("Security Reports|We've found no vulnerabilities for your group") }}</h4>
<p>
{{
s__(
"Security Reports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly.",
)
}}
</p>
<div class="text-center">
<gl-button variant="success" :href="link">{{
s__('Security Reports|Learn more about setting up your dashboard')
}}</gl-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { mapGetters } from 'vuex';
import DashboardFilter from './filter.vue';
export default {
......@@ -7,7 +7,9 @@ export default {
DashboardFilter,
},
computed: {
...mapState('filters', ['filters']),
...mapGetters({
filters: 'filters/visibleFilters',
}),
},
};
</script>
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import Pagination from '~/vue_shared/components/pagination_links.vue';
import SecurityDashboardTableRow from './security_dashboard_table_row.vue';
import EmptyState from './empty_state.vue';
export default {
name: 'SecurityDashboardTable',
components: {
EmptyState,
GlEmptyState,
Pagination,
SecurityDashboardTableRow,
},
......@@ -54,8 +54,6 @@ export default {
<template>
<div class="ci-table">
<h4 class="my-4">{{ __('Vulnerability List') }}</h4>
<div
class="gl-responsive-table-row table-row-header vulnerabilities-row-header px-2"
role="row"
......@@ -94,10 +92,17 @@ export default {
@openModal="openModal({ vulnerability })"
/>
<empty-state
<gl-empty-state
v-if="showEmptyState"
:title="s__(`Security Reports|We've found no vulnerabilities for your group`)"
:svg-path="emptyStateSvgPath"
:link="dashboardDocumentation"
:description="
s__(
`Security Reports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly.`,
)
"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')"
/>
<pagination
......
......@@ -18,6 +18,11 @@ export const setAllFilters = ({ commit }, payload) => {
commit(types.SET_ALL_FILTERS, payload);
};
export const lockFilter = ({ commit }, payload) => {
commit(types.SET_FILTER, payload);
commit(types.HIDE_FILTER, payload);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged
export default () => {};
......@@ -32,6 +32,8 @@ export const activeFilters = state =>
return acc;
}, {});
export const visibleFilters = ({ filters }) => filters.filter(({ hidden }) => !hidden);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged
export default () => {};
export const SET_FILTER = 'SET_FILTER';
export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS';
export const SET_ALL_FILTERS = 'SET_ALL_FILTERS';
export const HIDE_FILTER = 'HIDE_FILTER';
......@@ -47,4 +47,10 @@ export default {
const { filterId, options } = payload;
state.filters.find(filter => filter.id === filterId).options = options;
},
[types.HIDE_FILTER](state, { filterId }) {
const hiddenFilter = state.filters.find(({ id }) => id === filterId);
if (hiddenFilter) {
hiddenFilter.hidden = true;
}
},
};
......@@ -9,24 +9,28 @@ export default () => ({
name: s__('SecurityDashboard|Severity'),
id: 'severity',
options: [BASE_FILTERS.severity, ...optionsObjectToArray(SEVERITY_LEVELS)],
hidden: false,
selection: new Set(['all']),
},
{
name: s__('SecurityDashboard|Confidence'),
id: 'confidence',
options: [BASE_FILTERS.confidence, ...optionsObjectToArray(CONFIDENCE_LEVELS)],
hidden: false,
selection: new Set(['all']),
},
{
name: s__('SecurityDashboard|Report type'),
id: 'report_type',
options: [BASE_FILTERS.report_type, ...optionsObjectToArray(REPORT_TYPES)],
hidden: false,
selection: new Set(['all']),
},
{
name: s__('SecurityDashboard|Project'),
id: 'project_id',
options: [BASE_FILTERS.project_id],
hidden: false,
selection: new Set(['all']),
},
],
......
<script>
import { isUndefined } from 'underscore';
import { s__, sprintf } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import EmptySecurityDashboard from './components/empty_security_dashboard.vue';
import SplitSecurityReport from './split_security_reports_app.vue';
import SecurityDashboardApp from 'ee/security_dashboard/components/app.vue';
export default {
components: {
EmptySecurityDashboard,
GlEmptyState,
UserAvatarLink,
Icon,
SplitSecurityReport,
TimeagoTooltip,
SecurityDashboardApp,
},
props: {
hasPipelineData: {
......@@ -30,71 +31,6 @@ export default {
required: false,
default: null,
},
alwaysOpen: {
type: Boolean,
required: false,
default: false,
},
headBlobPath: {
type: String,
required: false,
default: null,
},
sastHeadPath: {
type: String,
required: false,
default: null,
},
dastHeadPath: {
type: String,
required: false,
default: null,
},
sastContainerHeadPath: {
type: String,
required: false,
default: null,
},
dependencyScanningHeadPath: {
type: String,
required: false,
default: null,
},
sastHelpPath: {
type: String,
required: false,
default: null,
},
sastContainerHelpPath: {
type: String,
required: false,
default: '',
},
dastHelpPath: {
type: String,
required: false,
default: '',
},
dependencyScanningHelpPath: {
type: String,
required: false,
default: null,
},
vulnerabilityFeedbackPath: {
type: String,
required: false,
default: '',
},
vulnerabilityFeedbackHelpPath: {
type: String,
required: false,
default: '',
},
pipelineId: {
type: Number,
required: false,
default: null,
},
commit: {
type: Object,
required: false,
......@@ -115,32 +51,40 @@ export default {
required: false,
default: () => ({}),
},
canCreateIssue: {
type: Boolean,
project: {
type: Object,
required: true,
validator: project => !isUndefined(project.id) && !isUndefined(project.name),
},
canCreateMergeRequest: {
type: Boolean,
required: true,
dashboardDocumentation: {
type: String,
required: false,
default: null,
},
canDismissVulnerability: {
type: Boolean,
required: true,
emptyStateSvgPath: {
type: String,
required: false,
default: null,
},
createVulnerabilityFeedbackIssuePath: {
vulnerabilityFeedbackHelpPath: {
type: String,
required: false,
default: '',
default: null,
},
vulnerabilitiesEndpoint: {
type: String,
required: false,
default: null,
},
createVulnerabilityFeedbackMergeRequestPath: {
vulnerabilitiesSummaryEndpoint: {
type: String,
required: false,
default: '',
default: null,
},
createVulnerabilityFeedbackDismissalPath: {
vulnerabilitiesHistoryEndpoint: {
type: String,
required: false,
default: '',
default: null,
},
},
computed: {
......@@ -153,60 +97,61 @@ export default {
false,
);
},
emptyStateDescription() {
return s__(
`SecurityDashboard|
The security dashboard displays the latest security report.
Use it to find and fix vulnerabilities.`,
).trim();
},
},
};
</script>
<template>
<div>
<div v-if="hasPipelineData" class="card security-dashboard prepend-top-default">
<div class="card-header">
<span class="js-security-dashboard-left">
<span v-html="headline"></span>
<timeago-tooltip :time="pipeline.created" />
{{ __('by') }}
<user-avatar-link
:link-href="triggeredBy.path"
:img-src="triggeredBy.avatarPath"
:img-alt="triggeredBy.name"
:img-size="24"
:username="triggeredBy.name"
class="avatar-image-container"
/>
</span>
<span class="js-security-dashboard-right pull-right">
<icon name="branch" /> <a :href="branch.path" class="monospace">{{ branch.id }}</a>
<span class="text-muted prepend-left-5 append-right-5">&middot;</span>
<icon name="commit" /> <a :href="commit.path" class="monospace">{{ commit.id }}</a>
</span>
<template v-if="hasPipelineData">
<div class="card security-dashboard prepend-top-default">
<div class="card-header border-bottom-0">
<span class="js-security-dashboard-left">
<span v-html="headline"></span>
<timeago-tooltip :time="pipeline.created" />
{{ __('by') }}
<user-avatar-link
:link-href="triggeredBy.path"
:img-src="triggeredBy.avatarPath"
:img-alt="triggeredBy.name"
:img-size="24"
:username="triggeredBy.name"
class="avatar-image-container"
/>
</span>
<span class="js-security-dashboard-right pull-right">
<icon name="branch" />
<a :href="branch.path" class="monospace">{{ branch.id }}</a>
<span class="text-muted prepend-left-5 append-right-5">&middot;</span>
<icon name="commit" />
<a :href="commit.path" class="monospace">{{ commit.id }}</a>
</span>
</div>
</div>
<split-security-report
:pipeline-id="pipelineId"
:head-blob-path="headBlobPath"
:sast-head-path="sastHeadPath"
:dast-head-path="dastHeadPath"
:sast-container-head-path="sastContainerHeadPath"
:dependency-scanning-head-path="dependencyScanningHeadPath"
:sast-help-path="sastHelpPath"
:sast-container-help-path="sastContainerHelpPath"
:dast-help-path="dastHelpPath"
:dependency-scanning-help-path="dependencyScanningHelpPath"
:vulnerability-feedback-path="vulnerabilityFeedbackPath"
<h4 class="mt-4 mb-3">{{ __('Vulnerabilities') }}</h4>
<security-dashboard-app
:lock-to-project="project"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesSummaryEndpoint"
:vulnerabilities-history-endpoint="vulnerabilitiesHistoryEndpoint"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
:create-vulnerability-feedback-issue-path="createVulnerabilityFeedbackIssuePath"
:create-vulnerability-feedback-merge-request-path="
createVulnerabilityFeedbackMergeRequestPath
"
:create-vulnerability-feedback-dismissal-path="createVulnerabilityFeedbackDismissalPath"
:can-create-issue="canCreateIssue"
:can-create-merge-request="canCreateMergeRequest"
:can-dismiss-vulnerability="canDismissVulnerability"
always-open
/>
</div>
<empty-security-dashboard
</template>
<gl-empty-state
v-else
:help-path="securityDashboardHelpPath"
:illustration-path="emptyStateIllustrationPath"
:title="s__('SecurityDashboard|Monitor vulnerabilities in your code')"
:svg-path="emptyStateIllustrationPath"
:description="emptyStateDescription"
:primary-button-link="securityDashboardHelpPath"
:primary-button-text="__('Learn more')"
/>
</div>
</template>
<script>
import { s__ } from '~/locale';
export default {
props: {
illustrationPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
},
computed: {
paragraphText: () =>
s__(
`SecurityDashboard|
The security dashboard displays the latest security report.
Use it to find and fix vulnerabilities.`,
),
},
};
</script>
<template>
<div class="row empty-state">
<div class="col-12">
<div class="svg-content"><img :src="illustrationPath" /></div>
</div>
<div class="col-12">
<div class="text-content text-center">
<h4>{{ s__('SecurityDashboard|Monitor vulnerabilities in your code') }}</h4>
<p>{{ paragraphText }}</p>
<a :href="helpPath" class="btn btn-success" rel="nofollow"> {{ __('Learn more') }} </a>
</div>
</div>
</div>
</template>
......@@ -161,18 +161,14 @@ module EE
}
else
{
head_blob_path: project_blob_path(project, pipeline.sha),
sast_head_path: pipeline.downloadable_path_for_report_type(:sast),
dependency_scanning_head_path: pipeline.downloadable_path_for_report_type(:dependency_scanning),
dast_head_path: pipeline.downloadable_path_for_report_type(:dast),
sast_container_head_path: pipeline.downloadable_path_for_report_type(:container_scanning),
vulnerability_feedback_path: project_vulnerability_feedback_index_path(project),
project: { id: project.id, name: project.name },
vulnerabilities_endpoint: group_security_vulnerabilities_path(project.group),
vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(project.group),
vulnerabilities_history_endpoint: history_group_security_vulnerabilities_path(project.group),
vulnerability_feedback_help_path: help_page_path("user/application_security/index", anchor: "interacting-with-the-vulnerabilities"),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
pipeline_id: pipeline.id,
vulnerability_feedback_help_path: help_page_path('user/application_security/index'),
sast_help_path: help_page_path('user/application_security/sast/index'),
dependency_scanning_help_path: help_page_path('user/application_security/dependency_scanning/index'),
dast_help_path: help_page_path('user/application_security/dast/index'),
sast_container_help_path: help_page_path('user/application_security/container_scanning/index'),
user_path: user_url(pipeline.user),
user_avatar_path: pipeline.user.avatar_url,
user_name: pipeline.user.name,
......@@ -181,11 +177,8 @@ module EE
ref_id: pipeline.ref,
ref_path: project_commits_url(project, pipeline.ref),
pipeline_path: pipeline_url(pipeline),
pipeline_created: pipeline.created_at.to_s,
has_pipeline_data: "true",
create_vulnerability_feedback_issue_path: create_vulnerability_feedback_issue_path(project),
create_vulnerability_feedback_merge_request_path: create_vulnerability_feedback_merge_request_path(project),
create_vulnerability_feedback_dismissal_path: create_vulnerability_feedback_dismissal_path(project)
pipeline_created: pipeline.created_at.to_s(:iso8601),
has_pipeline_data: "true"
}
end
end
......
---
title: Align group and project level security dashboard UX
merge_request: 13180
author:
type: changed
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import SecurityDashboardApp from 'ee/security_dashboard/components/app.vue';
import Filters from 'ee/security_dashboard/components/filters.vue';
import SecurityDashboardTable from 'ee/security_dashboard/components/security_dashboard_table.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/vulnerability_chart.vue';
import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import createStore from 'ee/security_dashboard/store';
const localVue = createLocalVue();
const projectsEndpoint = `${TEST_HOST}/projects`;
const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`;
const vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilities_summary`;
const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`;
describe('Card security reports app', () => {
let wrapper;
let mock;
let fetchProjectsSpy;
let lockFilterSpy;
const setup = () => {
mock = new MockAdapter(axios);
fetchProjectsSpy = jest.fn();
lockFilterSpy = jest.fn();
};
const createComponent = props => {
wrapper = shallowMount(SecurityDashboardApp, {
localVue,
store: createStore(),
sync: false,
methods: {
lockFilter: lockFilterSpy,
fetchProjects: fetchProjectsSpy,
},
propsData: {
dashboardDocumentation: '',
emptyStateSvgPath: '',
projectsEndpoint,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerabilities_feedback_help`,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('default', () => {
beforeEach(() => {
setup();
createComponent();
});
it('render sub components', () => {
expect(wrapper.find(Filters).exists()).toBe(true);
expect(wrapper.find(SecurityDashboardTable).exists()).toBe(true);
expect(wrapper.find(VulnerabilityChart).exists()).toBe(true);
expect(wrapper.find(VulnerabilityCountList).exists()).toBe(true);
});
it('fetches projects and does not lock projects filter', () => {
expect(wrapper.vm.isLockedToProject).toBe(false);
expect(fetchProjectsSpy).toHaveBeenCalled();
expect(lockFilterSpy).not.toHaveBeenCalled();
});
});
describe('with project lock', () => {
const project = {
id: 123,
name: 'my-project',
};
beforeEach(() => {
setup();
createComponent({
lockToProject: project,
});
});
it('locks to given project and does not fetch projects', () => {
expect(wrapper.vm.isLockedToProject).toBe(true);
expect(fetchProjectsSpy).not.toHaveBeenCalled();
expect(lockFilterSpy).toHaveBeenCalledWith({
filterId: 'project_id',
optionId: project.id,
});
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Card security reports app Empty State renders correctly renders empty state component with correct props 1`] = `
Object {
"description": "The security dashboard displays the latest security report. Use it to find and fix vulnerabilities.",
"primaryButtonLink": "http://test.host/help_dashboard",
"primaryButtonText": "Learn more",
"secondaryButtonLink": null,
"secondaryButtonText": null,
"svgPath": "http://test.host/img",
"title": "Monitor vulnerabilities in your code",
}
`;
exports[`Card security reports app Headline renders renders branch and commit information 1`] = `"<span class=\\"js-security-dashboard-right pull-right\\"><svg aria-hidden=\\"true\\" class=\\"s16 ic-branch\\"><use xlink:href=\\"undefined#branch\\"></use></svg> <a href=\\"http://test.host/branch\\" class=\\"monospace\\">master</a> <span class=\\"text-muted prepend-left-5 append-right-5\\">·</span> <svg aria-hidden=\\"true\\" class=\\"s16 ic-commit\\"><use xlink:href=\\"undefined#commit\\"></use></svg> <a href=\\"http://test.host/commit\\" class=\\"monospace\\">1234adf</a></span>"`;
import { mount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import CardSecurityDashboardApp from 'ee/vue_shared/security_reports/card_security_reports_app.vue';
import createStore from 'ee/security_dashboard/store';
import { trimText } from 'helpers/text_helper';
const localVue = createLocalVue();
const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`;
const vulnerabilitiesSummaryEndpoint = `${TEST_HOST}/vulnerabilities_summary`;
const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`;
describe('Card security reports app', () => {
let wrapper;
let mock;
const runDate = new Date();
runDate.setDate(runDate.getDate() - 7);
const createComponent = props => {
wrapper = mount(CardSecurityDashboardApp, {
localVue,
store: createStore(),
sync: false,
stubs: ['security-dashboard-table'],
propsData: {
hasPipelineData: true,
emptyStateIllustrationPath: `${TEST_HOST}/img`,
securityDashboardHelpPath: `${TEST_HOST}/help_dashboard`,
commit: {
id: '1234adf',
path: `${TEST_HOST}/commit`,
},
branch: {
id: 'master',
path: `${TEST_HOST}/branch`,
},
pipeline: {
id: '55',
created: runDate.toISOString(),
path: `${TEST_HOST}/pipeline`,
},
triggeredBy: {
path: `${TEST_HOST}/user`,
avatarPath: `${TEST_HOST}/img`,
name: 'TestUser',
},
project: {
id: 123,
name: 'my-project',
},
dashboardDocumentation: `${TEST_HOST}/dashboard_documentation`,
emptyStateSvgPath: `/empty_state.svg`,
vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerability_feedback_help`,
vulnerabilitiesEndpoint,
vulnerabilitiesSummaryEndpoint,
vulnerabilitiesHistoryEndpoint,
...props,
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
createComponent();
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('computed properties', () => {
describe('headline', () => {
it('renders `Pipeline <link> triggered`', () => {
expect(wrapper.vm.headline).toBe(
`Pipeline <a href="${TEST_HOST}/pipeline">#55</a> triggered`,
);
});
});
});
describe('Headline renders', () => {
it('renders pipeline metadata information', () => {
const element = wrapper.find('.card-header .js-security-dashboard-left');
expect(trimText(element.text())).toBe('Pipeline #55 triggered 1 week ago by TestUser');
const pipelineLink = element.find(`a[href="${TEST_HOST}/pipeline"]`);
expect(pipelineLink).not.toBeNull();
expect(pipelineLink.text()).toBe('#55');
const userAvatarLink = element.find('a.user-avatar-link');
expect(userAvatarLink).not.toBeNull();
expect(userAvatarLink.attributes('href')).toBe(`${TEST_HOST}/user`);
expect(userAvatarLink.find('img').attributes('src')).toBe(`${TEST_HOST}/img?width=24`);
expect(userAvatarLink.text().trim()).toBe('TestUser');
});
it('renders branch and commit information', () => {
const revInformation = wrapper.find('.card-header .js-security-dashboard-right');
expect(revInformation.html()).toMatchSnapshot();
});
});
describe('Dashboard renders properly', () => {
const findDashboard = () => wrapper.find(CardSecurityDashboardApp);
it('renders security dashboard', () => {
const dashboard = findDashboard();
expect(dashboard.exists()).toBe(true);
});
it('renders one filter less because projects filter is locked', () => {
const dashboard = findDashboard();
const filters = dashboard.findAll('.dashboard-filter');
expect(filters.length).toBe(wrapper.vm.$store.state.filters.filters.length - 1);
});
});
describe('Empty State renders correctly', () => {
beforeEach(() => {
createComponent({ hasPipelineData: false });
});
it('renders empty state component with correct props', () => {
const emptyState = wrapper.find(GlEmptyState);
expect(emptyState.exists()).toBe(true);
expect(emptyState.props()).toMatchSnapshot();
});
});
});
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'spec/test_constants';
import component from 'ee/vue_shared/security_reports/card_security_reports_app.vue';
import createStore from 'ee/vue_shared/security_reports/store';
import state from 'ee/vue_shared/security_reports/store/state';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { trimText } from 'spec/helpers/text_helper';
import { sastIssues, dast, dockerReport } from './mock_data';
describe('Card security reports app', () => {
const Component = Vue.extend(component);
let vm;
let mock;
const runDate = new Date();
runDate.setDate(runDate.getDate() - 7);
beforeEach(() => {
mock = new MockAdapter(axios);
vm = mountComponentWithStore(Component, {
store: createStore(),
props: {
hasPipelineData: true,
emptyStateIllustrationPath: `${TEST_HOST}/img`,
securityDashboardHelpPath: `${TEST_HOST}/help_dashboard`,
commit: {
id: '1234adf',
path: `${TEST_HOST}/commit`,
},
branch: {
id: 'master',
path: `${TEST_HOST}/branch`,
},
pipeline: {
id: '55',
created: runDate.toISOString(),
path: `${TEST_HOST}/pipeline`,
},
triggeredBy: {
path: `${TEST_HOST}/user`,
avatarPath: `${TEST_HOST}/img`,
name: 'TestUser',
},
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: `${TEST_HOST}/sast_head`,
dependencyScanningHeadPath: `${TEST_HOST}/dss_head`,
dastHeadPath: `${TEST_HOST}/dast_head`,
sastContainerHeadPath: `${TEST_HOST}/sast_container_head`,
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
vulnerabilityFeedbackPath: `${TEST_HOST}/vulnerability_feedback_path`,
createVulnerabilityFeedbackIssuePath: `${TEST_HOST}/vulnerability_feedback_path`,
createVulnerabilityFeedbackDismissalPath: `${TEST_HOST}/vulnerability_feedback_path`,
createVulnerabilityFeedbackMergeRequestPath: `${TEST_HOST}/vulnerability_feedback_path`,
vulnerabilityFeedbackHelpPath: 'path',
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateMergeRequest: true,
canDismissVulnerability: true,
},
});
});
afterEach(() => {
vm.$store.replaceState(state());
vm.$destroy();
mock.restore();
});
describe('computed properties', () => {
describe('headline', () => {
it('renders `Pipeline <link> triggered`', () => {
expect(vm.headline).toBe(`Pipeline <a href="${TEST_HOST}/pipeline">#55</a> triggered`);
});
});
});
describe('Headline renders', () => {
it('pipeline metadata information', () => {
const element = vm.$el.querySelector('.card-header .js-security-dashboard-left');
expect(trimText(element.textContent)).toBe('Pipeline #55 triggered 1 week ago by TestUser');
const pipelineLink = element.querySelector(`a[href="${TEST_HOST}/pipeline"]`);
expect(pipelineLink).not.toBeNull();
expect(pipelineLink.textContent).toBe('#55');
const userAvatarLink = element.querySelector('a.user-avatar-link');
expect(userAvatarLink).not.toBeNull();
expect(userAvatarLink.getAttribute('href')).toBe(`${TEST_HOST}/user`);
expect(userAvatarLink.querySelector('img').getAttribute('src')).toBe(
`${TEST_HOST}/img?width=24`,
);
expect(userAvatarLink.textContent.trim()).toBe('TestUser');
});
it('branch and commit information', () => {
const branchIcon = vm.$el.querySelector(
'.card-header .js-security-dashboard-right .ic-branch',
);
expect(branchIcon).not.toBeNull();
const branchLink = branchIcon.nextElementSibling;
expect(branchLink).not.toBeNull();
expect(branchLink.textContent).toBe('master');
expect(branchLink.getAttribute('href')).toBe(`${TEST_HOST}/branch`);
const middot = branchLink.nextElementSibling;
expect(middot).not.toBeNull();
expect(middot.textContent).toBe('·');
const commitIcon = middot.nextElementSibling;
expect(commitIcon).not.toBeNull();
expect(commitIcon.classList).toContain('ic-commit');
const commitLink = commitIcon.nextElementSibling;
expect(commitLink).not.toBeNull();
expect(commitLink.textContent).toContain('1234adf');
expect(commitLink.getAttribute('href')).toBe(`${TEST_HOST}/commit`);
});
});
describe('Empty State renders correctly', () => {
beforeEach(done => {
vm.hasPipelineData = false;
Vue.nextTick(done);
});
it('image illustration is set to defined path', () => {
const imgEl = vm.$el.querySelector('img');
expect(imgEl.getAttribute('src')).toBe(`${TEST_HOST}/img`);
});
it('headline text is to `Monitor vulnerabilities in your code`', () => {
const headingEl = vm.$el.querySelector('h4');
expect(headingEl.textContent.trim()).toBe('Monitor vulnerabilities in your code');
});
it('paragraph text is to `The security dashboard...`', () => {
const paragraphEl = vm.$el.querySelector('p');
expect(trimText(paragraphEl.textContent)).toBe(
'The security dashboard displays the latest security report. Use it to find and fix vulnerabilities.',
);
});
it('learn more link has correct path and text', () => {
const linkEl = vm.$el.querySelector('a');
expect(linkEl.textContent.trim()).toBe('Learn more');
expect(linkEl.getAttribute('href')).toBe(`${TEST_HOST}/help_dashboard`);
});
});
describe('Report renders correctly', () => {
describe('while loading', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/sast_head`).reply(200, sastIssues);
mock.onGet(`${TEST_HOST}/dss_head`).reply(200, sastIssues);
mock.onGet(`${TEST_HOST}/dast_head`).reply(200, dast);
mock.onGet(`${TEST_HOST}/sast_container_head`).reply(200, dockerReport);
mock.onGet(`${TEST_HOST}/vulnerability_feedback_path`).reply(200, []);
});
it('renders loading summary text + spinner', done => {
expect(vm.$el.querySelector('.spinner')).not.toBeNull();
expect(vm.$el.textContent).toContain('SAST is loading');
expect(vm.$el.textContent).toContain('Dependency scanning is loading');
expect(vm.$el.textContent).toContain('Container scanning is loading');
expect(vm.$el.textContent).toContain('DAST is loading');
setTimeout(() => {
done();
}, 0);
});
});
describe('with all reports', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/sast_head`).reply(200, sastIssues);
mock.onGet(`${TEST_HOST}/dss_head`).reply(200, sastIssues);
mock.onGet(`${TEST_HOST}/dast_head`).reply(200, dast);
mock.onGet(`${TEST_HOST}/sast_container_head`).reply(200, dockerReport);
mock.onGet(`${TEST_HOST}/vulnerability_feedback_path`).reply(200, []);
});
it('renders reports', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.textContent).toContain('SAST detected 3 vulnerabilities');
expect(vm.$el.textContent).toContain('Dependency scanning detected 3 vulnerabilities');
// Renders container scanning result
expect(vm.$el.textContent).toContain('Container scanning detected 2 vulnerabilities');
// Renders DAST result
expect(vm.$el.textContent).toContain('DAST detected 2 vulnerabilities');
expect(vm.$el.textContent).not.toContain('for the source branch only');
done();
}, 0);
});
it('renders all reports expanded and with no way to collapse', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-collapse-btn')).toBeNull();
const reports = vm.$el.querySelectorAll('.js-report-section-container');
reports.forEach(report => {
expect(report).not.toHaveCss({ display: 'none' });
});
done();
}, 0);
});
});
describe('with error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/sast_head`).reply(500);
mock.onGet(`${TEST_HOST}/dss_head`).reply(500);
mock.onGet(`${TEST_HOST}/dast_head`).reply(500);
mock.onGet(`${TEST_HOST}/sast_container_head`).reply(500);
mock.onGet(`${TEST_HOST}/vulnerability_feedback_path`).reply(500, []);
});
it('renders error state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.textContent).toContain('SAST: Loading resulted in an error');
expect(vm.$el.textContent).toContain('Dependency scanning: Loading resulted in an error');
expect(vm.$el.textContent).toContain('Container scanning: Loading resulted in an error');
expect(vm.$el.textContent).toContain('DAST: Loading resulted in an error');
done();
}, 0);
});
});
});
});
......@@ -11453,9 +11453,6 @@ msgstr ""
msgid "Security Reports|More info"
msgstr ""
msgid "Security Reports|No Vulnerabilities"
msgstr ""
msgid "Security Reports|There was an error creating the issue."
msgstr ""
......@@ -14626,6 +14623,9 @@ msgstr ""
msgid "VisualReviewApp|Review and give feedback directly from within the review app"
msgstr ""
msgid "Vulnerabilities"
msgstr ""
msgid "Vulnerability Chart"
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