Commit 23795036 authored by Nathan Friend's avatar Nathan Friend

Merge branch 'project-sec-dashboard-no-pipeline' into 'master'

Add a first class dashboard unconfigured state

See merge request gitlab-org/gitlab!28344
parents a132a3ac d5dd2222
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlEmptyState,
},
props: {
helpPath: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
DESCRIPTION: s__(
`SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities.`,
),
};
</script>
<template>
<gl-empty-state
:title="s__('SecurityDashboard|Monitor vulnerabilities in your code')"
:svg-path="svgPath"
:description="$options.DESCRIPTION"
:primary-button-link="helpPath"
:primary-button-text="__('Learn more')"
/>
</template>
<script> <script>
import ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import VulnerabilitiesCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue'; import VulnerabilitiesCountList from 'ee/security_dashboard/components/vulnerability_count_list.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 ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
export default { export default {
components: { components: {
SecurityDashboardLayout,
ProjectVulnerabilitiesApp, ProjectVulnerabilitiesApp,
ReportsNotConfigured,
SecurityDashboardLayout,
VulnerabilitiesCountList, VulnerabilitiesCountList,
Filters, Filters,
}, },
props: { props: {
dashboardDocumentation: { emptyStateSvgPath: {
type: String, type: String,
required: true, required: true,
}, },
emptyStateSvgPath: { securityDashboardHelpPath: {
type: String, type: String,
required: true, required: true,
}, },
projectFullPath: { projectFullPath: {
type: String, type: String,
required: true, required: false,
default: '',
},
dashboardDocumentation: {
type: String,
required: false,
default: '',
},
hasPipelineData: {
type: Boolean,
required: false,
default: false,
}, },
}, },
data() { data() {
...@@ -39,16 +52,25 @@ export default { ...@@ -39,16 +52,25 @@ export default {
</script> </script>
<template> <template>
<security-dashboard-layout> <div>
<template #header> <template v-if="hasPipelineData">
<vulnerabilities-count-list :project-full-path="projectFullPath" /> <security-dashboard-layout>
<filters @filterChange="handleFilterChange" /> <template #header>
<vulnerabilities-count-list :project-full-path="projectFullPath" />
<filters @filterChange="handleFilterChange" />
</template>
<project-vulnerabilities-app
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:project-full-path="projectFullPath"
:filters="filters"
/>
</security-dashboard-layout>
</template> </template>
<project-vulnerabilities-app <reports-not-configured
:dashboard-documentation="dashboardDocumentation" v-else
:empty-state-svg-path="emptyStateSvgPath" :svg-path="emptyStateSvgPath"
:project-full-path="projectFullPath" :help-path="securityDashboardHelpPath"
:filters="filters"
/> />
</security-dashboard-layout> </div>
</template> </template>
<script> <script>
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { GlEmptyState, GlSprintf } from '@gitlab/ui'; import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import ReportsNotConfigured from './empty_states/reports_not_configured.vue';
import SecurityDashboard from './security_dashboard_vuex.vue'; import SecurityDashboard from './security_dashboard_vuex.vue';
export default { export default {
...@@ -12,10 +12,11 @@ export default { ...@@ -12,10 +12,11 @@ export default {
components: { components: {
GlEmptyState, GlEmptyState,
GlSprintf, GlSprintf,
UserAvatarLink,
Icon, Icon,
TimeagoTooltip, ReportsNotConfigured,
SecurityDashboard, SecurityDashboard,
TimeagoTooltip,
UserAvatarLink,
}, },
props: { props: {
hasPipelineData: { hasPipelineData: {
...@@ -23,11 +24,6 @@ export default { ...@@ -23,11 +24,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
emptyStateIllustrationPath: {
type: String,
required: false,
default: null,
},
securityDashboardHelpPath: { securityDashboardHelpPath: {
type: String, type: String,
required: false, required: false,
...@@ -84,15 +80,6 @@ export default { ...@@ -84,15 +80,6 @@ export default {
default: null, default: null,
}, },
}, },
computed: {
emptyStateDescription() {
return s__(
`SecurityDashboard|
The security dashboard displays the latest security report.
Use it to find and fix vulnerabilities.`,
).trim();
},
},
}; };
</script> </script>
<template> <template>
...@@ -157,13 +144,10 @@ export default { ...@@ -157,13 +144,10 @@ export default {
</template> </template>
</security-dashboard> </security-dashboard>
</template> </template>
<gl-empty-state <reports-not-configured
v-else v-else
:title="s__('SecurityDashboard|Monitor vulnerabilities in your code')" :svg-path="emptyStateSvgPath"
:svg-path="emptyStateIllustrationPath" :help-path="securityDashboardHelpPath"
:description="emptyStateDescription"
:primary-button-link="securityDashboardHelpPath"
:primary-button-text="__('Learn more')"
/> />
</div> </div>
</template> </template>
...@@ -20,10 +20,17 @@ export default ( ...@@ -20,10 +20,17 @@ export default (
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
}); });
const { dashboardDocumentation, emptyStateSvgPath } = el.dataset; const {
dashboardDocumentation,
emptyStateSvgPath,
hasPipelineData,
securityDashboardHelpPath,
} = el.dataset;
const props = { const props = {
emptyStateSvgPath, emptyStateSvgPath,
dashboardDocumentation, dashboardDocumentation,
hasPipelineData: Boolean(hasPipelineData),
securityDashboardHelpPath,
}; };
let component; let component;
......
...@@ -10,40 +10,38 @@ export default () => { ...@@ -10,40 +10,38 @@ export default () => {
const securityTab = document.getElementById('js-security-report-app'); const securityTab = document.getElementById('js-security-report-app');
const { const {
hasPipelineData,
userPath,
userAvatarPath,
pipelineCreated,
pipelinePath,
userName,
commitId, commitId,
commitPath, commitPath,
refId, dashboardDocumentation,
refPath, emptyStateSvgPath,
hasPipelineData,
pipelineCreated,
pipelineId, pipelineId,
pipelinePath,
projectId, projectId,
projectName, projectName,
dashboardDocumentation, refId,
emptyStateSvgPath, refPath,
securityDashboardHelpPath,
userAvatarPath,
userName,
userPath,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
vulnerabilitiesSummaryEndpoint, vulnerabilitiesSummaryEndpoint,
vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath,
securityDashboardHelpPath,
emptyStateIllustrationPath,
} = securityTab.dataset; } = securityTab.dataset;
const parsedPipelineId = parseInt(pipelineId, 10); const parsedPipelineId = parseInt(pipelineId, 10);
const parsedHasPipelineData = parseBoolean(hasPipelineData); const parsedHasPipelineData = parseBoolean(hasPipelineData);
let props = { let props = {
hasPipelineData: parsedHasPipelineData,
dashboardDocumentation, dashboardDocumentation,
emptyStateSvgPath, emptyStateSvgPath,
hasPipelineData: parsedHasPipelineData,
securityDashboardHelpPath,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
vulnerabilitiesSummaryEndpoint, vulnerabilitiesSummaryEndpoint,
vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath,
securityDashboardHelpPath,
emptyStateIllustrationPath,
}; };
if (parsedHasPipelineData) { if (parsedHasPipelineData) {
props = { props = {
......
...@@ -110,7 +110,7 @@ export default { ...@@ -110,7 +110,7 @@ export default {
<gl-empty-state <gl-empty-state
:title="s__(`No vulnerabilities found for this project`)" :title="s__(`No vulnerabilities found for this project`)"
:svg-path="emptyStateSvgPath" :svg-path="emptyStateSvgPath"
:description="$options.emptyStateDecription" :description="$options.emptyStateDescription"
:primary-button-link="dashboardDocumentation" :primary-button-link="dashboardDocumentation"
:primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')" :primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')"
/> />
......
...@@ -213,9 +213,8 @@ module EE ...@@ -213,9 +213,8 @@ module EE
def project_security_dashboard_config(project, pipeline) def project_security_dashboard_config(project, pipeline)
if pipeline.nil? if pipeline.nil?
{ {
empty_state_illustration_path: image_path('illustrations/security-dashboard_empty.svg'), empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
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')
has_pipeline_data: "false"
} }
else else
{ {
......
...@@ -40,7 +40,7 @@ describe Projects::Security::DashboardController do ...@@ -40,7 +40,7 @@ describe Projects::Security::DashboardController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index) expect(response).to render_template(:index)
expect(response.body).to have_css("div#js-security-report-app[data-has-pipeline-data=true]") expect(response.body).to have_css("div#js-security-report-app[data-has-pipeline-data]")
end end
end end
...@@ -54,7 +54,7 @@ describe Projects::Security::DashboardController do ...@@ -54,7 +54,7 @@ describe Projects::Security::DashboardController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index) expect(response).to render_template(:index)
expect(response.body).to have_css("div#js-security-report-app[data-has-pipeline-data=true]") expect(response.body).to have_css("div#js-security-report-app[data-has-pipeline-data]")
end end
end end
...@@ -64,7 +64,7 @@ describe Projects::Security::DashboardController do ...@@ -64,7 +64,7 @@ describe Projects::Security::DashboardController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index) expect(response).to render_template(:index)
expect(response.body).to have_css("div#js-security-report-app[data-has-pipeline-data=false]") expect(response.body).not_to have_css("div#js-security-report-app[data-has-pipeline-data]")
end end
end end
end end
......
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.vue';
describe('reports not configured empty state', () => {
let wrapper;
const helpPath = '/help';
const svgPath = '/placeholder.svg';
const createComponent = () => {
wrapper = shallowMount(ReportsNotConfigured, {
propsData: { helpPath, svgPath },
});
};
const findEmptyState = () => wrapper.find(GlEmptyState);
beforeEach(() => {
createComponent();
});
it.each`
prop | data
${'title'} | ${'Monitor vulnerabilities in your code'}
${'svgPath'} | ${svgPath}
${'description'} | ${'The security dashboard displays the latest security report. Use it to find and fix vulnerabilities.'}
${'primaryButtonLink'} | ${helpPath}
${'primaryButtonText'} | ${'Learn more'}
`('passes the correct data to the $prop prop', ({ prop, data }) => {
expect(findEmptyState().props(prop)).toBe(data);
});
});
...@@ -3,11 +3,13 @@ import FirstClassProjectSecurityDashboard from 'ee/security_dashboard/components ...@@ -3,11 +3,13 @@ import FirstClassProjectSecurityDashboard from 'ee/security_dashboard/components
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 ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue'; import ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.vue';
const drilledProps = { const props = {
dashboardDocumentation: '/help/docs', dashboardDocumentation: '/help/docs',
emptyStateSvgPath: '/svgs/empty/svg', emptyStateSvgPath: '/svgs/empty/svg',
projectFullPath: '/group/project', projectFullPath: '/group/project',
securityDashboardHelpPath: '/security/dashboard/help-path',
}; };
const filters = { foo: 'bar' }; const filters = { foo: 'bar' };
...@@ -16,10 +18,14 @@ describe('First class Project Security Dashboard component', () => { ...@@ -16,10 +18,14 @@ describe('First class Project Security Dashboard component', () => {
const findFilters = () => wrapper.find(Filters); const findFilters = () => wrapper.find(Filters);
const findVulnerabilities = () => wrapper.find(ProjectVulnerabilitiesApp); const findVulnerabilities = () => wrapper.find(ProjectVulnerabilitiesApp);
const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured);
const createComponent = options => { const createComponent = options => {
wrapper = shallowMount(FirstClassProjectSecurityDashboard, { wrapper = shallowMount(FirstClassProjectSecurityDashboard, {
propsData: drilledProps, propsData: {
...props,
...options.props,
},
stubs: { SecurityDashboardLayout }, stubs: { SecurityDashboardLayout },
...options, ...options,
}); });
...@@ -29,30 +35,38 @@ describe('First class Project Security Dashboard component', () => { ...@@ -29,30 +35,38 @@ describe('First class Project Security Dashboard component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('on render', () => { describe('on render when pipeline has data', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ props: { hasPipelineData: true } });
}); });
it('should render the vulnerabilities', () => { it('should render the vulnerabilities', () => {
expect(findVulnerabilities().exists()).toBe(true); expect(findVulnerabilities().exists()).toBe(true);
}); });
it.each(Object.entries(drilledProps))( it('should pass down the %s prop to the vulnerabilities', () => {
'should pass down the %s prop to the vulnerabilities', expect(findVulnerabilities().props('dashboardDocumentation')).toBe(
(key, value) => { props.dashboardDocumentation,
expect(findVulnerabilities().props(key)).toBe(value); );
}, expect(findVulnerabilities().props('emptyStateSvgPath')).toBe(props.emptyStateSvgPath);
); expect(findVulnerabilities().props('projectFullPath')).toBe(props.projectFullPath);
});
it('should render the filters component', () => { it('should render the filters component', () => {
expect(findFilters().exists()).toBe(true); expect(findFilters().exists()).toBe(true);
}); });
it('does not display the unconfigured state', () => {
expect(findUnconfiguredState().exists()).toBe(false);
});
}); });
describe('with filter data', () => { describe('with filter data', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
props: {
hasPipelineData: true,
},
data() { data() {
return { filters }; return { filters };
}, },
...@@ -63,4 +77,18 @@ describe('First class Project Security Dashboard component', () => { ...@@ -63,4 +77,18 @@ describe('First class Project Security Dashboard component', () => {
expect(findVulnerabilities().props().filters).toEqual(filters); expect(findVulnerabilities().props().filters).toEqual(filters);
}); });
}); });
describe('when pipeline has no data', () => {
beforeEach(() => {
createComponent({
props: {
hasPipelineData: false,
},
});
});
it('displays the unconfigured state', () => {
expect(findUnconfiguredState().exists()).toBe(true);
});
});
}); });
...@@ -24,7 +24,7 @@ describe('Project Security Dashboard component', () => { ...@@ -24,7 +24,7 @@ describe('Project Security Dashboard component', () => {
stubs: ['security-dashboard-table'], stubs: ['security-dashboard-table'],
propsData: { propsData: {
hasPipelineData: true, hasPipelineData: true,
emptyStateIllustrationPath: `${TEST_HOST}/img`, emptyStateSvgPath: `${TEST_HOST}/img`,
securityDashboardHelpPath: `${TEST_HOST}/help_dashboard`, securityDashboardHelpPath: `${TEST_HOST}/help_dashboard`,
commit: { commit: {
id: '1234adf', id: '1234adf',
......
...@@ -96,8 +96,10 @@ describe ProjectsHelper do ...@@ -96,8 +96,10 @@ describe ProjectsHelper do
subject { helper.project_security_dashboard_config(project, nil) } subject { helper.project_security_dashboard_config(project, nil) }
it 'returns simple config' do it 'returns simple config' do
expect(subject[:security_dashboard_help_path]).to eq '/help/user/application_security/security_dashboard/index' expect(subject).to match(
expect(subject[:has_pipeline_data]).to eq 'false' empty_state_svg_path: start_with('/assets/illustrations/security-dashboard_empty'),
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index'
)
end end
end end
......
...@@ -17850,9 +17850,6 @@ msgstr "" ...@@ -17850,9 +17850,6 @@ msgstr ""
msgid "SecurityConfiguration|Status" msgid "SecurityConfiguration|Status"
msgstr "" msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
msgid "SecurityDashboard|%{firstProject} and %{secondProject}" msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
msgstr "" msgstr ""
...@@ -17913,6 +17910,9 @@ msgstr "" ...@@ -17913,6 +17910,9 @@ 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." 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 "" msgstr ""
msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
msgid "SecurityDashboard|Unable to add %{invalidProjects}" msgid "SecurityDashboard|Unable to add %{invalidProjects}"
msgstr "" 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