Commit 45f9a07f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '214117-improve-message-when-filtering' into 'master'

Refactor empty states

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