Commit efa7c501 authored by Samantha Ming's avatar Samantha Ming

Add loading state to security training config

Utilize the loading skeleton when trainers are being fetched
parent 9618f99c
...@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale'; ...@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue'; import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue'; import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants'; import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
...@@ -29,28 +30,8 @@ export const i18n = { ...@@ -29,28 +30,8 @@ export const i18n = {
securityTraining: s__('SecurityConfiguration|Security training'), 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 { export default {
i18n, i18n,
TRAINING_PROVIDERS,
components: { components: {
AutoDevOpsAlert, AutoDevOpsAlert,
AutoDevOpsEnabledAlert, AutoDevOpsEnabledAlert,
...@@ -107,6 +88,7 @@ export default { ...@@ -107,6 +88,7 @@ export default {
return { return {
autoDevopsEnabledAlertDismissedProjects: [], autoDevopsEnabledAlertDismissedProjects: [],
errorMessage: '', errorMessage: '',
securityTrainingProviders: [],
}; };
}, },
computed: { computed: {
...@@ -128,6 +110,11 @@ export default { ...@@ -128,6 +110,11 @@ export default {
); );
}, },
}, },
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
},
},
methods: { methods: {
dismissAutoDevopsEnabledAlert() { dismissAutoDevopsEnabledAlert() {
const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects); const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
...@@ -264,7 +251,10 @@ export default { ...@@ -264,7 +251,10 @@ export default {
> >
<section-layout :heading="$options.i18n.securityTraining"> <section-layout :heading="$options.i18n.securityTraining">
<template #features> <template #features>
<training-provider-list :providers="$options.TRAINING_PROVIDERS" /> <training-provider-list
:loading="$apollo.queries.securityTrainingProviders.loading"
:providers="securityTrainingProviders"
/>
</template> </template>
</section-layout> </section-layout>
</gl-tab> </gl-tab>
......
<script> <script>
import { GlCard, GlToggle, GlLink } from '@gitlab/ui'; import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
export default { export default {
components: { components: {
GlCard, GlCard,
GlToggle, GlToggle,
GlLink, GlLink,
GlSkeletonLoader,
}, },
props: { props: {
providers: { providers: {
type: Array, type: Array,
required: true, required: true,
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
}; };
</script> </script>
<template> <template>
<ul class="gl-list-style-none gl-m-0 gl-p-0"> <div
v-if="loading"
class="gl-mb-6 gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
>
<gl-skeleton-loader :width="350" :height="44">
<rect width="200" height="8" x="10" y="0" rx="4" />
<rect width="300" height="8" x="10" y="15" rx="4" />
<rect width="100" height="8" x="10" y="35" rx="4" />
</gl-skeleton-loader>
</div>
<ul v-else 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"> <li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6">
<gl-card> <gl-card>
<div class="gl-display-flex"> <div class="gl-display-flex">
......
query Query {
securityTrainingProviders {
name
id
description
isEnabled
url
}
}
...@@ -19017,9 +19017,6 @@ msgstr "" ...@@ -19017,9 +19017,6 @@ msgstr ""
msgid "Integrations|can't exceed %{recipients_limit}" msgid "Integrations|can't exceed %{recipients_limit}"
msgstr "" msgstr ""
msgid "Interactive developer security education."
msgstr ""
msgid "Interactive mode" msgid "Interactive mode"
msgstr "" msgstr ""
...@@ -20349,9 +20346,6 @@ msgstr "" ...@@ -20349,9 +20346,6 @@ msgstr ""
msgid "Ki" msgid "Ki"
msgstr "" msgstr ""
msgid "Kontra"
msgstr ""
msgid "Kroki" msgid "Kroki"
msgstr "" msgstr ""
...@@ -30860,9 +30854,6 @@ msgstr "" ...@@ -30860,9 +30854,6 @@ msgstr ""
msgid "Secure token that identifies an external storage request." msgid "Secure token that identifies an external storage request."
msgstr "" msgstr ""
msgid "SecureCodeWarrior"
msgstr ""
msgid "Security" msgid "Security"
msgstr "" msgstr ""
...@@ -30887,9 +30878,6 @@ msgstr "" ...@@ -30887,9 +30878,6 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})" msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr "" 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." msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
msgstr "" msgstr ""
......
import { GlTab } from '@gitlab/ui'; import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SecurityConfigurationApp, { import createMockApollo from 'helpers/mock_apollo_helper';
i18n, import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
TRAINING_PROVIDERS,
} from '~/security_configuration/components/app.vue';
import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.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 AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
import { import {
...@@ -30,6 +29,8 @@ import { ...@@ -30,6 +29,8 @@ import {
REPORT_TYPE_LICENSE_COMPLIANCE, REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST, REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants'; } from '~/vue_shared/security_reports/constants';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import { securityTrainingProvidersResponse, securityTrainingProviders } from '../mock_data';
const upgradePath = '/upgrade'; const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath'; const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
...@@ -38,10 +39,12 @@ const gitlabCiHistoryPath = 'test/historyPath'; ...@@ -38,10 +39,12 @@ const gitlabCiHistoryPath = 'test/historyPath';
const projectPath = 'namespace/project'; const projectPath = 'namespace/project';
useLocalStorageSpy(); useLocalStorageSpy();
Vue.use(VueApollo);
describe('App component', () => { describe('App component', () => {
let wrapper; let wrapper;
let userCalloutDismissSpy; let userCalloutDismissSpy;
let mockApollo;
const createComponent = ({ const createComponent = ({
shouldShowCallout = true, shouldShowCallout = true,
...@@ -49,9 +52,16 @@ describe('App component', () => { ...@@ -49,9 +52,16 @@ describe('App component', () => {
...propsData ...propsData
}) => { }) => {
userCalloutDismissSpy = jest.fn(); userCalloutDismissSpy = jest.fn();
mockApollo = createMockApollo([
[
securityTrainingProvidersQuery,
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
],
]);
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(SecurityConfigurationApp, { mount(SecurityConfigurationApp, {
apolloProvider: mockApollo,
propsData, propsData,
provide: { provide: {
upgradePath, upgradePath,
...@@ -134,6 +144,7 @@ describe('App component', () => { ...@@ -134,6 +144,7 @@ describe('App component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mockApollo = null;
}); });
describe('basic structure', () => { describe('basic structure', () => {
...@@ -187,7 +198,7 @@ describe('App component', () => { ...@@ -187,7 +198,7 @@ describe('App component', () => {
}); });
it('renders training provider list with correct props', () => { it('renders training provider list with correct props', () => {
expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS); expect(findTrainingProviderList().props('providers')).toEqual(securityTrainingProviders);
}); });
}); });
......
import { GlLink, GlToggle, GlCard } from '@gitlab/ui'; import { GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue'; import { securityTrainingProviders } from '../mock_data';
const DEFAULT_PROPS = { const DEFAULT_PROPS = {
providers: TRAINING_PROVIDERS, providers: securityTrainingProviders,
}; };
describe('TrainingProviderList component', () => { describe('TrainingProviderList component', () => {
...@@ -22,6 +22,7 @@ describe('TrainingProviderList component', () => { ...@@ -22,6 +22,7 @@ describe('TrainingProviderList component', () => {
const findCards = () => wrapper.findAllComponents(GlCard); const findCards = () => wrapper.findAllComponents(GlCard);
const findLinks = () => wrapper.findAllComponents(GlLink); const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle); const findToggles = () => wrapper.findAllComponents(GlToggle);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -57,4 +58,23 @@ describe('TrainingProviderList component', () => { ...@@ -57,4 +58,23 @@ describe('TrainingProviderList component', () => {
}); });
}); });
}); });
describe('loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('shows the loader', () => {
expect(findLoader().exists()).toBe(true);
});
it('does not show the cards', () => {
expect(findCards().exists()).toBe(false);
});
it('does not show loader when not loading', () => {
createComponent({ loading: false });
expect(findLoader().exists()).toBe(false);
});
});
}); });
export const securityTrainingProviders = [
{
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 const securityTrainingProvidersResponse = {
data: {
securityTrainingProviders,
},
};
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