Commit 14b4bdd0 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '346593-loading-config-sec-training' into 'master'

Add loading state to security training config

See merge request gitlab-org/gitlab!76352
parents e3461eac 2beda150
...@@ -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-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 @client {
name
id
description
isEnabled
url
}
}
...@@ -2,10 +2,39 @@ import Vue from 'vue'; ...@@ -2,10 +2,39 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import { __ } from '~/locale';
import SecurityConfigurationApp from './components/app.vue'; import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants'; import { securityFeatures, complianceFeatures } from './components/constants';
import { augmentFeatures } from './utils'; import { augmentFeatures } from './utils';
// Note: this is behind a feature flag and only a placeholder
// until the actual GraphQL fields have been added
// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
export const tempResolvers = {
Query: {
securityTrainingProviders() {
return [
{
__typename: 'SecurityTrainingProvider',
id: 101,
name: __('Kontra'),
description: __('Interactive developer security education.'),
url: 'https://application.security/',
isEnabled: false,
},
{
__typename: 'SecurityTrainingProvider',
id: 102,
name: __('SecureCodeWarrior'),
description: __('Security training with guide and learning pathways.'),
url: 'https://www.securecodewarrior.com/',
isEnabled: true,
},
];
},
},
};
export const initSecurityConfiguration = (el) => { export const initSecurityConfiguration = (el) => {
if (!el) { if (!el) {
return null; return null;
...@@ -14,7 +43,7 @@ export const initSecurityConfiguration = (el) => { ...@@ -14,7 +43,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(tempResolvers),
}); });
const { const {
......
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 waitForPromises from 'helpers/wait_for_promises';
import { securityTrainingProviders } from '../mock_data';
const upgradePath = '/upgrade'; const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath'; const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
...@@ -38,10 +39,21 @@ const gitlabCiHistoryPath = 'test/historyPath'; ...@@ -38,10 +39,21 @@ 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;
let mockSecurityTrainingProvidersData;
const mockResolvers = {
Query: {
securityTrainingProviders() {
return securityTrainingProviders;
},
},
};
const createComponent = ({ const createComponent = ({
shouldShowCallout = true, shouldShowCallout = true,
...@@ -49,9 +61,11 @@ describe('App component', () => { ...@@ -49,9 +61,11 @@ describe('App component', () => {
...propsData ...propsData
}) => { }) => {
userCalloutDismissSpy = jest.fn(); userCalloutDismissSpy = jest.fn();
mockApollo = createMockApollo([], mockResolvers);
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(SecurityConfigurationApp, { mount(SecurityConfigurationApp, {
apolloProvider: mockApollo,
propsData, propsData,
provide: { provide: {
upgradePath, upgradePath,
...@@ -134,10 +148,14 @@ describe('App component', () => { ...@@ -134,10 +148,14 @@ describe('App component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mockApollo = null;
}); });
describe('basic structure', () => { describe('basic structure', () => {
beforeEach(() => { beforeEach(() => {
mockSecurityTrainingProvidersData = jest.fn();
mockSecurityTrainingProvidersData.mockResolvedValue(securityTrainingProviders);
createComponent({ createComponent({
augmentedSecurityFeatures: securityFeaturesMock, augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock, augmentedComplianceFeatures: complianceFeaturesMock,
...@@ -186,8 +204,9 @@ describe('App component', () => { ...@@ -186,8 +204,9 @@ describe('App component', () => {
expect(findSecurityViewHistoryLink().exists()).toBe(false); expect(findSecurityViewHistoryLink().exists()).toBe(false);
}); });
it('renders training provider list with correct props', () => { it('renders training provider list with correct props', async () => {
expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS); await waitForPromises();
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