diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue index dea94503e62c033b1b2d82de118f23685bad7704..539e2bff17c2a423f81f47f2ff3ad87a5d680115 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -49,7 +49,7 @@ export default { data() { return { errorMessage: '', - toggleLoading: false, + providerLoadingId: null, securityTrainingProviders: [], hasTouchedConfiguration: false, }; @@ -89,37 +89,29 @@ export default { Sentry.captureException(e); } }, - toggleProvider(selectedProviderId) { - const toggledProviders = this.securityTrainingProviders.map((provider) => ({ - ...provider, - ...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }), - })); + toggleProvider(provider) { + const { isEnabled } = provider; + const toggledIsEnabled = !isEnabled; - const enabledProviderIds = toggledProviders - .filter(({ isEnabled }) => isEnabled) - .map(({ id }) => id); - - const { isEnabled: selectedProviderIsEnabled } = toggledProviders.find( - (provider) => provider.id === selectedProviderId, - ); - - this.trackProviderToggle(selectedProviderId, selectedProviderIsEnabled); - this.storeEnabledProviders(enabledProviderIds); + this.trackProviderToggle(provider.id, toggledIsEnabled); + this.storeProvider({ ...provider, isEnabled: toggledIsEnabled }); }, - async storeEnabledProviders(enabledProviderIds) { - this.toggleLoading = true; + async storeProvider({ id, isEnabled, isPrimary }) { + this.providerLoadingId = id; try { const { data: { - configureSecurityTrainingProviders: { errors = [] }, + securityTrainingUpdate: { errors = [] }, }, } = await this.$apollo.mutate({ mutation: configureSecurityTrainingProvidersMutation, variables: { input: { - enabledProviders: enabledProviderIds, - fullPath: this.projectFullPath, + projectPath: this.projectFullPath, + providerId: id, + isEnabled, + isPrimary, }, }, }); @@ -133,7 +125,7 @@ export default { } catch { this.errorMessage = this.$options.i18n.configMutationErrorMessage; } finally { - this.toggleLoading = false; + this.providerLoadingId = null; } }, trackProviderToggle(providerId, providerIsEnabled) { @@ -166,25 +158,21 @@ export default { </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 securityTrainingProviders" - :key="id" - class="gl-mb-6" - > + <li v-for="provider in securityTrainingProviders" :key="provider.id" class="gl-mb-6"> <gl-card> <div class="gl-display-flex"> <gl-toggle - :value="isEnabled" + :value="provider.isEnabled" :label="__('Training mode')" label-position="hidden" - :is-loading="toggleLoading" - @change="toggleProvider(id)" + :is-loading="providerLoadingId === provider.id" + @change="toggleProvider(provider)" /> <div class="gl-ml-5"> - <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3> + <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3> <p> - {{ description }} - <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link> + {{ provider.description }} + <gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link> </p> </div> </div> diff --git a/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql index 660e0fadafb5ab997e827c3dab84694590cbe5ba..3528bfaf7b8601db469138ff462a47dd7a34ded6 100644 --- a/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql +++ b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql @@ -1,9 +1,10 @@ -mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) { - configureSecurityTrainingProviders(input: $input) @client { +mutation updateSecurityTraining($input: SecurityTrainingUpdateInput!) { + securityTrainingUpdate(input: $input) { errors - securityTrainingProviders { + training { id isEnabled + isPrimary } } } diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql index a8326bb1968d9adeae23520963ae56ad15fe4120..2baeda318f3a9fceb247f600c5af6b8873f88654 100644 --- a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql +++ b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql @@ -5,6 +5,7 @@ query getSecurityTrainingProviders($fullPath: ID!) { name id description + isPrimary isEnabled url } diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js index c8255a010b279443442acfa3db161b4f2a8aea23..8416692dd27a0f19fabe822944706c9a766a9d96 100644 --- a/app/assets/javascripts/security_configuration/index.js +++ b/app/assets/javascripts/security_configuration/index.js @@ -5,7 +5,6 @@ import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; import SecurityConfigurationApp from './components/app.vue'; import { securityFeatures, complianceFeatures } from './components/constants'; import { augmentFeatures } from './utils'; -import tempResolvers from './resolver'; export const initSecurityConfiguration = (el) => { if (!el) { @@ -15,7 +14,7 @@ export const initSecurityConfiguration = (el) => { Vue.use(VueApollo); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(tempResolvers), + defaultClient: createDefaultClient(), }); const { diff --git a/app/assets/javascripts/security_configuration/resolver.js b/app/assets/javascripts/security_configuration/resolver.js deleted file mode 100644 index 51c02839a47cb0888308fbd9862ef39650912e49..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/security_configuration/resolver.js +++ /dev/null @@ -1,60 +0,0 @@ -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, fullPath } }, - { cache }, - ) => { - const sourceData = cache.readQuery({ - query: securityTrainingProvidersQuery, - variables: { - fullPath, - }, - }); - - const data = produce(sourceData.project, (draftData) => { - /* eslint-disable no-param-reassign */ - draftData.securityTrainingProviders.forEach((provider) => { - provider.isPrimary = provider.id === primaryProvider; - provider.isEnabled = - provider.id === primaryProvider || enabledProviders.includes(provider.id); - }); - }); - - return { - __typename: 'configureSecurityTrainingProvidersPayload', - securityTrainingProviders: data.securityTrainingProviders, - }; - }, - }, -}; diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/provider.js b/ee/app/assets/javascripts/security_dashboard/graphql/provider.js index 055ec1b059b40b679105475adefa34a0100d81a1..9153c5252b359d27bc54a650272d06f95dbf94d2 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/provider.js +++ b/ee/app/assets/javascripts/security_dashboard/graphql/provider.js @@ -1,13 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import tempResolvers from '~/security_configuration/resolver'; Vue.use(VueApollo); -const defaultClient = createDefaultClient({ - ...tempResolvers, -}); +const defaultClient = createDefaultClient(); export default new VueApollo({ defaultClient, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8769f0fcede5bea0353b3326aa7b91a5a7acc8e8..6eddbc54e6ebf3f3d300ea78b5a666520fd0429f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19717,9 +19717,6 @@ msgstr "" msgid "Integrations|can't exceed %{recipients_limit}" msgstr "" -msgid "Interactive developer security education." -msgstr "" - msgid "Interactive mode" msgstr "" @@ -21058,9 +21055,6 @@ msgstr "" msgid "Ki" msgstr "" -msgid "Kontra" -msgstr "" - msgid "Kroki" msgstr "" @@ -32018,9 +32012,6 @@ msgstr "" msgid "Secure token that identifies an external storage request." msgstr "" -msgid "SecureCodeWarrior" -msgstr "" - msgid "Security" msgstr "" @@ -32045,9 +32036,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 "" diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index ea5df9ca550655af9ab6b9bf833a92dac6ed43fc..18c9ada6bde9c189265f9e8333416f69a901629a 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -19,6 +19,8 @@ import { dismissUserCalloutErrorResponse, securityTrainingProviders, securityTrainingProvidersResponse, + updateSecurityTrainingProvidersResponse, + updateSecurityTrainingProvidersErrorResponse, testProjectPath, textProviderIds, } from '../mock_data'; @@ -29,18 +31,22 @@ describe('TrainingProviderList component', () => { let wrapper; let apolloProvider; - const createApolloProvider = ({ resolvers, handlers = [] } = {}) => { + const createApolloProvider = ({ handlers = [] } = {}) => { const defaultHandlers = [ [ securityTrainingProvidersQuery, jest.fn().mockResolvedValue(securityTrainingProvidersResponse), ], + [ + configureSecurityTrainingProvidersMutation, + jest.fn().mockResolvedValue(updateSecurityTrainingProvidersResponse), + ], ]; // make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])]; - apolloProvider = createMockApollo(mergedHandlers, resolvers); + apolloProvider = createMockApollo(mergedHandlers); }; const createComponent = () => { @@ -62,7 +68,7 @@ describe('TrainingProviderList component', () => { const findLoader = () => wrapper.findComponent(GlSkeletonLoader); const findErrorAlert = () => wrapper.findComponent(GlAlert); - const toggleFirstProvider = () => findFirstToggle().vm.$emit('change'); + const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]); afterEach(() => { wrapper.destroy(); @@ -146,9 +152,9 @@ describe('TrainingProviderList component', () => { beforeEach(async () => { jest.spyOn(apolloProvider.defaultClient, 'mutate'); - await waitForMutationToBeLoaded(); + await waitForQueryToBeLoaded(); - toggleFirstProvider(); + await toggleFirstProvider(); }); it.each` @@ -166,7 +172,14 @@ describe('TrainingProviderList component', () => { expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith( expect.objectContaining({ mutation: configureSecurityTrainingProvidersMutation, - variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } }, + variables: { + input: { + providerId: textProviderIds[0], + isEnabled: true, + isPrimary: false, + projectPath: testProjectPath, + }, + }, }), ); }); @@ -264,14 +277,12 @@ describe('TrainingProviderList component', () => { describe('when storing training provider configurations', () => { beforeEach(async () => { createApolloProvider({ - resolvers: { - Mutation: { - configureSecurityTrainingProviders: () => ({ - errors: ['something went wrong!'], - securityTrainingProviders: [], - }), - }, - }, + handlers: [ + [ + configureSecurityTrainingProvidersMutation, + jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse), + ], + ], }); createComponent(); diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index e47a255fbac3beb8b09918c14cdc1c12b58ca1ad..b042e8704679560694f6543eb144818554281cc6 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -9,6 +9,7 @@ export const securityTrainingProviders = [ description: 'Interactive developer security education', url: 'https://www.example.org/security/training', isEnabled: false, + isPrimary: false, }, { id: textProviderIds[1], @@ -16,6 +17,7 @@ export const securityTrainingProviders = [ description: 'Security training with guide and learning pathways.', url: 'https://www.vendornametwo.com/', isEnabled: true, + isPrimary: false, }, ]; @@ -51,3 +53,26 @@ export const dismissUserCalloutErrorResponse = { }, }, }; + +export const updateSecurityTrainingProvidersResponse = { + data: { + securityTrainingUpdate: { + errors: [], + training: { + id: 101, + name: 'Acme', + isEnabled: true, + isPrimary: false, + }, + }, + }, +}; + +export const updateSecurityTrainingProvidersErrorResponse = { + data: { + securityTrainingUpdate: { + errors: ['something went wrong!'], + training: null, + }, + }, +};