Commit d463a5ab authored by Savas Vedova's avatar Savas Vedova

Refactor empty states

- Display different message when filters
  are applied in the security dashaboards
- Move the empty state logic inside of the
  vulnerabilities component
- Update tests
parent 5ff6d0ea
<script>
import { GlEmptyState } from '@gitlab/ui';
export default {
components: {
GlEmptyState,
},
inject: ['emptyStateSvgPath', 'dashboardDocumentation'],
};
</script>
<template>
<gl-empty-state
:title="s__(`SecurityReports|No vulnerabilities found`)"
:svg-path="emptyStateSvgPath"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('SecurityReports|Learn more about setting up your dashboard')"
:description="
s__(
`SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.`,
)
"
/>
</template>
<script>
import { GlEmptyState } from '@gitlab/ui';
export default {
components: {
GlEmptyState,
},
inject: ['noVulnerabilitiesSvgPath'],
};
</script>
<template>
<gl-empty-state
:title="s__('SecurityReports|Sorry, your filter produced no results')"
:svg-path="noVulnerabilitiesSvgPath"
:description="s__(`SecurityReports|To widen your search, change or remove filters above`)"
/>
</template>
<script>
import { GlEmptyState } from '@gitlab/ui';
export default {
components: {
GlEmptyState,
},
inject: ['dashboardDocumentation', 'emptyStateSvgPath'],
};
</script>
<template>
<gl-empty-state
:title="s__('SecurityReports|Add projects to your group')"
:description="
s__(
'SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Add projects to your group to view their vulnerabilities here.',
)
"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('SecurityReports|Learn more about setting up your dashboard')"
:svg-path="emptyStateSvgPath"
/>
</template>
......@@ -7,16 +7,7 @@ export default {
GlButton,
GlLink,
},
props: {
svgPath: {
type: String,
required: true,
},
dashboardDocumentation: {
type: String,
required: true,
},
},
inject: ['dashboardDocumentation', 'emptyStateSvgPath'],
methods: {
handleAddProjectsClick() {
this.$emit('handleAddProjectsClick');
......@@ -28,7 +19,7 @@ export default {
<template>
<gl-empty-state
:title="s__('SecurityReports|Add a project to your dashboard')"
:svg-path="svgPath"
:svg-path="emptyStateSvgPath"
>
<template #description>
{{
......
......@@ -11,11 +11,8 @@ export default {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
inject: ['emptyStateSvgPath'],
DESCRIPTION: s__(
`SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities.`,
),
......@@ -25,7 +22,7 @@ export default {
<template>
<gl-empty-state
:title="s__('SecurityReports|Monitor vulnerabilities in your code')"
:svg-path="svgPath"
:svg-path="emptyStateSvgPath"
:description="$options.DESCRIPTION"
:primary-button-link="helpPath"
:primary-button-text="__('Learn more')"
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import GroupSecurityVulnerabilities from 'ee/security_dashboard/components/first_class_group_security_dashboard_vulnerabilities.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
......@@ -6,6 +7,7 @@ import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vul
import CsvExportButton from './csv_export_button.vue';
import VulnerabilitySeverity from './vulnerability_severity.vue';
import vulnerabilityHistoryQuery from '../graphql/group_vulnerability_history.graphql';
import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue';
export default {
components: {
......@@ -15,16 +17,10 @@ export default {
VulnerabilityChart,
Filters,
CsvExportButton,
DashboardNotConfigured,
GlLoadingIcon,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
groupFullPath: {
type: String,
required: true,
......@@ -42,45 +38,56 @@ export default {
return {
filters: {},
projects: [],
projectsWereFetched: false,
vulnerabilityHistoryQuery,
};
},
computed: {
isNotYetConfigured() {
return this.projects.length === 0 && this.projectsWereFetched;
},
},
methods: {
handleFilterChange(filters) {
this.filters = filters;
},
handleProjectsFetch(projects) {
this.projects = projects;
this.projectsWereFetched = true;
},
},
};
</script>
<template>
<security-dashboard-layout>
<template #header>
<header class="page-title-holder flex-fill d-flex align-items-center">
<h2 class="page-title flex-grow">{{ s__('SecurityReports|Group Security Dashboard') }}</h2>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" />
</header>
</template>
<template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<group-security-vulnerabilities
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:group-full-path="groupFullPath"
:filters="filters"
@projectFetch="handleProjectsFetch"
/>
<template #aside>
<vulnerability-chart
:query="vulnerabilityHistoryQuery"
<div>
<gl-loading-icon v-if="!projectsWereFetched" size="lg" class="gl-mt-6" />
<dashboard-not-configured v-if="isNotYetConfigured" />
<security-dashboard-layout v-else :class="{ 'gl-display-none': !projectsWereFetched }">
<template #header>
<header class="page-title-holder flex-fill d-flex align-items-center">
<h2 class="page-title flex-grow">
{{ s__('SecurityReports|Group Security Dashboard') }}
</h2>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" />
</header>
</template>
<template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<group-security-vulnerabilities
:group-full-path="groupFullPath"
class="mb-4"
:filters="filters"
@projectFetch="handleProjectsFetch"
/>
<vulnerability-severity :endpoint="vulnerableProjectsEndpoint" />
</template>
</security-dashboard-layout>
<template #aside>
<vulnerability-chart
:query="vulnerabilityHistoryQuery"
:group-full-path="groupFullPath"
class="mb-4"
/>
<vulnerability-severity :endpoint="vulnerableProjectsEndpoint" />
</template>
</security-dashboard-layout>
</div>
</template>
<script>
import { s__ } from '~/locale';
import { GlAlert, GlButton, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
import { GlAlert, GlButton, GlIntersectionObserver } from '@gitlab/ui';
import VulnerabilityList from './vulnerability_list.vue';
import vulnerabilitiesQuery from '../graphql/group_vulnerabilities.graphql';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';
......@@ -9,19 +8,10 @@ export default {
components: {
GlAlert,
GlButton,
GlEmptyState,
GlIntersectionObserver,
VulnerabilityList,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
groupFullPath: {
type: String,
required: true,
......@@ -85,9 +75,6 @@ export default {
}
},
},
emptyStateDescription: s__(
`SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.`,
),
};
</script>
......@@ -107,22 +94,11 @@ export default {
</gl-alert>
<vulnerability-list
v-else
:filters="filters"
:is-loading="isLoadingFirstResult"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities="vulnerabilities"
should-show-project-namespace
>
<template #emptyState>
<gl-empty-state
:title="s__(`SecurityReports|No vulnerabilities found for this group`)"
:svg-path="emptyStateSvgPath"
:description="$options.emptyStateDescription"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('SecurityReports|Learn more about setting up your dashboard')"
/>
</template>
</vulnerability-list>
/>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
class="text-center"
......
......@@ -11,7 +11,7 @@ import projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_d
import ProjectManager from './first_class_project_manager/project_manager.vue';
import CsvExportButton from './csv_export_button.vue';
import vulnerabilityHistoryQuery from '../graphql/instance_vulnerability_history.graphql';
import DashboardNotConfigured from './empty_states/dashboard_not_configured.vue';
import DashboardNotConfigured from './empty_states/instance_dashboard_not_configured.vue';
export default {
components: {
......@@ -27,11 +27,7 @@ export default {
DashboardNotConfigured,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
vulnerableProjectsEndpoint: {
type: String,
required: true,
},
......@@ -120,14 +116,10 @@ export default {
<instance-security-vulnerabilities
v-if="shouldShowDashboard"
:projects="projects"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:filters="filters"
/>
<dashboard-not-configured
v-else-if="shouldShowEmptyState"
:svg-path="emptyStateSvgPath"
:dashboard-documentation="dashboardDocumentation"
@handleAddProjectsClick="toggleProjectSelector"
/>
<div v-else class="d-flex justify-content-center">
......
<script>
import { GlAlert, GlButton, GlEmptyState, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { GlAlert, GlButton, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import VulnerabilityList from './vulnerability_list.vue';
import vulnerabilitiesQuery from '../graphql/instance_vulnerabilities.graphql';
......@@ -10,20 +9,11 @@ export default {
components: {
GlAlert,
GlButton,
GlEmptyState,
GlIntersectionObserver,
GlLoadingIcon,
VulnerabilityList,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
filters: {
type: Object,
required: false,
......@@ -79,9 +69,6 @@ export default {
}
},
},
emptyStateDescription: s__(
`SecurityReports|While it's rare to have no vulnerabilities, 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.`,
),
};
</script>
......@@ -101,22 +88,11 @@ export default {
</gl-alert>
<vulnerability-list
v-else
:filters="filters"
:is-loading="isFirstResultLoading"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities="vulnerabilities"
should-show-project-namespace
>
<template #emptyState>
<gl-empty-state
:title="s__(`SecurityReports|No vulnerabilities found for dashboard`)"
:svg-path="emptyStateSvgPath"
:description="$options.emptyStateDescription"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('SecurityReports|Learn more about setting up your dashboard')"
/>
</template>
</vulnerability-list>
/>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
class="text-center"
......
......@@ -23,10 +23,6 @@ export default {
GlBanner,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
securityDashboardHelpPath: {
type: String,
required: true,
......@@ -36,11 +32,6 @@ export default {
required: false,
default: '',
},
dashboardDocumentation: {
type: String,
required: false,
default: '',
},
hasVulnerabilities: {
type: Boolean,
required: false,
......@@ -72,6 +63,7 @@ export default {
isBannerVisible: this.showIntroductionBanner && !parseBoolean(Cookies.get(BANNER_COOKIE_KEY)), // The and statement is for backward compatibility. See https://gitlab.com/gitlab-org/gitlab/-/issues/213671 for more information.
};
},
inject: ['dashboardDocumentation'],
methods: {
handleFilterChange(filters) {
this.filters = filters;
......@@ -120,16 +112,11 @@ export default {
</template>
<project-vulnerabilities-app
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:project-full-path="projectFullPath"
:filters="filters"
/>
</security-dashboard-layout>
</template>
<reports-not-configured
v-else
:svg-path="emptyStateSvgPath"
:help-path="securityDashboardHelpPath"
/>
<reports-not-configured v-else :help-path="securityDashboardHelpPath" />
</div>
</template>
<script>
import { s__ } from '~/locale';
import { GlAlert, GlDeprecatedButton, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
import { GlAlert, GlDeprecatedButton, GlIntersectionObserver } from '@gitlab/ui';
import VulnerabilityList from './vulnerability_list.vue';
import vulnerabilitiesQuery from '../graphql/project_vulnerabilities.graphql';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';
......@@ -10,19 +9,10 @@ export default {
components: {
GlAlert,
GlDeprecatedButton,
GlEmptyState,
GlIntersectionObserver,
VulnerabilityList,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
projectFullPath: {
type: String,
required: true,
......@@ -87,9 +77,6 @@ export default {
this.$apollo.queries.vulnerabilities.refetch();
},
},
emptyStateDescription: s__(
`SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.`,
),
};
</script>
......@@ -105,24 +92,12 @@ export default {
<vulnerability-list
v-else
:is-loading="isLoadingFirstVulnerabilities"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:filters="filters"
:vulnerabilities="vulnerabilities"
:should-show-identifier="true"
:should-show-report-type="true"
@refetch-vulnerabilities="refetchVulnerabilities"
>
<template #emptyState>
<gl-empty-state
:title="s__(`SecurityReports|No vulnerabilities found for this project`)"
:svg-path="emptyStateSvgPath"
:description="$options.emptyStateDescription"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('SecurityReports|Learn more about setting up your dashboard')"
/>
</template>
</vulnerability-list>
/>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
class="text-center"
......
<script>
import { s__, __, sprintf } from '~/locale';
import { GlEmptyState, GlFormCheckbox, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import { GlFormCheckbox, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SelectionSummary from './selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import IssueLink from 'ee/vulnerabilities/components/issue_link.vue';
import VulnerabilityCommentIcon from './vulnerability_comment_icon.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import SelectionSummary from './selection_summary.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';
export default {
name: 'VulnerabilityList',
components: {
GlEmptyState,
GlFormCheckbox,
GlLink,
GlSkeletonLoading,
......@@ -23,20 +24,14 @@ export default {
SelectionSummary,
SeverityBadge,
VulnerabilityCommentIcon,
FiltersProducedNoResults,
DashboardHasNoVulnerabilities,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
filters: {
type: Object,
required: false,
default: null,
default: () => ({}),
},
shouldShowIdentifier: {
type: Boolean,
......@@ -74,6 +69,9 @@ export default {
};
},
computed: {
hasAnyFiltersSelected() {
return Object.keys(this.filters).length > 0;
},
hasSelectedAllVulnerabilities() {
return this.numOfSelectedVulnerabilities === this.vulnerabilities.length;
},
......@@ -304,16 +302,8 @@ export default {
</template>
<template #empty>
<slot name="emptyState">
<gl-empty-state
:title="s__(`We've found no vulnerabilities`)"
:description="
__(
`While it's rare to have no vulnerabilities, 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.`,
)
"
/>
</slot>
<filters-produced-no-results v-if="hasAnyFiltersSelected && !isLoading" />
<dashboard-has-no-vulnerabilities v-else-if="!isLoading" />
</template>
</gl-table>
</div>
......
......@@ -34,13 +34,12 @@ export default (
}
const props = {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
dashboardDocumentation: el.dataset.dashboardDocumentation,
hasVulnerabilities: Boolean(el.dataset.hasVulnerabilities),
securityDashboardHelpPath: el.dataset.securityDashboardHelpPath,
projectAddEndpoint: el.dataset.projectAddEndpoint,
projectListEndpoint: el.dataset.projectListEndpoint,
vulnerabilitiesExportEndpoint: el.dataset.vulnerabilitiesExportEndpoint,
noVulnerabilitiesSvgPath: el.dataset.noVulnerabilitiesSvgPath,
};
let component;
......@@ -67,6 +66,11 @@ export default (
store,
router,
apolloProvider,
provide: () => ({
dashboardDocumentation: el.dataset.dashboardDocumentation,
noVulnerabilitiesSvgPath: el.dataset.noVulnerabilitiesSvgPath,
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
}),
render(createElement) {
return createElement(component, { props });
},
......
......@@ -201,6 +201,7 @@ module EE
vulnerabilities_export_endpoint: api_v4_security_projects_vulnerability_exports_path(id: project.id),
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'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'),
user_callouts_path: user_callouts_path,
......
......@@ -33,6 +33,7 @@ module Groups::SecurityFeaturesHelper
projects_endpoint: expose_url(api_v4_groups_projects_path(id: group.id)),
group_full_path: group.full_path,
vulnerability_feedback_help_path: help_page_path("user/application_security/index", anchor: "interacting-with-the-vulnerabilities"),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
vulnerable_projects_endpoint: group_security_vulnerable_projects_path(group),
......
......@@ -4,6 +4,7 @@ module SecurityHelper
def instance_security_dashboard_data
{
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
project_add_endpoint: security_projects_path,
......
---
title: Refactor empty states to reflect better messages
merge_request: 35624
author:
type: changed
import { mount } from '@vue/test-utils';
import { GlEmptyState, GlButton } from '@gitlab/ui';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
describe('dashboard has no vulnerabilities empty state', () => {
let wrapper;
const emptyStateSvgPath = '/placeholder.svg';
const dashboardDocumentation = '/path/to/dashboard/documentation';
const createWrapper = () =>
mount(DashboardHasNoVulnerabilities, {
provide: {
emptyStateSvgPath,
dashboardDocumentation,
},
});
const findGlEmptyState = () => wrapper.find(GlEmptyState);
const findButton = () => wrapper.find(GlButton);
const findLink = () => wrapper.find('a');
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('contains a GlEmptyState', () => {
expect(findGlEmptyState().exists()).toBe(true);
expect(findGlEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('contains a GlLink with href attribute equal to dashboardDocumentation', () => {
expect(findLink().attributes('href')).toBe(dashboardDocumentation);
});
it('contains a GlButton', () => {
expect(findButton().exists()).toBe(true);
});
it('has the correct message', () => {
expect(findGlEmptyState().text()).toContain(
"While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.",
);
});
it('has the correct title', () => {
expect(findGlEmptyState().text()).toContain('No vulnerabilities found');
});
});
import { mount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
describe('filters produced no results empty state', () => {
let wrapper;
const noVulnerabilitiesSvgPath = '/placeholder.svg';
const createWrapper = () =>
mount(FiltersProducedNoResults, {
provide: {
noVulnerabilitiesSvgPath,
},
});
const findGlEmptyState = () => wrapper.find(GlEmptyState);
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('contains a GlEmptyState', () => {
expect(findGlEmptyState().exists()).toBe(true);
expect(findGlEmptyState().props('svgPath')).toBe(noVulnerabilitiesSvgPath);
});
it('has the correct message', () => {
expect(findGlEmptyState().text()).toContain(
'To widen your search, change or remove filters above',
);
});
it('has the correct title', () => {
expect(findGlEmptyState().text()).toContain('Sorry, your filter produced no results');
});
});
import { mount } from '@vue/test-utils';
import { GlEmptyState, GlButton } from '@gitlab/ui';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/group_dashboard_not_configured.vue';
describe('first class group security dashboard empty state', () => {
let wrapper;
const dashboardDocumentation = '/path/to/dashboard/documentation';
const emptyStateSvgPath = '/placeholder.svg';
const createWrapper = () =>
mount(DashboardNotConfigured, {
provide: {
dashboardDocumentation,
emptyStateSvgPath,
},
});
const findGlEmptyState = () => wrapper.find(GlEmptyState);
const findButton = () => wrapper.find(GlButton);
const findLink = () => wrapper.find('a');
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('contains a GlEmptyState', () => {
expect(findGlEmptyState().exists()).toBe(true);
expect(findGlEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('contains a GlLink with href attribute equal to dashboardDocumentation', () => {
expect(findLink().attributes('href')).toBe(dashboardDocumentation);
});
it('contains a GlButton', () => {
expect(findButton().exists()).toBe(true);
});
it('has the correct message', () => {
expect(findGlEmptyState().text()).toContain(
'The security dashboard displays the latest security findings for projects you wish to monitor. Add projects to your group to view their vulnerabilities here.',
);
});
it('has the correct title', () => {
expect(findGlEmptyState().text()).toContain('Add projects to your group');
});
});
import { mount } from '@vue/test-utils';
import { GlEmptyState, GlButton, GlLink } from '@gitlab/ui';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/dashboard_not_configured.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/instance_dashboard_not_configured.vue';
describe('first class instance security dashboard empty state', () => {
let wrapper;
const dashboardDocumentation = '/path/to/dashboard/documentation';
const svgPath = '/placeholder.svg';
const emptyStateSvgPath = '/placeholder.svg';
const createWrapper = () =>
mount(DashboardNotConfigured, {
propsData: { svgPath, dashboardDocumentation },
provide: {
dashboardDocumentation,
emptyStateSvgPath,
},
});
const findGlEmptyState = () => wrapper.find(GlEmptyState);
const findButton = () => wrapper.find(GlButton);
const findLink = () => wrapper.find(GlLink);
......@@ -23,16 +27,9 @@ describe('first class instance security dashboard empty state', () => {
wrapper.destroy();
});
it('should render correctly', () => {
expect(wrapper.props()).toEqual({
svgPath,
dashboardDocumentation,
});
});
it('contains a GlEmptyState', () => {
expect(findGlEmptyState().exists()).toBe(true);
expect(findGlEmptyState().props()).toMatchObject({ svgPath });
expect(findGlEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('contains a GlLink with href attribute equal to dashboardDocumentation', () => {
......
......@@ -5,11 +5,14 @@ import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/
describe('reports not configured empty state', () => {
let wrapper;
const helpPath = '/help';
const svgPath = '/placeholder.svg';
const emptyStateSvgPath = '/placeholder.svg';
const createComponent = () => {
wrapper = shallowMount(ReportsNotConfigured, {
propsData: { helpPath, svgPath },
provide: {
emptyStateSvgPath,
},
propsData: { helpPath },
});
};
const findEmptyState = () => wrapper.find(GlEmptyState);
......@@ -21,7 +24,7 @@ describe('reports not configured empty state', () => {
it.each`
prop | data
${'title'} | ${'Monitor vulnerabilities in your code'}
${'svgPath'} | ${svgPath}
${'svgPath'} | ${emptyStateSvgPath}
${'description'} | ${'The security dashboard displays the latest security report. Use it to find and fix vulnerabilities.'}
${'primaryButtonLink'} | ${helpPath}
${'primaryButtonText'} | ${'Learn more'}
......
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import FirstClassGroupDashboard from 'ee/security_dashboard/components/first_class_group_security_dashboard.vue';
import FirstClassGroupVulnerabilities from 'ee/security_dashboard/components/first_class_group_security_dashboard_vulnerabilities.vue';
import VulnerabilitySeverity from 'ee/security_dashboard/components/vulnerability_severity.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/group_dashboard_not_configured.vue';
import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
......@@ -16,13 +18,16 @@ describe('First Class Group Dashboard Component', () => {
const vulnerableProjectsEndpoint = '/vulnerable/projects';
const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports';
const findDashboardLayout = () => wrapper.find(SecurityDashboardLayout);
const findGroupVulnerabilities = () => wrapper.find(FirstClassGroupVulnerabilities);
const findVulnerabilitySeverity = () => wrapper.find(VulnerabilitySeverity);
const findVulnerabilityChart = () => wrapper.find(VulnerabilityChart);
const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findFilters = () => wrapper.find(Filters);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(DashboardNotConfigured);
const createWrapper = () => {
const createWrapper = ({ data } = {}) => {
return shallowMount(FirstClassGroupDashboard, {
propsData: {
dashboardDocumentation,
......@@ -31,61 +36,114 @@ describe('First Class Group Dashboard Component', () => {
vulnerableProjectsEndpoint,
vulnerabilitiesExportEndpoint,
},
data,
stubs: {
SecurityDashboardLayout,
},
});
};
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('should render correctly', () => {
expect(findGroupVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
groupFullPath,
filters: {},
describe('when loading', () => {
beforeEach(() => {
wrapper = createWrapper();
});
});
it('has filters', () => {
expect(findFilters().exists()).toBe(true);
});
it('loading button should be visible', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('has the vulnerability history chart', () => {
expect(findVulnerabilityChart().props('groupFullPath')).toBe(groupFullPath);
});
it('dashboard should have display none because it needs to fetch the projects', () => {
expect(findDashboardLayout().attributes('class')).toEqual('gl-display-none');
});
it('responds to the projectFetch event', () => {
const projects = [{ id: 1, name: 'GitLab Org' }];
findGroupVulnerabilities().vm.$listeners.projectFetch(projects);
return wrapper.vm.$nextTick(() => {
expect(findFilters().props('projects')).toEqual(projects);
it('should not display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(false);
});
});
it('responds to the filterChange event', () => {
const filters = { severity: 'critical' };
findFilters().vm.$listeners.filterChange(filters);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.filters).toEqual(filters);
expect(findGroupVulnerabilities().props('filters')).toEqual(filters);
describe('when has projects', () => {
beforeEach(() => {
wrapper = createWrapper({
data: () => ({ projects: [{ id: 1 }], projectsWereFetched: true }),
});
});
it('should render correctly', () => {
expect(findGroupVulnerabilities().props()).toEqual({
groupFullPath,
filters: {},
});
});
it('has filters', () => {
expect(findFilters().exists()).toBe(true);
});
it('has the vulnerability history chart', () => {
expect(findVulnerabilityChart().props('groupFullPath')).toBe(groupFullPath);
});
it('responds to the projectFetch event', () => {
const projects = [{ id: 1, name: 'GitLab Org' }];
findGroupVulnerabilities().vm.$listeners.projectFetch(projects);
return wrapper.vm.$nextTick(() => {
expect(findFilters().props('projects')).toEqual(projects);
});
});
it('responds to the filterChange event', () => {
const filters = { severity: 'critical' };
findFilters().vm.$listeners.filterChange(filters);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.filters).toEqual(filters);
expect(findGroupVulnerabilities().props('filters')).toEqual(filters);
});
});
it('displays the vulnerability severity in an aside', () => {
expect(findVulnerabilitySeverity().exists()).toBe(true);
});
it('displays the csv export button', () => {
expect(findCsvExportButton().props('vulnerabilitiesExportEndpoint')).toBe(
vulnerabilitiesExportEndpoint,
);
});
it('loading button should not be rendered', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('dashboard should no more have display none', () => {
expect(findDashboardLayout().attributes('class')).toEqual('');
});
});
it('displays the vulnerability severity in an aside', () => {
expect(findVulnerabilitySeverity().exists()).toBe(true);
it('should not display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(false);
});
});
it('displays the csv export button', () => {
expect(findCsvExportButton().props('vulnerabilitiesExportEndpoint')).toBe(
vulnerabilitiesExportEndpoint,
);
describe('when has no projects', () => {
beforeEach(() => {
wrapper = createWrapper({
data: () => ({ projectsWereFetched: true }),
});
});
it('loading button should not be rendered', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('dashboard should not be rendered', () => {
expect(findDashboardLayout().exists()).toBe(false);
});
it('should display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(true);
});
});
});
......@@ -7,22 +7,15 @@ import { generateVulnerabilities } from './mock_data';
describe('First Class Group Dashboard Vulnerabilities Component', () => {
let wrapper;
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const groupFullPath = 'group-full-path';
const emptyStateDescription =
"While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.";
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findVulnerabilities = () => wrapper.find(VulnerabilityList);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findAlert = () => wrapper.find(GlAlert);
const createWrapper = ({ $apollo, stubs }) => {
return shallowMount(FirstClassGroupVulnerabilities, {
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
groupFullPath,
},
stubs,
......@@ -48,9 +41,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
it('passes down isLoading correctly', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
filters: {},
isLoading: true,
shouldShowIdentifier: false,
shouldShowReportType: false,
......@@ -96,25 +87,6 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
});
});
describe('when the query returned an empty vulnerability list', () => {
beforeEach(() => {
wrapper = createWrapper({
$apollo: {
queries: { vulnerabilities: { loading: false } },
},
stubs: {
VulnerabilityList,
GlTable,
GlEmptyState,
},
});
});
it('displays the empty state', () => {
expect(findEmptyState().text()).toContain(emptyStateDescription);
});
});
describe('when the query is loaded and we have results', () => {
const vulnerabilities = generateVulnerabilities();
......@@ -135,15 +107,9 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
});
});
it('does not have an empty state', () => {
expect(wrapper.html()).not.toContain(emptyStateDescription);
});
it('passes down properties correctly', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
filters: {},
isLoading: false,
shouldShowIdentifier: false,
shouldShowReportType: false,
......
......@@ -8,15 +8,13 @@ import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vul
import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import ProjectManager from 'ee/security_dashboard/components/first_class_project_manager/project_manager.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/dashboard_not_configured.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/instance_dashboard_not_configured.vue';
describe('First Class Instance Dashboard Component', () => {
let wrapper;
const defaultMocks = { $apollo: { queries: { projects: { loading: false } } } };
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const vulnerableProjectsEndpoint = '/vulnerable/projects';
const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports';
......@@ -35,8 +33,6 @@ describe('First Class Instance Dashboard Component', () => {
},
mocks: { ...defaultMocks },
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
vulnerableProjectsEndpoint,
vulnerabilitiesExportEndpoint,
},
......@@ -63,8 +59,6 @@ describe('First Class Instance Dashboard Component', () => {
it('should render the vulnerabilities', () => {
expect(findInstanceVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: {},
});
});
......@@ -111,10 +105,7 @@ describe('First Class Instance Dashboard Component', () => {
});
it('renders the empty state', () => {
expect(findEmptyState().props()).toEqual({
svgPath: emptyStateSvgPath,
dashboardDocumentation,
});
expect(findEmptyState().props()).toEqual({});
});
it('does not render the vulnerability list', () => {
......
......@@ -12,14 +12,8 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
let wrapper;
let store;
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const emptyStateDescription =
"While it's rare to have no vulnerabilities, 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.";
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findVulnerabilities = () => wrapper.find(VulnerabilityList);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findAlert = () => wrapper.find(GlAlert);
const createWrapper = ({ stubs, loading = false, isUpdatingProjects, data } = {}) => {
......@@ -43,10 +37,6 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
return shallowMount(FirstClassInstanceVulnerabilities, {
localVue,
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
},
store,
stubs,
mocks: {
......@@ -73,9 +63,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
it('passes down isLoading correctly', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
filters: {},
isLoading: true,
shouldShowIdentifier: false,
shouldShowReportType: false,
......@@ -118,22 +106,6 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
});
});
describe('when the query returned an empty vulnerability list', () => {
beforeEach(() => {
wrapper = createWrapper({
stubs: {
VulnerabilityList,
GlTable,
GlEmptyState,
},
});
});
it('displays the empty state', () => {
expect(findEmptyState().text()).toContain(emptyStateDescription);
});
});
describe('when the query is loaded and we have results', () => {
const vulnerabilities = generateVulnerabilities();
......@@ -151,15 +123,9 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
});
});
it('does not have an empty state', () => {
expect(wrapper.html()).not.toContain(emptyStateDescription);
});
it('passes down properties correctly', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
filters: {},
isLoading: false,
shouldShowIdentifier: false,
shouldShowReportType: false,
......
......@@ -15,8 +15,6 @@ import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/
import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.vue';
const props = {
dashboardDocumentation: '/help/docs',
emptyStateSvgPath: '/svgs/empty/svg',
projectFullPath: '/group/project',
securityDashboardHelpPath: '/security/dashboard/help-path',
vulnerabilitiesExportEndpoint: '/vulnerabilities/exports',
......@@ -24,6 +22,12 @@ const props = {
userCalloutsPath: `${TEST_HOST}/user_callouts`,
showIntroductionBanner: false,
};
const provide = {
dashboardDocumentation: '/help/docs',
emptyStateSvgPath: '/svgs/empty/svg',
};
const filters = { foo: 'bar' };
describe('First class Project Security Dashboard component', () => {
......@@ -41,6 +45,7 @@ describe('First class Project Security Dashboard component', () => {
...props,
...options.props,
},
provide,
stubs: { SecurityDashboardLayout, GlBanner },
...options,
});
......@@ -61,10 +66,6 @@ describe('First class Project Security Dashboard component', () => {
});
it('should pass down the %s prop to the vulnerabilities', () => {
expect(findVulnerabilities().props('dashboardDocumentation')).toBe(
props.dashboardDocumentation,
);
expect(findVulnerabilities().props('emptyStateSvgPath')).toBe(props.emptyStateSvgPath);
expect(findVulnerabilities().props('projectFullPath')).toBe(props.projectFullPath);
});
......@@ -107,7 +108,7 @@ describe('First class Project Security Dashboard component', () => {
});
it('links the banner to the proper documentation page', () => {
expect(findIntroductionBanner().props('buttonLink')).toBe(props.dashboardDocumentation);
expect(findIntroductionBanner().props('buttonLink')).toBe(provide.dashboardDocumentation);
});
it('hides the banner when the user clicks on the dismiss button', () => {
......
......@@ -21,9 +21,11 @@ describe('Project Security Dashboard component', () => {
wrapper = mount(ProjectSecurityDashboard, {
store: createStore(),
stubs: ['security-dashboard-table'],
provide: {
emptyStateSvgPath: `${TEST_HOST}/img`,
},
propsData: {
hasVulnerabilities: true,
emptyStateSvgPath: `${TEST_HOST}/img`,
securityDashboardHelpPath: `${TEST_HOST}/help_dashboard`,
commit: {
id: '1234adf',
......
import { mount } from '@vue/test-utils';
import { GlEmptyState, GlSkeletonLoading } from '@gitlab/ui';
import { GlSkeletonLoading } from '@gitlab/ui';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import { generateVulnerabilities, vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => {
......@@ -16,14 +18,17 @@ describe('Vulnerability list component', () => {
const createWrapper = ({ props = {}, data = defaultData }) => {
return mount(VulnerabilityList, {
propsData: {
dashboardDocumentation: '#',
emptyStateSvgPath: '#',
vulnerabilities: [],
...props,
},
stubs: {
GlPopover: true,
},
provide: () => ({
noVulnerabilitiesSvgPath: '#',
dashboardDocumentation: '#',
emptyStateSvgPath: '#',
}),
data: () => data,
attachToDocument: true,
});
......@@ -36,6 +41,8 @@ describe('Vulnerability list component', () => {
const findDataCell = label => wrapper.find(`[data-testid="${label}"]`);
const findDataCells = label => wrapper.findAll(`[data-testid="${label}"]`);
const findCellText = label => findDataCell(label).text();
const findFiltersProducedNoResults = () => wrapper.find(FiltersProducedNoResults);
const findDashboardHasNoVulnerabilities = () => wrapper.find(DashboardHasNoVulnerabilities);
afterEach(() => {
wrapper.destroy();
......@@ -283,14 +290,27 @@ describe('Vulnerability list component', () => {
});
});
describe('with no vulnerabilities', () => {
describe('with no vulnerabilities when there are no filters', () => {
beforeEach(() => {
wrapper = createWrapper({});
});
it('should show the empty state', () => {
expect(findCell('status').exists()).toEqual(false);
expect(wrapper.find(GlEmptyState).exists()).toEqual(true);
expect(findDashboardHasNoVulnerabilities().exists()).toEqual(true);
expect(findFiltersProducedNoResults().exists()).toEqual(false);
});
});
describe('with no vulnerabilities when there are filters', () => {
beforeEach(() => {
wrapper = createWrapper({ props: { filters: { someFilter: 'true' } } });
});
it('should show the empty state', () => {
expect(findCell('status').exists()).toEqual(false);
expect(findFiltersProducedNoResults().exists()).toEqual(true);
expect(findDashboardHasNoVulnerabilities().exists()).toEqual(false);
});
});
});
......@@ -127,6 +127,7 @@ RSpec.describe Groups::SecurityFeaturesHelper do
vulnerabilities_history_endpoint: "/groups/#{group.full_path}/-/security/vulnerability_findings/history",
projects_endpoint: "http://localhost/api/v4/groups/#{group.id}/projects",
group_full_path: group.full_path,
no_vulnerabilities_svg_path: '/images/illustrations/issues.svg',
vulnerability_feedback_help_path: '/help/user/application_security/index#interacting-with-the-vulnerabilities',
empty_state_svg_path: '/images/illustrations/security-dashboard-empty-state.svg',
dashboard_documentation: '/help/user/application_security/security_dashboard/index',
......
......@@ -125,6 +125,7 @@ RSpec.describe ProjectsHelper do
vulnerabilities_summary_endpoint: "/#{project.full_path}/-/security/vulnerability_findings/summary",
vulnerabilities_export_endpoint: "/api/v4/security/projects/#{project.id}/vulnerability_exports",
vulnerability_feedback_help_path: '/help/user/application_security/index#interacting-with-the-vulnerabilities',
no_vulnerabilities_svg_path: start_with('/assets/illustrations/issues-'),
empty_state_svg_path: start_with('/assets/illustrations/security-dashboard-empty-state'),
dashboard_documentation: '/help/user/application_security/security_dashboard/index',
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
......
......@@ -9,6 +9,7 @@ RSpec.describe SecurityHelper do
it 'returns vulnerability, project, feedback, asset, and docs paths for the instance security dashboard' do
is_expected.to eq({
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
project_add_endpoint: security_projects_path,
......
......@@ -20539,6 +20539,9 @@ msgstr ""
msgid "SecurityReports|Add projects"
msgstr ""
msgid "SecurityReports|Add projects to your group"
msgstr ""
msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
......@@ -20611,7 +20614,7 @@ msgstr ""
msgid "SecurityReports|More information"
msgstr ""
msgid "SecurityReports|No vulnerabilities found for dashboard"
msgid "SecurityReports|No vulnerabilities found"
msgstr ""
msgid "SecurityReports|No vulnerabilities found for this group"
......@@ -20662,12 +20665,18 @@ msgstr ""
msgid "SecurityReports|Severity"
msgstr ""
msgid "SecurityReports|Sorry, your filter produced no results"
msgstr ""
msgid "SecurityReports|Status"
msgstr ""
msgid "SecurityReports|The rating \"unknown\" indicates that the underlying scanner doesn’t contain or provide a severity rating."
msgstr ""
msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Add projects to your group to view their vulnerabilities here."
msgstr ""
msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
......@@ -20701,6 +20710,9 @@ msgstr ""
msgid "SecurityReports|There was an error while generating the report."
msgstr ""
msgid "SecurityReports|To widen your search, change or remove filters above"
msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}"
msgstr ""
......@@ -20719,7 +20731,7 @@ msgstr ""
msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
msgid "SecurityReports|While it's rare to have no vulnerabilities, 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."
msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
msgid "SecurityReports|Won't fix / Accept risk"
......
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