From d91cacb23cb3b7d04eac320dafefa010450d0bca Mon Sep 17 00:00:00 2001 From: Samantha Ming <sming@gitlab.com> Date: Thu, 25 Nov 2021 15:15:33 +0100 Subject: [PATCH] Create configuration security training basic ui Add security training ui to security configuration. This is within the vulnerability management tab. --- .../security_configuration/components/app.vue | 28 ++++++++- .../components/training_provider_list.vue | 36 +++++++++++ locale/gitlab.pot | 15 +++++ .../components/app_spec.js | 11 +++- .../components/training_provider_list_spec.js | 60 +++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/security_configuration/components/training_provider_list.vue create mode 100644 spec/frontend/security_configuration/components/training_provider_list_spec.js diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index cd2add6407f2..62c30c941ebb 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -8,6 +8,7 @@ import AutoDevOpsAlert from './auto_dev_ops_alert.vue'; import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue'; import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants'; import FeatureCard from './feature_card.vue'; +import TrainingProviderList from './training_provider_list.vue'; import SectionLayout from './section_layout.vue'; import UpgradeBanner from './upgrade_banner.vue'; @@ -28,8 +29,28 @@ export const i18n = { securityTraining: s__('SecurityConfiguration|Security training'), }; +// This will be removed and replaced with GraphQL query: +// https://gitlab.com/gitlab-org/gitlab/-/issues/346480 +export const TRAINING_PROVIDERS = [ + { + id: 101, + name: __('Kontra'), + description: __('Interactive developer security education.'), + url: 'https://application.security/', + isEnabled: false, + }, + { + id: 102, + name: __('SecureCodeWarrior'), + description: __('Security training with guide and learning pathways.'), + url: 'https://www.securecodewarrior.com/', + isEnabled: true, + }, +]; + export default { i18n, + TRAINING_PROVIDERS, components: { AutoDevOpsAlert, AutoDevOpsEnabledAlert, @@ -43,6 +64,7 @@ export default { SectionLayout, UpgradeBanner, UserCalloutDismisser, + TrainingProviderList, }, mixins: [glFeatureFlagsMixin()], inject: ['projectPath'], @@ -240,7 +262,11 @@ export default { data-testid="vulnerability-management-tab" :title="$options.i18n.vulnerabilityManagement" > - <section-layout :heading="$options.i18n.securityTraining" /> + <section-layout :heading="$options.i18n.securityTraining"> + <template #features> + <training-provider-list :providers="$options.TRAINING_PROVIDERS" /> + </template> + </section-layout> </gl-tab> </gl-tabs> </article> diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue new file mode 100644 index 000000000000..160540f69895 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -0,0 +1,36 @@ +<script> +import { GlCard, GlToggle, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlCard, + GlToggle, + GlLink, + }, + props: { + providers: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <ul class="gl-list-style-none gl-m-0 gl-p-0"> + <li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6"> + <gl-card> + <div class="gl-display-flex"> + <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" /> + <div class="gl-ml-5"> + <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3> + <p> + {{ description }} + <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link> + </p> + </div> + </div> + </gl-card> + </li> + </ul> +</template> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9b5b254b522b..fa4169cc75c6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18846,6 +18846,9 @@ msgstr "" msgid "Integrations|can't exceed %{recipients_limit}" msgstr "" +msgid "Interactive developer security education." +msgstr "" + msgid "Interactive mode" msgstr "" @@ -20199,6 +20202,9 @@ msgstr "" msgid "Ki" msgstr "" +msgid "Kontra" +msgstr "" + msgid "Kroki" msgstr "" @@ -30674,6 +30680,9 @@ msgstr "" msgid "Secure token that identifies an external storage request." msgstr "" +msgid "SecureCodeWarrior" +msgstr "" + msgid "Security" msgstr "" @@ -30698,6 +30707,9 @@ msgstr "" msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})" msgstr "" +msgid "Security training with guide and learning pathways." +msgstr "" + msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability." msgstr "" @@ -36609,6 +36621,9 @@ msgstr "" msgid "Track your GitLab projects with GitLab for Slack." msgstr "" +msgid "Training mode" +msgstr "" + msgid "Transfer" msgstr "" diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js index 4e0ea6a97171..759844b941a4 100644 --- a/spec/frontend/security_configuration/components/app_spec.js +++ b/spec/frontend/security_configuration/components/app_spec.js @@ -5,7 +5,10 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import stubChildren from 'helpers/stub_children'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue'; +import SecurityConfigurationApp, { + i18n, + TRAINING_PROVIDERS, +} from '~/security_configuration/components/app.vue'; import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue'; import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue'; import { @@ -20,6 +23,7 @@ import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY, } from '~/security_configuration/components/constants'; import FeatureCard from '~/security_configuration/components/feature_card.vue'; +import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue'; import { @@ -78,6 +82,7 @@ describe('App component', () => { const findTabs = () => wrapper.findAllComponents(GlTab); const findByTestId = (id) => wrapper.findByTestId(id); const findFeatureCards = () => wrapper.findAllComponents(FeatureCard); + const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList); const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert'); const findLink = ({ href, text, container = wrapper }) => { const selector = `a[href="${href}"]`; @@ -180,6 +185,10 @@ describe('App component', () => { expect(findComplianceViewHistoryLink().exists()).toBe(false); expect(findSecurityViewHistoryLink().exists()).toBe(false); }); + + it('renders training provider list with correct props', () => { + expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS); + }); }); describe('Manage via MR Error Alert', () => { diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js new file mode 100644 index 000000000000..1169a977d442 --- /dev/null +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -0,0 +1,60 @@ +import { GlLink, GlToggle, GlCard } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; +import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue'; + +const DEFAULT_PROPS = { + providers: TRAINING_PROVIDERS, +}; + +describe('TrainingProviderList component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TrainingProviderList, { + propsData: { + ...DEFAULT_PROPS, + ...props, + }, + }); + }; + + const findCards = () => wrapper.findAllComponents(GlCard); + const findLinks = () => wrapper.findAllComponents(GlLink); + const findToggles = () => wrapper.findAllComponents(GlToggle); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('basic structure', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders correct amount of cards', () => { + expect(findCards()).toHaveLength(DEFAULT_PROPS.providers.length); + }); + + DEFAULT_PROPS.providers.forEach(({ name, description, url, isEnabled }, index) => { + it(`shows the name for card ${index}`, () => { + expect(findCards().at(index).text()).toContain(name); + }); + + it(`shows the description for card ${index}`, () => { + expect(findCards().at(index).text()).toContain(description); + }); + + it(`shows the learn more link for card ${index}`, () => { + expect(findLinks().at(index).attributes()).toEqual({ + target: '_blank', + href: url, + }); + }); + + it(`shows the toggle with the correct value for card ${index}`, () => { + expect(findToggles().at(index).props('value')).toEqual(isEnabled); + }); + }); + }); +}); -- GitLab