Commit 265c3dcc authored by Savas Vedova's avatar Savas Vedova

Merge branch '321775-unify-group-and-instance-reports' into 'master'

Unify group and instance level security dashboards

See merge request gitlab-org/gitlab!57702
parents 2c9ac85a 32bd255a
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import instanceProjectsQuery from 'ee/security_dashboard/graphql/queries/instance_projects.query.graphql';
import createFlash from '~/flash';
import { vulnerabilitiesSeverityCountScopes } from '../constants';
import { PROJECT_LOADING_ERROR_MESSAGE } from '../helpers';
import CsvExportButton from './csv_export_button.vue';
import DashboardNotConfigured from './empty_states/instance_dashboard_not_configured.vue';
import InstanceSecurityVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue';
import SurveyRequestBanner from './survey_request_banner.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue';
export default {
components: {
CsvExportButton,
SecurityDashboardLayout,
InstanceSecurityVulnerabilities,
Filters,
DashboardNotConfigured,
VulnerabilitiesCountList,
SurveyRequestBanner,
GlLoadingIcon,
},
apollo: {
projects: {
query: instanceProjectsQuery,
update(data) {
return data.instanceSecurityDashboard.projects.nodes;
},
error() {
createFlash({ message: PROJECT_LOADING_ERROR_MESSAGE });
},
},
},
data() {
return {
filters: null,
projects: [],
};
},
computed: {
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
hasNoProjects() {
return this.projects.length === 0;
},
},
methods: {
handleFilterChange(filters) {
this.filters = filters;
},
},
vulnerabilitiesSeverityCountScopes,
};
</script>
<template>
<gl-loading-icon v-if="isLoadingProjects" size="lg" class="gl-mt-6" />
<div v-else-if="hasNoProjects">
<survey-request-banner class="gl-mt-5" />
<dashboard-not-configured />
</div>
<security-dashboard-layout v-else>
<template #header>
<survey-request-banner class="gl-mt-5" />
<header class="gl-my-6 gl-display-flex gl-align-items-center" data-testid="header">
<h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }}
</h2>
<csv-export-button />
</header>
<vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.instance"
:filters="filters"
/>
</template>
<template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<instance-security-vulnerabilities :projects="projects" :filters="filters" />
</security-dashboard-layout>
</template>
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import groupProjectsQuery from '../graphql/queries/group_projects.query.graphql';
import vulnerabilityGradesQuery from '../graphql/queries/group_vulnerability_grades.query.graphql'; import vulnerabilityGradesQuery from '../graphql/queries/group_vulnerability_grades.query.graphql';
import vulnerabilityHistoryQuery from '../graphql/queries/group_vulnerability_history.query.graphql'; import vulnerabilityHistoryQuery from '../graphql/queries/group_vulnerability_history.query.graphql';
import groupProjectsQuery from '../graphql/queries/vulnerable_projects_group.query.graphql';
import { PROJECT_LOADING_ERROR_MESSAGE } from '../helpers'; import { PROJECT_LOADING_ERROR_MESSAGE } from '../helpers';
import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue'; import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue';
import VulnerabilityChart from './first_class_vulnerability_chart.vue'; import VulnerabilityChart from './first_class_vulnerability_chart.vue';
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
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 InstanceSecurityVulnerabilities from 'ee/security_dashboard/components/first_class_instance_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';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import { PROJECT_LOADING_ERROR_MESSAGE } from 'ee/security_dashboard/helpers'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import createFlash from '~/flash'; import { s__ } from '~/locale';
import { vulnerabilitiesSeverityCountScopes } from '../constants'; import { vulnerabilitiesSeverityCountScopes } from '../constants';
import groupProjectsQuery from '../graphql/queries/group_projects.query.graphql'; import vulnerableProjectsGroupQuery from '../graphql/queries/vulnerable_projects_group.query.graphql';
import vulnerableProjectsInstanceQuery from '../graphql/queries/vulnerable_projects_instance.query.graphql';
import CsvExportButton from './csv_export_button.vue'; import CsvExportButton from './csv_export_button.vue';
import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue'; import DashboardNotConfiguredGroup from './empty_states/group_dashboard_not_configured.vue';
import DashboardNotConfiguredInstance from './empty_states/instance_dashboard_not_configured.vue';
import SurveyRequestBanner from './survey_request_banner.vue'; import SurveyRequestBanner from './survey_request_banner.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue'; import VulnerabilitiesCountList from './vulnerability_count_list.vue';
...@@ -16,40 +19,68 @@ export default { ...@@ -16,40 +19,68 @@ export default {
components: { components: {
SecurityDashboardLayout, SecurityDashboardLayout,
GroupSecurityVulnerabilities, GroupSecurityVulnerabilities,
InstanceSecurityVulnerabilities,
Filters, Filters,
CsvExportButton, CsvExportButton,
DashboardNotConfigured, SurveyRequestBanner,
DashboardNotConfiguredGroup,
DashboardNotConfiguredInstance,
GlLoadingIcon, GlLoadingIcon,
VulnerabilitiesCountList, VulnerabilitiesCountList,
SurveyRequestBanner,
}, },
inject: ['groupFullPath'], inject: ['groupFullPath'],
props: {
dashboardType: {
type: String,
required: true,
validator: (value) => Object.values(DASHBOARD_TYPES).includes(value),
},
},
queries: {
[DASHBOARD_TYPES.GROUP]: vulnerableProjectsGroupQuery,
[DASHBOARD_TYPES.INSTANCE]: vulnerableProjectsInstanceQuery,
},
apollo: { apollo: {
projects: { projects: {
query: groupProjectsQuery, query() {
return this.$options.queries[this.dashboardType];
},
variables() { variables() {
return { fullPath: this.groupFullPath }; return this.isGroup ? { fullPath: this.groupFullPath } : {};
}, },
update(data) { update(data) {
return data.group.projects.nodes; return this.isGroup
? data.group.projects.nodes
: data.instanceSecurityDashboard.projects.nodes;
}, },
error() { skip() {
createFlash({ message: PROJECT_LOADING_ERROR_MESSAGE }); return !this.$options.queries[this.dashboardType];
}, },
}, },
}, },
data() { data() {
return { return {
filters: null, filters: {},
projects: [], projects: [],
}; };
}, },
computed: { computed: {
isLoadingProjects() { projectsWereFetched() {
return this.$apollo.queries.projects.loading; return !this.$apollo.queries.projects?.loading;
},
scope() {
return this.isGroup
? vulnerabilitiesSeverityCountScopes.group
: vulnerabilitiesSeverityCountScopes.instance;
},
isGroup() {
return this.dashboardType === DASHBOARD_TYPES.GROUP;
},
isInstance() {
return this.dashboardType === DASHBOARD_TYPES.INSTANCE;
}, },
hasNoProjects() { hasNoProjects() {
return this.projects.length === 0; return this.projects.length === 0 && this.projectsWereFetched;
}, },
}, },
methods: { methods: {
...@@ -57,37 +88,36 @@ export default { ...@@ -57,37 +88,36 @@ export default {
this.filters = filters; this.filters = filters;
}, },
}, },
vulnerabilitiesSeverityCountScopes, i18n: {
title: s__('SecurityReports|Vulnerability Report'),
},
}; };
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoadingProjects" size="lg" class="gl-mt-6" /> <div>
<gl-loading-icon v-if="!projectsWereFetched" size="lg" class="gl-mt-6" />
<div v-else-if="hasNoProjects"> <template v-else-if="hasNoProjects">
<survey-request-banner class="gl-mt-5" /> <survey-request-banner class="gl-mt-5" />
<dashboard-not-configured /> <dashboard-not-configured-group v-if="isGroup" />
</div> <dashboard-not-configured-instance v-else-if="isInstance" />
</template>
<security-dashboard-layout v-else> <security-dashboard-layout v-else>
<template #header> <template #header>
<survey-request-banner class="gl-mt-5" /> <survey-request-banner class="gl-mt-5" />
<header class="gl-my-6 gl-display-flex gl-align-items-center"> <header class="gl-my-6 gl-display-flex gl-align-items-center">
<h2 class="gl-flex-grow-1 gl-my-0"> <h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }} {{ $options.i18n.title }}
</h2> </h2>
<csv-export-button /> <csv-export-button />
</header> </header>
<vulnerabilities-count-list <vulnerabilities-count-list :scope="scope" :full-path="groupFullPath" :filters="filters" />
:scope="$options.vulnerabilitiesSeverityCountScopes.group"
:full-path="groupFullPath"
:filters="filters"
/>
</template> </template>
<template #sticky> <template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" /> <filters :projects="projects" @filterChange="handleFilterChange" />
</template> </template>
<group-security-vulnerabilities :filters="filters" /> <group-security-vulnerabilities v-if="isGroup" :filters="filters" />
<instance-security-vulnerabilities v-if="isInstance" :filters="filters" />
</security-dashboard-layout> </security-dashboard-layout>
</div>
</template> </template>
query groupProjects($fullPath: ID!) { query vulnerableProjects($fullPath: ID!) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
projects(includeSubgroups: true) { projects(includeSubgroups: true) {
nodes { nodes {
......
query vulnerableProjects {
instanceSecurityDashboard {
projects {
nodes {
id
name
}
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue';
import FirstClassInstanceSecurityDashboard from './components/first_class_instance_security_dashboard.vue';
import FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue'; import FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue';
import UnavailableState from './components/unavailable_state.vue'; import UnavailableState from './components/unavailable_state.vue';
import VulnerabilityReport from './components/vulnerability_report.vue';
import apolloProvider from './graphql/provider'; import apolloProvider from './graphql/provider';
import createRouter from './router'; import createRouter from './router';
import createStore from './store'; import createStore from './store';
...@@ -62,10 +61,10 @@ export default (el, dashboardType) => { ...@@ -62,10 +61,10 @@ export default (el, dashboardType) => {
emptyStateSvgPath, emptyStateSvgPath,
notEnabledScannersHelpPath, notEnabledScannersHelpPath,
noPipelineRunScannersHelpPath, noPipelineRunScannersHelpPath,
groupFullPath,
securityConfigurationPath, securityConfigurationPath,
surveyRequestSvgPath, surveyRequestSvgPath,
vulnerabilitiesExportEndpoint, vulnerabilitiesExportEndpoint,
groupFullPath,
hasVulnerabilities: parseBoolean(hasVulnerabilities), hasVulnerabilities: parseBoolean(hasVulnerabilities),
scanners: scanners ? JSON.parse(scanners) : [], scanners: scanners ? JSON.parse(scanners) : [],
hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean( hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean(
...@@ -77,6 +76,7 @@ export default (el, dashboardType) => { ...@@ -77,6 +76,7 @@ export default (el, dashboardType) => {
securityDashboardHelpPath, securityDashboardHelpPath,
projectAddEndpoint, projectAddEndpoint,
projectListEndpoint, projectListEndpoint,
dashboardType,
}; };
let component; let component;
...@@ -94,10 +94,10 @@ export default (el, dashboardType) => { ...@@ -94,10 +94,10 @@ export default (el, dashboardType) => {
provide.autoFixDocumentation = autoFixDocumentation; provide.autoFixDocumentation = autoFixDocumentation;
provide.autoFixMrsPath = autoFixMrsPath; provide.autoFixMrsPath = autoFixMrsPath;
} else if (dashboardType === DASHBOARD_TYPES.GROUP) { } else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard; component = VulnerabilityReport;
} else if (dashboardType === DASHBOARD_TYPES.INSTANCE) { } else if (dashboardType === DASHBOARD_TYPES.INSTANCE) {
provide.instanceDashboardSettingsPath = instanceDashboardSettingsPath; provide.instanceDashboardSettingsPath = instanceDashboardSettingsPath;
component = FirstClassInstanceSecurityDashboard; component = VulnerabilityReport;
} }
const router = createRouter(); const router = createRouter();
......
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/group_dashboard_not_configured.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 Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/survey_request_banner.vue';
import VulnerabilitiesCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
describe('First Class Group Dashboard Component', () => {
let wrapper;
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const groupFullPath = 'group-full-path';
const findDashboardLayout = () => wrapper.findComponent(SecurityDashboardLayout);
const findGroupVulnerabilities = () => wrapper.findComponent(FirstClassGroupVulnerabilities);
const findCsvExportButton = () => wrapper.findComponent(CsvExportButton);
const findFilters = () => wrapper.findComponent(Filters);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(DashboardNotConfigured);
const findVulnerabilitiesCountList = () => wrapper.findComponent(VulnerabilitiesCountList);
const findSurveyRequestBanner = () => wrapper.findComponent(SurveyRequestBanner);
const createWrapper = ({ data, loading = false } = {}) => {
return shallowMount(FirstClassGroupDashboard, {
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
},
provide: { groupFullPath },
data,
stubs: {
SecurityDashboardLayout,
},
mocks: {
$apollo: {
queries: {
projects: { loading },
},
},
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when loading', () => {
beforeEach(() => {
wrapper = createWrapper({ loading: true });
});
it('loading button should be visible', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('should not display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('should not show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(false);
});
});
describe('when has projects', () => {
beforeEach(() => {
wrapper = createWrapper({
data: () => ({
projects: [{ id: 1, name: 'GitLab Org' }],
projectsWereFetched: true,
filters: {},
}),
});
});
it('should render correctly', () => {
expect(findGroupVulnerabilities().props()).toEqual({
filters: {},
});
});
it('has filters', () => {
expect(findFilters().exists()).toBe(true);
});
it('loads projects from data', () => {
const projects = [{ id: 1, name: 'GitLab Org' }];
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 csv export button', () => {
expect(findCsvExportButton().exists()).toBe(true);
});
it('loading button should not be rendered', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should not display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('should display the vulnerability count list with the correct data', () => {
expect(findVulnerabilitiesCountList().props()).toMatchObject({
scope: 'group',
fullPath: groupFullPath,
filters: wrapper.vm.filters,
});
});
it('should show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(true);
});
});
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);
});
it('should show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(true);
});
});
});
...@@ -193,3 +193,33 @@ export const mockProjectSecurityChartsWithData = () => ({ ...@@ -193,3 +193,33 @@ export const mockProjectSecurityChartsWithData = () => ({
}, },
}, },
}); });
export const mockVulnerableProjectsInstance = () => ({
data: {
instanceSecurityDashboard: {
projects: {
nodes: [
{
id: 'gid://gitlab/Project/2',
name: 'Gitlab Shell',
},
],
},
},
},
});
export const mockVulnerableProjectsGroup = () => ({
data: {
group: {
projects: {
nodes: [
{
id: 'gid://gitlab/Project/2',
name: 'Gitlab Shell',
},
],
},
},
},
});
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