diff options
Diffstat (limited to 'app/assets/javascripts/security_configuration')
7 files changed, 195 insertions, 65 deletions
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index 75d2b324623..d228f77f27d 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -27,6 +27,9 @@ export const i18n = { securityConfiguration: __('Security Configuration'), vulnerabilityManagement: s__('SecurityConfiguration|Vulnerability Management'), securityTraining: s__('SecurityConfiguration|Security training'), + securityTrainingDescription: s__( + 'SecurityConfiguration|Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability.', + ), }; export default { @@ -160,8 +163,12 @@ export default { </template> </user-callout-dismisser> - <gl-tabs content-class="gl-pt-0"> - <gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting"> + <gl-tabs content-class="gl-pt-0" sync-active-tab-with-query-params lazy> + <gl-tab + data-testid="security-testing-tab" + :title="$options.i18n.securityTesting" + query-param-value="security-testing" + > <auto-dev-ops-enabled-alert v-if="shouldShowAutoDevopsEnabledAlert" class="gl-mt-3" @@ -185,9 +192,12 @@ export default { {{ $options.i18n.description }} </p> <p v-if="canViewCiHistory"> - <gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{ - $options.i18n.configurationHistory - }}</gl-link> + <gl-link + data-testid="security-view-history-link" + data-qa-selector="security_configuration_history_link" + :href="gitlabCiHistoryPath" + >{{ $options.i18n.configurationHistory }}</gl-link + > </p> </template> @@ -203,7 +213,11 @@ export default { </template> </section-layout> </gl-tab> - <gl-tab data-testid="compliance-testing-tab" :title="$options.i18n.compliance"> + <gl-tab + data-testid="compliance-testing-tab" + :title="$options.i18n.compliance" + query-param-value="compliance-testing" + > <section-layout :heading="$options.i18n.compliance"> <template #description> <p> @@ -241,8 +255,14 @@ export default { v-if="glFeatures.secureVulnerabilityTraining" data-testid="vulnerability-management-tab" :title="$options.i18n.vulnerabilityManagement" + query-param-value="vulnerability-management" > <section-layout :heading="$options.i18n.securityTraining"> + <template #description> + <p> + {{ $options.i18n.securityTrainingDescription }} + </p> + </template> <template #features> <training-provider-list /> </template> diff --git a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue index ce6a1b4888b..315f676e659 100644 --- a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue +++ b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue @@ -28,6 +28,7 @@ export default { variant="info" :primary-button-link="autoDevopsPath" :primary-button-text="$options.i18n.primaryButtonText" + data-qa-selector="autodevops_container" @dismiss="dismissMethod" > <gl-sprintf :message="$options.i18n.body"> diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index dd8ba72ad1f..034dba29196 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -254,7 +254,7 @@ export const securityFeatures = [ helpPath: COVERAGE_FUZZING_HELP_PATH, configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH, type: REPORT_TYPE_COVERAGE_FUZZING, - secondary: gon?.features?.corpusManagement + secondary: gon?.features?.corpusManagementUi ? { type: REPORT_TYPE_CORPUS_MANAGEMENT, name: CORPUS_MANAGEMENT_NAME, 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 509377a63e8..ca4596e16b3 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -1,21 +1,39 @@ <script> -import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui'; +import { GlAlert, GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui'; +import { __ } from '~/locale'; import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql'; +import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql'; + +const i18n = { + providerQueryErrorMessage: __( + 'Could not fetch training providers. Please refresh the page, or try again later.', + ), + configMutationErrorMessage: __( + 'Could not save configuration. Please refresh the page, or try again later.', + ), +}; export default { components: { + GlAlert, GlCard, GlToggle, GlLink, GlSkeletonLoader, }, + inject: ['projectPath'], apollo: { securityTrainingProviders: { query: securityTrainingProvidersQuery, + error() { + this.errorMessage = this.$options.i18n.providerQueryErrorMessage; + }, }, }, data() { return { + errorMessage: '', + toggleLoading: false, securityTrainingProviders: [], }; }, @@ -24,38 +42,92 @@ export default { return this.$apollo.queries.securityTrainingProviders.loading; }, }, + methods: { + toggleProvider(selectedProviderId) { + const toggledProviders = this.securityTrainingProviders.map((provider) => ({ + ...provider, + ...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }), + })); + + const enabledProviderIds = toggledProviders + .filter(({ isEnabled }) => isEnabled) + .map(({ id }) => id); + + this.storeEnabledProviders(toggledProviders, enabledProviderIds); + }, + async storeEnabledProviders(toggledProviders, enabledProviderIds) { + this.toggleLoading = true; + + try { + const { + data: { + configureSecurityTrainingProviders: { errors = [] }, + }, + } = await this.$apollo.mutate({ + mutation: configureSecurityTrainingProvidersMutation, + variables: { + input: { + enabledProviders: enabledProviderIds, + fullPath: this.projectPath, + }, + }, + }); + + if (errors.length > 0) { + // throwing an error here means we can handle scenarios within the `catch` block below + throw new Error(); + } + } catch { + this.errorMessage = this.$options.i18n.configMutationErrorMessage; + } finally { + this.toggleLoading = false; + } + }, + }, + i18n, }; </script> <template> - <div - v-if="isLoading" - 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 securityTrainingProviders" - :key="id" - class="gl-mb-6" + <div> + <gl-alert v-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-6"> + {{ errorMessage }} + </gl-alert> + <div + v-if="isLoading" + class="gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100" > - <gl-card> - <div class="gl-display-flex"> - <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" /> - <div class="gl-ml-5"> - <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3> - <p> - {{ description }} - <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link> - </p> + <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 securityTrainingProviders" + :key="id" + class="gl-mb-6" + > + <gl-card> + <div class="gl-display-flex"> + <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> + {{ description }} + <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link> + </p> + </div> </div> - </div> - </gl-card> - </li> - </ul> + </gl-card> + </li> + </ul> + </div> </template> 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 new file mode 100644 index 00000000000..660e0fadafb --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql @@ -0,0 +1,9 @@ +mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) { + configureSecurityTrainingProviders(input: $input) @client { + errors + securityTrainingProviders { + id + isEnabled + } + } +} diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js index c86ff1a58f2..24c0585e077 100644 --- a/app/assets/javascripts/security_configuration/index.js +++ b/app/assets/javascripts/security_configuration/index.js @@ -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) { diff --git a/app/assets/javascripts/security_configuration/resolver.js b/app/assets/javascripts/security_configuration/resolver.js new file mode 100644 index 00000000000..93175d4a3d1 --- /dev/null +++ b/app/assets/javascripts/security_configuration/resolver.js @@ -0,0 +1,56 @@ +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, + }; + }, + }, +}; |