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';
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-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 @client {
name
id
description
isEnabled
url
}
}
......@@ -2,10 +2,39 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import { __ } from '~/locale';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
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) => {
if (!el) {
return null;
......@@ -14,7 +43,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient(tempResolvers),
});
const {
......
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 waitForPromises from 'helpers/wait_for_promises';
import { securityTrainingProviders } from '../mock_data';
const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
......@@ -38,10 +39,21 @@ const gitlabCiHistoryPath = 'test/historyPath';
const projectPath = 'namespace/project';
useLocalStorageSpy();
Vue.use(VueApollo);
describe('App component', () => {
let wrapper;
let userCalloutDismissSpy;
let mockApollo;
let mockSecurityTrainingProvidersData;
const mockResolvers = {
Query: {
securityTrainingProviders() {
return securityTrainingProviders;
},
},
};
const createComponent = ({
shouldShowCallout = true,
......@@ -49,9 +61,11 @@ describe('App component', () => {
...propsData
}) => {
userCalloutDismissSpy = jest.fn();
mockApollo = createMockApollo([], mockResolvers);
wrapper = extendedWrapper(
mount(SecurityConfigurationApp, {
apolloProvider: mockApollo,
propsData,
provide: {
upgradePath,
......@@ -134,10 +148,14 @@ describe('App component', () => {
afterEach(() => {
wrapper.destroy();
mockApollo = null;
});
describe('basic structure', () => {
beforeEach(() => {
mockSecurityTrainingProvidersData = jest.fn();
mockSecurityTrainingProvidersData.mockResolvedValue(securityTrainingProviders);
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
......@@ -186,8 +204,9 @@ describe('App component', () => {
expect(findSecurityViewHistoryLink().exists()).toBe(false);
});
it('renders training provider list with correct props', () => {
expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS);
it('renders training provider list with correct props', async () => {
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 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