Commit a473923c authored by Mark Florian's avatar Mark Florian Committed by Kushal Pandya

Add Instance Security Dashboard component

This implements the basic scaffolding of the root application component
that wraps the security dashboard, and leaves a placeholder for the
project selector.

Another MR will implement the project selector, and likely a third will
integrate the two into the store.

Part of the [Instance Security Dashboard MVC][1].

[1]: https://gitlab.com/gitlab-org/gitlab-ee/issues/6953
parent f4056979
<script>
import { mapActions, mapState } from 'vuex';
import { GlButton, GlEmptyState, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import SecurityDashboard from './app.vue';
export default {
name: 'InstanceSecurityDashboard',
components: {
GlButton,
GlEmptyState,
GlLink,
GlLoadingIcon,
SecurityDashboard,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
emptyDashboardStateSvgPath: {
type: String,
required: true,
},
projectsEndpoint: {
type: String,
required: true,
},
vulnerabilitiesEndpoint: {
type: String,
required: true,
},
vulnerabilitiesCountEndpoint: {
type: String,
required: true,
},
vulnerabilitiesHistoryEndpoint: {
type: String,
required: true,
},
vulnerabilityFeedbackHelpPath: {
type: String,
required: true,
},
},
data() {
return {
isInitialized: false,
showProjectSelector: false,
};
},
computed: {
...mapState('projects', ['projects']),
toggleButtonProps() {
return this.showProjectSelector
? {
variant: 'success',
text: s__('SecurityDashboard|Return to dashboard'),
}
: {
variant: 'secondary',
text: s__('SecurityDashboard|Edit dashboard'),
};
},
shouldShowEmptyState() {
return this.isInitialized && this.projects.length === 0;
},
},
created() {
this.setProjectsEndpoint(this.projectsEndpoint);
this.fetchProjects()
// Failure to fetch projects will be handled in the store, so do nothing here.
.catch(() => {})
.finally(() => {
this.isInitialized = true;
});
},
methods: {
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
toggleProjectSelector() {
this.showProjectSelector = !this.showProjectSelector;
},
},
};
</script>
<template>
<article>
<header class="page-title-holder flex-fill d-flex align-items-center">
<h2 class="page-title">{{ s__('SecurityDashboard|Security Dashboard') }}</h2>
<gl-button
v-if="isInitialized"
new-style
class="page-title-controls js-project-selector-toggle"
:variant="toggleButtonProps.variant"
@click="toggleProjectSelector"
v-text="toggleButtonProps.text"
/>
</header>
<template v-if="isInitialized">
<section v-if="showProjectSelector" class="js-dashboard-project-selector">
<h3>{{ s__('SecurityDashboard|Add or remove projects from your dashboard') }}</h3>
</section>
<template v-else>
<gl-empty-state
v-if="shouldShowEmptyState"
:title="s__('SecurityDashboard|Add a project to your dashboard')"
:svg-path="emptyStateSvgPath"
>
<template #description>
{{
s__(
'SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select "Edit dashboard" to add and remove projects.',
)
}}
<gl-link :href="dashboardDocumentation">{{
s__('SecurityDashboard|More information')
}}</gl-link
>.
</template>
<template #actions>
<gl-button new-style variant="success" @click="toggleProjectSelector">
{{ s__('SecurityDashboard|Add projects') }}
</gl-button>
</template>
</gl-empty-state>
<security-dashboard
v-else
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyDashboardStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesCountEndpoint"
:vulnerabilities-history-endpoint="vulnerabilitiesHistoryEndpoint"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
/>
</template>
</template>
<gl-loading-icon v-else size="md" />
</article>
</template>
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import InstanceSecurityDashboard from 'ee/security_dashboard/components/instance_security_dashboard.vue';
import SecurityDashboard from 'ee/security_dashboard/components/app.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
const dashboardDocumentation = '/help/docs';
const emptyStateSvgPath = '/svgs/empty.svg';
const emptyDashboardStateSvgPath = '/svgs/empty-dash.svg';
const projectsEndpoint = '/projects';
const vulnerabilitiesEndpoint = '/vulnerabilities';
const vulnerabilitiesCountEndpoint = '/vulnerabilities_summary';
const vulnerabilitiesHistoryEndpoint = '/vulnerabilities_history';
const vulnerabilityFeedbackHelpPath = '/vulnerabilities_feedback_help';
describe('Instance Security Dashboard component', () => {
let store;
let wrapper;
let actionResolvers;
const factory = ({ projects = [] } = {}) => {
store = new Vuex.Store({
modules: {
projects: {
namespaced: true,
actions: {
fetchProjects() {},
setProjectsEndpoint() {},
},
state: {
projects,
},
},
},
});
actionResolvers = [];
jest.spyOn(store, 'dispatch').mockImplementation(
() =>
new Promise(resolve => {
actionResolvers.push(resolve);
}),
);
wrapper = shallowMount(InstanceSecurityDashboard, {
localVue,
store,
sync: false,
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
emptyDashboardStateSvgPath,
projectsEndpoint,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilityFeedbackHelpPath,
},
});
};
const resolveActions = () => {
actionResolvers.forEach(resolve => resolve());
};
const findProjectSelectorToggleButton = () => wrapper.find('.js-project-selector-toggle');
const clickProjectSelectorToggleButton = () => {
findProjectSelectorToggleButton().vm.$emit('click');
return wrapper.vm.$nextTick();
};
const expectComponentWithProps = (Component, props) => {
const componentWrapper = wrapper.find(Component);
expect(componentWrapper.exists()).toBe(true);
expect(componentWrapper.props()).toEqual(expect.objectContaining(props));
};
const expectProjectSelectorState = () => {
expect(findProjectSelectorToggleButton().exists()).toBe(true);
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
};
afterEach(() => {
wrapper.destroy();
});
describe('on creation', () => {
beforeEach(() => {
factory();
});
it('dispatches the expected actions', () => {
expect(store.dispatch.mock.calls).toEqual([
['projects/setProjectsEndpoint', projectsEndpoint],
['projects/fetchProjects', undefined],
]);
});
it('displays the initial loading state', () => {
expect(findProjectSelectorToggleButton().exists()).toBe(false);
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
});
});
describe('given there are no projects', () => {
beforeEach(() => {
factory();
resolveActions();
});
it('renders the empty state', () => {
expect(findProjectSelectorToggleButton().exists()).toBe(true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
expectComponentWithProps(GlEmptyState, {
svgPath: emptyStateSvgPath,
});
});
describe('after clicking the project selector toggle button', () => {
beforeEach(clickProjectSelectorToggleButton);
it('renders the project selector state', () => {
expectProjectSelectorState();
});
});
});
describe('given there are projects', () => {
beforeEach(() => {
factory({ projects: [{ name: 'foo', id: 1 }] });
resolveActions();
});
it('renders the security dashboard state', () => {
expect(findProjectSelectorToggleButton().exists()).toBe(true);
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expectComponentWithProps(SecurityDashboard, {
dashboardDocumentation,
emptyStateSvgPath: emptyDashboardStateSvgPath,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilityFeedbackHelpPath,
});
});
describe('after clicking the project selector toggle button', () => {
beforeEach(clickProjectSelectorToggleButton);
it('renders the project selector state', () => {
expectProjectSelectorState();
});
});
});
});
......@@ -14063,15 +14063,30 @@ msgstr ""
msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
msgid "SecurityDashboard|Add a project to your dashboard"
msgstr ""
msgid "SecurityDashboard|Add or remove projects from your dashboard"
msgstr ""
msgid "SecurityDashboard|Add projects"
msgstr ""
msgid "SecurityDashboard|Confidence"
msgstr ""
msgid "SecurityDashboard|Edit dashboard"
msgstr ""
msgid "SecurityDashboard|Hide dismissed"
msgstr ""
msgid "SecurityDashboard|Monitor vulnerabilities in your code"
msgstr ""
msgid "SecurityDashboard|More information"
msgstr ""
msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered"
msgstr ""
......@@ -14081,9 +14096,18 @@ msgstr ""
msgid "SecurityDashboard|Report type"
msgstr ""
msgid "SecurityDashboard|Return to dashboard"
msgstr ""
msgid "SecurityDashboard|Security Dashboard"
msgstr ""
msgid "SecurityDashboard|Severity"
msgstr ""
msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
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