Commit ef7c0b90 authored by David Pisek's avatar David Pisek Committed by Mike Greiling

Add frontend for secure configuration screen

This commit adds a vue controller to render a configuration
page, which displays a list of secure features, their current
configuration status and links to relevant documentation.
parent fcd7c1f4
import initSecurityConfiguration from 'ee/security_configuration';
document.addEventListener('DOMContentLoaded', initSecurityConfiguration);
<script>
import { __, sprintf } from '~/locale';
import { GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlLink,
Icon,
},
props: {
autoDevOpsEnabled: {
type: Boolean,
required: false,
default: false,
},
helpPagePath: {
type: String,
required: true,
},
pipelinesHelpPagePath: {
type: String,
required: true,
},
autoDevOpsHelpPagePath: {
type: String,
required: true,
},
latestPipelinePath: {
type: String,
required: false,
default: '',
},
features: {
type: Array,
required: true,
},
},
computed: {
headerContent() {
const body = __('Configure Security %{wordBreakOpportunity}and Compliance');
const wordBreakOpportunity = '<wbr />';
return sprintf(body, { wordBreakOpportunity }, false);
},
callOutLink() {
if (this.autoDevOpsEnabled) {
return this.autoDevOpsHelpPagePath;
}
if (this.latestPipelinePath) {
return this.latestPipelinePath;
}
return this.pipelinesHelpPagePath;
},
calloutContent() {
const bodyDefault = __(`The configuration status of the table below only applies to the default branch and
is based on the %{linkStart}latest pipeline%{linkEnd}.
Once you've configured a scan for the default branch, any subsequent feature branch you create will include the scan.`);
const bodyAutoDevOpsEnabled = __(
'All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project',
);
const body = this.autoDevOpsEnabled ? bodyAutoDevOpsEnabled : bodyDefault;
const linkStart = `<a href="${this.callOutLink}" target="_blank" rel="noopener">`;
const linkEnd = '</a>';
return sprintf(body, { linkStart, linkEnd }, false);
},
},
};
</script>
<template>
<article>
<header>
<h2 class="h4 my-3">
<span v-html="headerContent"></span>
<gl-link
target="_blank"
:href="helpPagePath"
:aria-label="__('Security configuration help link')"
>
<icon name="question" />
</gl-link>
</h2>
</header>
<section
ref="callout"
class="bs-callout bs-callout-info mb-3 m-md-1 text-secondary"
v-html="calloutContent"
></section>
<section ref="featuresTable" class="mt-0">
<div class="gl-responsive-table-row table-row-header text-2 font-weight-bold px-2" role="row">
<div class="table-section section-80">
{{ s__('SecurityConfiguration|Secure features') }}
</div>
<div class="table-section section-20">{{ s__('SecurityConfiguration|Status') }}</div>
</div>
<div
v-for="feature in features"
ref="featureRow"
:key="feature.name"
class="gl-responsive-table-row flex-md-column align-items-md-stretch px-2"
>
<div class="d-md-flex align-items-center">
<div class="table-section section-80 section-wrap pr-md-3">
<div role="rowheader" class="table-mobile-header">
{{ s__('SecurityConfiguration|Feature') }}
</div>
<div class="table-mobile-content">
<div class="d-flex align-items-center justify-content-end justify-content-md-start">
<div class="text-2">
{{ feature.name }}
</div>
<gl-link
class="d-inline-flex ml-1"
target="_blank"
:href="feature.link"
:aria-label="s__('SecurityConfiguration|Feature documentation')"
><icon name="external-link"
/></gl-link>
</div>
<div class="text-secondary">
{{ feature.description }}
</div>
</div>
</div>
<div class="table-section section-20 section-wrap pr-md-3">
<div role="rowheader" class="table-mobile-header">
{{ s__('SecurityConfiguration|Status') }}
</div>
<div ref="featureConfigStatus" class="table-mobile-content">
{{
feature.configured
? s__('SecurityConfiguration|Configured')
: s__('SecurityConfiguration|Not yet configured')
}}
</div>
</div>
</div>
</div>
</section>
</article>
</template>
import Vue from 'vue';
import SecurityConfigurationApp from './components/app.vue';
export default function init() {
const el = document.getElementById('js-security-configuration');
const {
autoDevOpsEnabled,
autoDevOpsHelpPagePath,
features,
helpPagePath,
latestPipelinePath,
pipelinesHelpPagePath,
} = el.dataset;
return new Vue({
el,
components: {
SecurityConfigurationApp,
},
render(createElement) {
return createElement(SecurityConfigurationApp, {
props: {
autoDevOpsEnabled,
autoDevOpsHelpPagePath,
features: JSON.parse(features),
helpPagePath,
latestPipelinePath,
pipelinesHelpPagePath,
},
});
},
});
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Security Configuration App features table displays a given feature 1`] = `
<section
class="mt-0"
>
<div
class="gl-responsive-table-row table-row-header text-2 font-weight-bold px-2"
role="row"
>
<div
class="table-section section-80"
>
Secure features
</div>
<div
class="table-section section-20"
>
Status
</div>
</div>
<div
class="gl-responsive-table-row flex-md-column align-items-md-stretch px-2"
>
<div
class="d-md-flex align-items-center"
>
<div
class="table-section section-80 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
role="rowheader"
>
Feature
</div>
<div
class="table-mobile-content"
>
<div
class="d-flex align-items-center justify-content-end justify-content-md-start"
>
<div
class="text-2"
>
name-feature-0
</div>
<gllink-stub
aria-label="Feature documentation"
class="d-inline-flex ml-1"
href="link-feature-0"
target="_blank"
>
<icon-stub
name="external-link"
size="16"
/>
</gllink-stub>
</div>
<div
class="text-secondary"
>
description-feature-0
</div>
</div>
</div>
<div
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
role="rowheader"
>
Status
</div>
<div
class="table-mobile-content"
>
Not yet configured
</div>
</div>
</div>
</div>
</section>
`;
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import SecurityConfigurationApp from 'ee/security_configuration/components/app.vue';
const localVue = createLocalVue();
describe('Security Configuration App', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(SecurityConfigurationApp, {
localVue,
propsData: {
features: [],
autoDevOpsEnabled: false,
latestPipelinePath: 'http://latestPipelinePath',
autoDevOpsHelpPagePath: 'http://autoDevOpsHelpPagePath',
helpPagePath: 'http://helpPagePath',
pipelinesHelpPagePath: 'http://pipelineHelpPagePath',
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const generateFeatures = n =>
[...Array(n).keys()].map(i => ({
name: `name-feature-${i}`,
description: `description-feature-${i}`,
link: `link-feature-${i}`,
}));
const getHelpLink = () => wrapper.find('header').find(GlLink);
const getNotification = () => wrapper.find({ ref: 'callout' });
const getPipelinesLink = () => getNotification().find('a');
const getFeaturesTable = () => wrapper.find({ ref: 'featuresTable' });
const getFeatureConfigStatus = () => wrapper.find({ ref: 'featureConfigStatus' });
describe('header', () => {
it('displays a link to the given help page', () => {
const helpPagePath = 'http://foo';
createComponent({ helpPagePath });
expect(getHelpLink().attributes('href')).toBe(helpPagePath);
});
it.each`
autoDevOpsEnabled | latestPipelinePath | expectedUrl
${true} | ${'http://latestPipeline'} | ${'http://autoDevOpsHelpPagePath'}
${false} | ${'http://latestPipeline'} | ${'http://latestPipeline'}
${false} | ${undefined} | ${'http://pipelineHelpPagePath'}
`(
'displays a link to "$expectedUrl" when autoDevOps is "$autoDevOpsEnabled" and pipelinesPath is $latestPipelinePath',
({ autoDevOpsEnabled, latestPipelinePath, expectedUrl }) => {
createComponent({ autoDevOpsEnabled, latestPipelinePath });
expect(getPipelinesLink().attributes('href')).toBe(expectedUrl);
expect(getPipelinesLink().attributes('rel')).toBe('noopener');
},
);
});
describe('features table', () => {
it('displays a row for each given feature', () => {
const features = generateFeatures(5);
createComponent({ features });
expect(wrapper.findAll({ ref: 'featureRow' }).length).toBe(5);
});
it('displays a given feature', () => {
const features = generateFeatures(1);
createComponent({ features });
expect(getFeaturesTable().element).toMatchSnapshot();
});
it.each`
configured | statusText
${true} | ${'Configured'}
${false} | ${'Not yet configured'}
`(
`displays "$statusText" if the given feature's configuration status is: "$configured"`,
({ configured, statusText }) => {
const features = [{ configured }];
createComponent({ features });
expect(getFeatureConfigStatus().text()).toBe(statusText);
},
);
});
});
......@@ -1417,6 +1417,9 @@ msgstr ""
msgid "All projects"
msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
msgid "All users"
msgstr ""
......@@ -4422,6 +4425,9 @@ msgstr ""
msgid "Configure Prometheus"
msgstr ""
msgid "Configure Security %{wordBreakOpportunity}and Compliance"
msgstr ""
msgid "Configure Tracing"
msgstr ""
......@@ -15081,9 +15087,30 @@ msgstr ""
msgid "Security Reports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
msgid "Security configuration help link"
msgstr ""
msgid "Security dashboard"
msgstr ""
msgid "SecurityConfiguration|Configured"
msgstr ""
msgid "SecurityConfiguration|Feature"
msgstr ""
msgid "SecurityConfiguration|Feature documentation"
msgstr ""
msgid "SecurityConfiguration|Not yet configured"
msgstr ""
msgid "SecurityConfiguration|Secure features"
msgstr ""
msgid "SecurityConfiguration|Status"
msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
......@@ -16873,6 +16900,9 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
msgid "The configuration status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've configured a scan for the default branch, any subsequent feature branch you create will include the scan."
msgstr ""
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
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