Commit 90f2fbc7 authored by Alexander Turinske's avatar Alexander Turinske Committed by Phil Hughes

Implement empty state on security dashboard

- previously there was an empty state that showed on the
  security dashboards
- when I migrated the dashboards to use the security
  charts component, this feature was missed
- add the empty state back in for the security dashboards
- update tests
parent a4b0530c
......@@ -4,9 +4,9 @@ import VulnerabilitySeverities from 'ee/security_dashboard/components/first_clas
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_dashboard_projects.query.graphql';
import createFlash from '~/flash';
import { createProjectLoadingError } from '../helpers';
import InstanceSecurityVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import CsvExportButton from './csv_export_button.vue';
import vulnerabilityHistoryQuery from '../graphql/instance_vulnerability_history.query.graphql';
import vulnerabilityGradesQuery from '../graphql/instance_vulnerability_grades.query.graphql';
......@@ -35,7 +35,7 @@ export default {
return data.instanceSecurityDashboard.projects.nodes;
},
error() {
createFlash(__('Something went wrong, unable to get projects'));
createFlash({ message: createProjectLoadingError() });
},
},
},
......
......@@ -2,6 +2,7 @@
import { GlAlert } from '@gitlab/ui';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_dashboard_projects.query.graphql';
import { createProjectLoadingError } from '../helpers';
import ProjectManager from './first_class_project_manager/project_manager.vue';
export default {
......@@ -27,13 +28,18 @@ export default {
hasError: false,
};
},
computed: {
errorMessage() {
return createProjectLoadingError();
},
},
};
</script>
<template>
<security-dashboard-layout>
<gl-alert v-if="hasError" variant="danger">
{{ __('Something went wrong, unable to get projects') }}
{{ errorMessage }}
</gl-alert>
<div v-else class="gl-display-flex gl-justify-content-center">
<project-manager :projects="projects" />
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { createProjectLoadingError } from '../helpers';
import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue';
import SecurityChartsLayout from './security_charts_layout.vue';
import VulnerabilityChart from './first_class_vulnerability_chart.vue';
import VulnerabilitySeverities from './first_class_vulnerability_severities.vue';
import vulnerabilityHistoryQuery from '../graphql/group_vulnerability_history.query.graphql';
import vulnerabilityGradesQuery from '../graphql/group_vulnerability_grades.query.graphql';
import SecurityChartsLayout from './security_charts_layout.vue';
import vulnerableProjectsQuery from '../graphql/vulnerable_projects.query.graphql';
export default {
components: {
GlLoadingIcon,
DashboardNotConfigured,
SecurityChartsLayout,
VulnerabilitySeverities,
VulnerabilityChart,
......@@ -17,18 +24,55 @@ export default {
required: true,
},
},
apollo: {
projects: {
query: vulnerableProjectsQuery,
variables() {
return { fullPath: this.groupFullPath };
},
update(data) {
return data?.group?.projects?.nodes ?? [];
},
error() {
createFlash({ message: createProjectLoadingError() });
},
},
},
data() {
return {
projects: [],
vulnerabilityHistoryQuery,
vulnerabilityGradesQuery,
};
},
computed: {
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
shouldShowCharts() {
return Boolean(!this.isLoadingProjects && this.projects.length);
},
shouldShowEmptyState() {
return !this.isLoadingProjects && !this.projects.length;
},
},
};
</script>
<template>
<security-charts-layout>
<vulnerability-chart :query="vulnerabilityHistoryQuery" :group-full-path="groupFullPath" />
<vulnerability-severities :query="vulnerabilityGradesQuery" :group-full-path="groupFullPath" />
<template v-if="shouldShowEmptyState" #empty-state>
<dashboard-not-configured />
</template>
<template v-else-if="shouldShowCharts" #default>
<vulnerability-chart :query="vulnerabilityHistoryQuery" :group-full-path="groupFullPath" />
<vulnerability-severities
:query="vulnerabilityGradesQuery"
:group-full-path="groupFullPath"
/>
</template>
<template v-else #loading>
<gl-loading-icon size="lg" class="gl-mt-6" />
</template>
</security-charts-layout>
</template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { createProjectLoadingError } from '../helpers';
import DashboardNotConfigured from './empty_states/instance_dashboard_not_configured.vue';
import SecurityChartsLayout from './security_charts_layout.vue';
import VulnerabilityChart from './first_class_vulnerability_chart.vue';
import VulnerabilitySeverities from './first_class_vulnerability_severities.vue';
import projectsQuery from '../graphql/get_instance_security_dashboard_projects.query.graphql';
import vulnerabilityHistoryQuery from '../graphql/instance_vulnerability_history.query.graphql';
import vulnerabilityGradesQuery from '../graphql/instance_vulnerability_grades.query.graphql';
export default {
components: {
GlLoadingIcon,
DashboardNotConfigured,
SecurityChartsLayout,
VulnerabilitySeverities,
VulnerabilityChart,
},
apollo: {
projects: {
query: projectsQuery,
update(data) {
return data?.instanceSecurityDashboard?.projects?.nodes ?? [];
},
error() {
createFlash({ message: createProjectLoadingError() });
},
},
},
data() {
return {
projects: [],
vulnerabilityHistoryQuery,
vulnerabilityGradesQuery,
};
},
computed: {
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
shouldShowCharts() {
return Boolean(!this.isLoadingProjects && this.projects.length);
},
shouldShowEmptyState() {
return !this.isLoadingProjects && !this.projects.length;
},
},
};
</script>
<template>
<security-charts-layout>
<vulnerability-chart :query="vulnerabilityHistoryQuery" />
<vulnerability-severities :query="vulnerabilityGradesQuery" />
<template v-if="shouldShowEmptyState" #empty-state>
<dashboard-not-configured />
</template>
<template v-else-if="shouldShowCharts" #default>
<vulnerability-chart :query="vulnerabilityHistoryQuery" />
<vulnerability-severities :query="vulnerabilityGradesQuery" />
</template>
<template v-else #loading>
<gl-loading-icon size="lg" class="gl-mt-6" />
</template>
</security-charts-layout>
</template>
......@@ -8,11 +8,13 @@ export default {
};
</script>
<template functional>
<div>
<template>
<div data-testid="security-charts-layout">
<h2>{{ $options.i18n.title }}</h2>
<slot name="loading"></slot>
<div class="security-charts gl-display-flex gl-flex-wrap">
<slot></slot>
</div>
<slot name="empty-state"></slot>
</div>
</template>
export const COLLAPSE_SECURITY_REPORTS_SUMMARY_LOCAL_STORAGE_KEY =
'hide_pipelines_security_reports_summary_details';
export default () => ({});
......@@ -3,7 +3,7 @@ import { ALL, BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/c
import { REPORT_TYPES, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
const parseOptions = obj =>
Object.entries(obj).map(([id, name]) => ({ id: id.toUpperCase(), name }));
......@@ -107,4 +107,6 @@ export const preparePageInfo = pageInfo => {
return { ...pageInfo, hasNextPage: Boolean(pageInfo?.endCursor) };
};
export const createProjectLoadingError = () => __('An error occurred while retrieving projects.');
export default () => ({});
......@@ -28,6 +28,10 @@ export default (el, dashboardType) => {
}
const props = {};
const provide = {
dashboardDocumentation: el.dataset.dashboardDocumentation,
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
};
let component;
......@@ -36,6 +40,7 @@ export default (el, dashboardType) => {
props.groupFullPath = el.dataset.groupFullPath;
} else if (dashboardType === DASHBOARD_TYPES.INSTANCE) {
component = InstanceSecurityCharts;
provide.instanceDashboardSettingsPath = el.dataset.instanceDashboardSettingsPath;
}
const router = createRouter();
......@@ -46,6 +51,7 @@ export default (el, dashboardType) => {
store,
router,
apolloProvider,
provide: () => provide,
render(createElement) {
return createElement(component, { props });
},
......
---
title: Implement empty state on security dashboard
merge_request: 40413
author:
type: changed
......@@ -51,7 +51,7 @@ describe('First Class Instance Dashboard Component', () => {
});
it('renders the alert component', () => {
expect(findAlert().text()).toBe('Something went wrong, unable to get projects');
expect(findAlert().text()).toBe('An error occurred while retrieving projects.');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'jest/helpers/test_constants';
import { GlLoadingIcon } from '@gitlab/ui';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/group_dashboard_not_configured.vue';
import GroupSecurityCharts from 'ee/security_dashboard/components/group_security_charts.vue';
import SecurityChartsLayout from 'ee/security_dashboard/components/security_charts_layout.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import VulnerabilitySeverities from 'ee/security_dashboard/components/first_class_vulnerability_severities.vue';
import vulnerabilityHistoryQuery from 'ee/security_dashboard/graphql/group_vulnerability_history.query.graphql';
import vulnerabilityGradesQuery from 'ee/security_dashboard/graphql/group_vulnerability_grades.query.graphql';
jest.mock('ee/security_dashboard/graphql/group_vulnerability_history.query.graphql', () => ({}));
jest.mock('ee/security_dashboard/graphql/group_vulnerability_grades.query.graphql', () => ({
mockGrades: true,
}));
jest.mock('ee/security_dashboard/graphql/group_vulnerability_history.query.graphql', () => ({
mockHistory: true,
}));
describe('Group Security Charts component', () => {
let wrapper;
......@@ -13,32 +22,87 @@ describe('Group Security Charts component', () => {
const groupFullPath = `${TEST_HOST}/group/5`;
const findSecurityChartsLayoutComponent = () => wrapper.find(SecurityChartsLayout);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findVulnerabilityChart = () => wrapper.find(VulnerabilityChart);
const findVulnerabilitySeverities = () => wrapper.find(VulnerabilitySeverities);
const findDashboardNotConfigured = () => wrapper.find(DashboardNotConfigured);
const createWrapper = () => {
const createWrapper = ({ loading = false } = {}) => {
wrapper = shallowMount(GroupSecurityCharts, {
mocks: {
$apollo: {
queries: {
projects: {
loading,
},
},
},
},
propsData: { groupFullPath },
stubs: {
SecurityChartsLayout,
},
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the default page', () => {
it('renders the loading page', () => {
createWrapper({ loading: true });
const securityChartsLayout = findSecurityChartsLayoutComponent();
const dashboardNotConfigured = findDashboardNotConfigured();
const loadingIcon = findLoadingIcon();
const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(vulnerabilityChart.props()).toEqual({ query: {}, groupFullPath });
expect(dashboardNotConfigured.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(true);
expect(vulnerabilityChart.exists()).toBe(false);
expect(vulnerabilitySeverities.exists()).toBe(false);
});
it('renders the empty state', () => {
createWrapper();
const securityChartsLayout = findSecurityChartsLayoutComponent();
const dashboardNotConfigured = findDashboardNotConfigured();
const loadingIcon = findLoadingIcon();
const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(dashboardNotConfigured.exists()).toBe(true);
expect(loadingIcon.exists()).toBe(false);
expect(vulnerabilityChart.exists()).toBe(false);
expect(vulnerabilitySeverities.exists()).toBe(false);
});
it('renders the default page', async () => {
createWrapper();
wrapper.setData({ projects: [{ name: 'project1' }] });
await wrapper.vm.$nextTick();
const securityChartsLayout = findSecurityChartsLayoutComponent();
const dashboardNotConfigured = findDashboardNotConfigured();
const loadingIcon = findLoadingIcon();
const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(dashboardNotConfigured.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(false);
expect(vulnerabilityChart.exists()).toBe(true);
expect(vulnerabilityChart.props()).toEqual({ query: vulnerabilityHistoryQuery, groupFullPath });
expect(vulnerabilitySeverities.exists()).toBe(true);
expect(vulnerabilitySeverities.props().groupFullPath).toEqual(groupFullPath);
expect(vulnerabilitySeverities.props()).toEqual({
query: vulnerabilityGradesQuery,
groupFullPath,
helpPagePath: '',
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import InstanceSecurityCharts from 'ee/security_dashboard/components/instance_security_charts.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/instance_dashboard_not_configured.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import VulnerabilitySeverities from 'ee/security_dashboard/components/first_class_vulnerability_severities.vue';
import SecurityChartsLayout from 'ee/security_dashboard/components/security_charts_layout.vue';
......@@ -17,28 +19,79 @@ describe('Instance Security Charts component', () => {
let wrapper;
const findSecurityChartsLayoutComponent = () => wrapper.find(SecurityChartsLayout);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findVulnerabilityChart = () => wrapper.find(VulnerabilityChart);
const findVulnerabilitySeverities = () => wrapper.find(VulnerabilitySeverities);
const findDashboardNotConfigured = () => wrapper.find(DashboardNotConfigured);
const createWrapper = () => {
wrapper = shallowMount(InstanceSecurityCharts, {});
const createWrapper = ({ loading = false } = {}) => {
wrapper = shallowMount(InstanceSecurityCharts, {
mocks: {
$apollo: {
queries: {
projects: {
loading,
},
},
},
},
stubs: {
SecurityChartsLayout,
},
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the default page', () => {
it('renders the loading page', () => {
createWrapper({ loading: true });
const securityChartsLayout = findSecurityChartsLayoutComponent();
const dashboardNotConfigured = findDashboardNotConfigured();
const loadingIcon = findLoadingIcon();
const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(dashboardNotConfigured.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(true);
expect(vulnerabilityChart.exists()).toBe(false);
expect(vulnerabilitySeverities.exists()).toBe(false);
});
it('renders the empty state', () => {
createWrapper();
const securityChartsLayout = findSecurityChartsLayoutComponent();
const dashboardNotConfigured = findDashboardNotConfigured();
const loadingIcon = findLoadingIcon();
const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(dashboardNotConfigured.exists()).toBe(true);
expect(loadingIcon.exists()).toBe(false);
expect(vulnerabilityChart.exists()).toBe(false);
expect(vulnerabilitySeverities.exists()).toBe(false);
});
it('renders the default page', async () => {
createWrapper();
wrapper.setData({ projects: [{ name: 'project1' }] });
await wrapper.vm.$nextTick();
const securityChartsLayout = findSecurityChartsLayoutComponent();
const dashboardNotConfigured = findDashboardNotConfigured();
const loadingIcon = findLoadingIcon();
const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(dashboardNotConfigured.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(false);
expect(vulnerabilityChart.props()).toEqual({ query: vulnerabilityHistoryQuery });
expect(vulnerabilitySeverities.exists()).toBe(true);
expect(vulnerabilitySeverities.props()).toEqual({
......
......@@ -4,19 +4,23 @@ import SecurityChartsLayout from 'ee/security_dashboard/components/security_char
describe('Security Charts Layout component', () => {
let wrapper;
const DummyComponent = {
name: 'dummy-component',
template: '<p>dummy component</p>',
const DummyComponent1 = {
name: 'dummy-component-1',
template: '<p>dummy component 1</p>',
};
const DummyComponent2 = {
name: 'dummy-component-2',
template: '<p>dummy component 2</p>',
};
const findSlot = () => wrapper.find('.security-charts');
const findSlot = () => wrapper.find(`[data-testid="security-charts-layout"]`);
const createWrapper = slots => {
wrapper = shallowMount(SecurityChartsLayout, { slots });
};
beforeEach(() => {
createWrapper({ default: DummyComponent });
createWrapper({ default: DummyComponent1, 'empty-state': DummyComponent2 });
});
afterEach(() => {
......@@ -26,6 +30,11 @@ describe('Security Charts Layout component', () => {
it('should render the default slot', () => {
const slot = findSlot();
expect(slot.find(DummyComponent).exists()).toBe(true);
expect(slot.find(DummyComponent1).exists()).toBe(true);
});
it('should render the empty-state slot', () => {
const slot = findSlot();
expect(slot.find(DummyComponent2).exists()).toBe(true);
});
});
......@@ -2834,6 +2834,9 @@ msgstr ""
msgid "An error occurred while retrieving diff files"
msgstr ""
msgid "An error occurred while retrieving projects."
msgstr ""
msgid "An error occurred while saving LDAP override status. Please try again."
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment