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';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.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 AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
......@@ -29,28 +30,8 @@ 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,
......@@ -107,6 +88,7 @@ export default {
return {
autoDevopsEnabledAlertDismissedProjects: [],
errorMessage: '',
securityTrainingProviders: [],
};
},
computed: {
......@@ -128,6 +110,11 @@ export default {
);
},
},
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
},
},
methods: {
dismissAutoDevopsEnabledAlert() {
const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
......@@ -264,7 +251,10 @@ export default {
>
<section-layout :heading="$options.i18n.securityTraining">
<template #features>
<training-provider-list :providers="$options.TRAINING_PROVIDERS" />
<training-provider-list
:loading="$apollo.queries.securityTrainingProviders.loading"
:providers="securityTrainingProviders"
/>
</template>
</section-layout>
</gl-tab>
......
<script>
import { GlCard, GlToggle, GlLink } from '@gitlab/ui';
import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
export default {
components: {
GlCard,
GlToggle,
GlLink,
GlSkeletonLoader,
},
props: {
providers: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<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">
<gl-card>
<div class="gl-display-flex">
......
query Query {
securityTrainingProviders {
name
id
description
isEnabled
url
}
}
......@@ -19017,9 +19017,6 @@ msgstr ""
msgid "Integrations|can't exceed %{recipients_limit}"
msgstr ""
msgid "Interactive developer security education."
msgstr ""
msgid "Interactive mode"
msgstr ""
......@@ -20349,9 +20346,6 @@ msgstr ""
msgid "Ki"
msgstr ""
msgid "Kontra"
msgstr ""
msgid "Kroki"
msgstr ""
......@@ -30860,9 +30854,6 @@ msgstr ""
msgid "Secure token that identifies an external storage request."
msgstr ""
msgid "SecureCodeWarrior"
msgstr ""
msgid "Security"
msgstr ""
......@@ -30887,9 +30878,6 @@ 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 ""
......
import { GlTab } from '@gitlab/ui';
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 { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SecurityConfigurationApp, {
i18n,
TRAINING_PROVIDERS,
} from '~/security_configuration/components/app.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import SecurityConfigurationApp, { i18n } 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 {
......@@ -30,6 +29,8 @@ import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} 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 autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
......@@ -38,10 +39,12 @@ const gitlabCiHistoryPath = 'test/historyPath';
const projectPath = 'namespace/project';
useLocalStorageSpy();
Vue.use(VueApollo);
describe('App component', () => {
let wrapper;
let userCalloutDismissSpy;
let mockApollo;
const createComponent = ({
shouldShowCallout = true,
......@@ -49,9 +52,16 @@ describe('App component', () => {
...propsData
}) => {
userCalloutDismissSpy = jest.fn();
mockApollo = createMockApollo([
[
securityTrainingProvidersQuery,
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
],
]);
wrapper = extendedWrapper(
mount(SecurityConfigurationApp, {
apolloProvider: mockApollo,
propsData,
provide: {
upgradePath,
......@@ -134,6 +144,7 @@ describe('App component', () => {
afterEach(() => {
wrapper.destroy();
mockApollo = null;
});
describe('basic structure', () => {
......@@ -187,7 +198,7 @@ describe('App component', () => {
});
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 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 = {
providers: TRAINING_PROVIDERS,
providers: securityTrainingProviders,
};
describe('TrainingProviderList component', () => {
......@@ -22,6 +22,7 @@ describe('TrainingProviderList component', () => {
const findCards = () => wrapper.findAllComponents(GlCard);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
afterEach(() => {
wrapper.destroy();
......@@ -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