diff --git a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ec4326634da6f98ad5c77cae062d92dd57269c0a
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+ {{ s__('SecurityDashboard|Security Dashboard') }}
+
+
+
+
+
+ {{ s__('SecurityDashboard|Add or remove projects from your dashboard') }}
+
+
+
+
+
+ {{
+ s__(
+ 'SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select "Edit dashboard" to add and remove projects.',
+ )
+ }}
+ {{
+ s__('SecurityDashboard|More information')
+ }}.
+
+
+
+ {{ s__('SecurityDashboard|Add projects') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a41e58ff063499114591c8b7f83de51bc6339e85
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
@@ -0,0 +1,168 @@
+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();
+ });
+ });
+ });
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2b4f75d0a58e1c4fd14f33ee07e407fccbfadf48..c3937a3890595be88628a348e88e55976b31a6df 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14060,15 +14060,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 ""
@@ -14078,9 +14093,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 ""