Commit 2fd4e9c4 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '347414-coonnect-mutation-sec-training-config' into 'master'

Connect mutation for security training configuration

See merge request gitlab-org/gitlab!76570
parents 7e084da1 6e440f27
<script>
import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql';
export default {
components: {
......@@ -9,6 +10,7 @@ export default {
GlLink,
GlSkeletonLoader,
},
inject: ['projectPath'],
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
......@@ -16,6 +18,7 @@ export default {
},
data() {
return {
toggleLoading: false,
securityTrainingProviders: [],
};
},
......@@ -24,6 +27,37 @@ export default {
return this.$apollo.queries.securityTrainingProviders.loading;
},
},
methods: {
toggleProvider(selectedProviderId) {
const toggledProviders = this.securityTrainingProviders.map((provider) => ({
...provider,
...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
}));
this.storeEnabledProviders(toggledProviders);
},
storeEnabledProviders(toggledProviders) {
const enabledProviderIds = toggledProviders
.filter(({ isEnabled }) => isEnabled)
.map(({ id }) => id);
this.toggleLoading = true;
return this.$apollo
.mutate({
mutation: configureSecurityTrainingProvidersMutation,
variables: {
input: {
enabledProviders: enabledProviderIds,
fullPath: this.projectPath,
},
},
})
.then(() => {
this.toggleLoading = false;
});
},
},
};
</script>
......@@ -46,7 +80,13 @@ export default {
>
<gl-card>
<div class="gl-display-flex">
<gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" />
<gl-toggle
:value="isEnabled"
:label="__('Training mode')"
label-position="hidden"
:is-loading="toggleLoading"
@change="toggleProvider(id)"
/>
<div class="gl-ml-5">
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
<p>
......
mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
configureSecurityTrainingProviders(input: $input) @client {
securityTrainingProviders {
id
isEnabled
}
}
}
......@@ -2,38 +2,10 @@ 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,
},
];
},
},
};
import tempResolvers from './resolver';
export const initSecurityConfiguration = (el) => {
if (!el) {
......
import produce from 'immer';
import { __ } from '~/locale';
import securityTrainingProvidersQuery from './graphql/security_training_providers.query.graphql';
// 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 default {
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,
},
];
},
},
Mutation: {
configureSecurityTrainingProviders: (
_,
{ input: { enabledProviders, primaryProvider } },
{ cache },
) => {
const sourceData = cache.readQuery({
query: securityTrainingProvidersQuery,
});
const data = produce(sourceData.securityTrainingProviders, (draftData) => {
/* eslint-disable no-param-reassign */
draftData.forEach((provider) => {
provider.isPrimary = provider.id === primaryProvider;
provider.isEnabled =
provider.id === primaryProvider || enabledProviders.includes(provider.id);
});
});
return {
__typename: 'configureSecurityTrainingProvidersPayload',
securityTrainingProviders: data,
};
},
},
};
......@@ -4,8 +4,14 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { securityTrainingProviders, mockResolvers } from '../mock_data';
import {
securityTrainingProviders,
mockResolvers,
testProjectPath,
textProviderIds,
} from '../mock_data';
Vue.use(VueApollo);
......@@ -18,6 +24,9 @@ describe('TrainingProviderList component', () => {
mockApollo = createMockApollo([], mockResolvers);
wrapper = shallowMount(TrainingProviderList, {
provide: {
projectPath: testProjectPath,
},
apolloProvider: mockApollo,
});
};
......@@ -85,4 +94,36 @@ describe('TrainingProviderList component', () => {
});
});
});
describe('success mutation', () => {
const firstToggle = () => findToggles().at(0);
beforeEach(async () => {
jest.spyOn(mockApollo.defaultClient, 'mutate');
await waitForQueryToBeLoaded();
firstToggle().vm.$emit('change');
});
it('calls mutation when toggle is changed', () => {
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: configureSecurityTrainingProvidersMutation,
variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
}),
);
});
it.each`
loading | wait | desc
${true} | ${false} | ${'enables loading of GlToggle when mutation is called'}
${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'}
`('$desc', async ({ loading, wait }) => {
if (wait) {
await waitForPromises();
}
expect(firstToggle().props('isLoading')).toBe(loading);
});
});
});
export const testProjectPath = 'foo/bar';
export const textProviderIds = [101, 102];
export const securityTrainingProviders = [
{
id: 101,
id: textProviderIds[0],
name: 'Kontra',
description: 'Interactive developer security education.',
url: 'https://application.security/',
isEnabled: false,
},
{
id: 102,
id: textProviderIds[1],
name: 'SecureCodeWarrior',
description: 'Security training with guide and learning pathways.',
url: 'https://www.securecodewarrior.com/',
......
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