diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /app/assets/javascripts/security_configuration | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) | |
download | gitlab-ce-41fe97390ceddf945f3d967b8fdb3de4c66b7dea.tar.gz |
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/security_configuration')
6 files changed, 207 insertions, 27 deletions
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index 81d222438e3..39a2939f52a 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -16,6 +16,8 @@ import { REPORT_TYPE_LICENSE_COMPLIANCE, } from '~/vue_shared/security_reports/constants'; +import kontraLogo from 'images/vulnerability/kontra-logo.svg'; +import scwLogo from 'images/vulnerability/scw-logo.svg'; import configureSastMutation from '../graphql/configure_sast.mutation.graphql'; import configureSastIacMutation from '../graphql/configure_iac.mutation.graphql'; import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql'; @@ -222,14 +224,12 @@ export const securityFeatures = [ helpPath: COVERAGE_FUZZING_HELP_PATH, configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH, type: REPORT_TYPE_COVERAGE_FUZZING, - secondary: gon?.features?.corpusManagementUi - ? { - type: REPORT_TYPE_CORPUS_MANAGEMENT, - name: CORPUS_MANAGEMENT_NAME, - description: CORPUS_MANAGEMENT_DESCRIPTION, - configurationText: CORPUS_MANAGEMENT_CONFIG_TEXT, - } - : {}, + secondary: { + type: REPORT_TYPE_CORPUS_MANAGEMENT, + name: CORPUS_MANAGEMENT_NAME, + description: CORPUS_MANAGEMENT_DESCRIPTION, + configurationText: CORPUS_MANAGEMENT_CONFIG_TEXT, + }, }, ]; @@ -281,3 +281,21 @@ export const featureToMutationMap = { export const AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY = 'security_configuration_auto_devops_enabled_dismissed_projects'; + +// Fetch the svg path from the GraphQL query once this issue is resolved +// https://gitlab.com/gitlab-org/gitlab/-/issues/346899 +export const TEMP_PROVIDER_LOGOS = { + Kontra: { + svg: kontraLogo, + }, + [__('Secure Code Warrior')]: { + svg: scwLogo, + }, +}; + +// Use the `url` field from the GraphQL query once this issue is resolved +// https://gitlab.com/gitlab-org/gitlab/-/issues/356129 +export const TEMP_PROVIDER_URLS = { + Kontra: 'https://application.security/', + [__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/', +}; diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue index 1c37d8008de..cd5ad86e1a8 100644 --- a/app/assets/javascripts/security_configuration/components/feature_card.vue +++ b/app/assets/javascripts/security_configuration/components/feature_card.vue @@ -31,13 +31,12 @@ export default { const button = this.enabled ? { text: this.$options.i18n.configureFeature, - category: 'secondary', } : { text: this.$options.i18n.enableFeature, - category: 'primary', }; + button.category = 'secondary'; button.text = sprintf(button.text, { feature: this.shortName }); return button; @@ -126,7 +125,7 @@ export default { v-else-if="showManageViaMr" :feature="feature" variant="confirm" - category="primary" + category="secondary" class="gl-mt-5" :data-qa-selector="`${feature.type}_mr_button`" @error="onError" 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 539e2bff17c..bb540303cfd 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -1,15 +1,31 @@ <script> -import { GlAlert, GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui'; +import { + GlAlert, + GlTooltipDirective, + GlCard, + GlToggle, + GlLink, + GlSkeletonLoader, + GlIcon, + GlSafeHtmlDirective, +} from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import Tracking from '~/tracking'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; import { TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, TRACK_TOGGLE_TRAINING_PROVIDER_LABEL, + TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, + TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL, } from '~/security_configuration/constants'; import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; -import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql'; -import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql'; +import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; +import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql'; +import { + updateSecurityTrainingCache, + updateSecurityTrainingOptimisticResponse, +} from '~/security_configuration/graphql/cache_utils'; +import { TEMP_PROVIDER_LOGOS, TEMP_PROVIDER_URLS } from './constants'; const i18n = { providerQueryErrorMessage: __( @@ -18,6 +34,10 @@ const i18n = { configMutationErrorMessage: __( 'Could not save configuration. Please refresh the page, or try again later.', ), + primaryTraining: s__('SecurityTraining|Primary Training'), + primaryTrainingDescription: s__( + 'SecurityTraining|Training from this partner takes precedence when more than one training partner is enabled.', + ), }; export default { @@ -27,6 +47,11 @@ export default { GlToggle, GlLink, GlSkeletonLoader, + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, mixins: [Tracking.mixin()], inject: ['projectFullPath'], @@ -49,12 +74,14 @@ export default { data() { return { errorMessage: '', - providerLoadingId: null, securityTrainingProviders: [], hasTouchedConfiguration: false, }; }, computed: { + enabledProviders() { + return this.securityTrainingProviders.filter(({ isEnabled }) => isEnabled); + }, isLoading() { return this.$apollo.queries.securityTrainingProviders.loading; }, @@ -89,15 +116,41 @@ export default { Sentry.captureException(e); } }, - toggleProvider(provider) { - const { isEnabled } = provider; + async toggleProvider(provider) { + const { isEnabled, isPrimary } = provider; const toggledIsEnabled = !isEnabled; this.trackProviderToggle(provider.id, toggledIsEnabled); - this.storeProvider({ ...provider, isEnabled: toggledIsEnabled }); + + // when the current primary provider gets disabled then set the first enabled to be the new primary + if (!toggledIsEnabled && isPrimary && this.enabledProviders.length > 1) { + const firstOtherEnabledProvider = this.enabledProviders.find( + ({ id }) => id !== provider.id, + ); + this.setPrimaryProvider(firstOtherEnabledProvider); + } + + this.storeProvider({ + ...provider, + isEnabled: toggledIsEnabled, + }); }, - async storeProvider({ id, isEnabled, isPrimary }) { - this.providerLoadingId = id; + setPrimaryProvider(provider) { + this.storeProvider({ ...provider, isPrimary: true }); + }, + async storeProvider(provider) { + const { id, isEnabled, isPrimary } = provider; + let nextIsPrimary = isPrimary; + + // if the current provider has been disabled it can't be primary + if (!isEnabled) { + nextIsPrimary = false; + } + + // if the current provider is the only enabled provider it should be primary + if (isEnabled && !this.enabledProviders.length) { + nextIsPrimary = true; + } try { const { @@ -111,9 +164,18 @@ export default { projectPath: this.projectFullPath, providerId: id, isEnabled, - isPrimary, + isPrimary: nextIsPrimary, }, }, + optimisticResponse: updateSecurityTrainingOptimisticResponse({ + id, + isEnabled, + isPrimary: nextIsPrimary, + }), + update: updateSecurityTrainingCache({ + query: securityTrainingProvidersQuery, + variables: { fullPath: this.projectFullPath }, + }), }); if (errors.length > 0) { @@ -124,8 +186,6 @@ export default { this.hasTouchedConfiguration = true; } catch { this.errorMessage = this.$options.i18n.configMutationErrorMessage; - } finally { - this.providerLoadingId = null; } }, trackProviderToggle(providerId, providerIsEnabled) { @@ -137,8 +197,16 @@ export default { }, }); }, + trackProviderLearnMoreClick(providerId) { + this.track(TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, { + label: TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL, + property: providerId, + }); + }, }, i18n, + TEMP_PROVIDER_LOGOS, + TEMP_PROVIDER_URLS, }; </script> @@ -165,15 +233,54 @@ export default { :value="provider.isEnabled" :label="__('Training mode')" label-position="hidden" - :is-loading="providerLoadingId === provider.id" @change="toggleProvider(provider)" /> - <div class="gl-ml-5"> + <div v-if="$options.TEMP_PROVIDER_LOGOS[provider.name]" class="gl-ml-4"> + <div + v-safe-html="$options.TEMP_PROVIDER_LOGOS[provider.name].svg" + data-testid="provider-logo" + style="width: 18px" + role="presentation" + ></div> + </div> + <div class="gl-ml-3"> <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3> <p> {{ provider.description }} - <gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link> + <gl-link + v-if="$options.TEMP_PROVIDER_URLS[provider.name]" + :href="$options.TEMP_PROVIDER_URLS[provider.name]" + target="_blank" + @click="trackProviderLearnMoreClick(provider.id)" + > + {{ __('Learn more.') }} + </gl-link> </p> + <!-- Note: The following `div` and it's content will be replaced by 'GlFormRadio' once https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1720#note_857342988 is resolved --> + <div + class="gl-form-radio custom-control custom-radio" + data-testid="primary-provider-radio" + > + <input + :id="`security-training-provider-${provider.id}`" + type="radio" + :checked="provider.isPrimary" + class="custom-control-input" + :disabled="!provider.isEnabled" + @change="setPrimaryProvider(provider)" + /> + <label + class="custom-control-label" + :for="`security-training-provider-${provider.id}`" + > + {{ $options.i18n.primaryTraining }} + </label> + <gl-icon + v-gl-tooltip="$options.i18n.primaryTrainingDescription" + name="information-o" + class="gl-ml-2 gl-cursor-help" + /> + </div> </div> </div> </gl-card> diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js index dc76436e91d..14eb10ac2aa 100644 --- a/app/assets/javascripts/security_configuration/constants.js +++ b/app/assets/javascripts/security_configuration/constants.js @@ -1,2 +1,8 @@ export const TRACK_TOGGLE_TRAINING_PROVIDER_ACTION = 'toggle_security_training_provider'; export const TRACK_TOGGLE_TRAINING_PROVIDER_LABEL = 'update_security_training_provider'; +export const TRACK_CLICK_TRAINING_LINK_ACTION = 'click_security_training_link'; +export const TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION = 'click_link'; +export const TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL = 'security_training_provider'; +export const TRACK_TRAINING_LOADED_ACTION = 'security_training_link_loaded'; +export const TRACK_PROMOTION_BANNER_CTA_CLICK_ACTION = 'click_button'; +export const TRACK_PROMOTION_BANNER_CTA_CLICK_LABEL = 'security_training_promotion_cta'; diff --git a/app/assets/javascripts/security_configuration/graphql/cache_utils.js b/app/assets/javascripts/security_configuration/graphql/cache_utils.js new file mode 100644 index 00000000000..6d5258b01dc --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/cache_utils.js @@ -0,0 +1,40 @@ +import produce from 'immer'; + +export const updateSecurityTrainingOptimisticResponse = (changes) => ({ + // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 + // eslint-disable-next-line @gitlab/require-i18n-strings + __typename: 'Mutation', + securityTrainingUpdate: { + __typename: 'SecurityTrainingUpdatePayload', + training: { + __typename: 'ProjectSecurityTraining', + ...changes, + }, + errors: [], + }, +}); + +export const updateSecurityTrainingCache = ({ query, variables }) => (cache, { data }) => { + const { + securityTrainingUpdate: { training: updatedProvider }, + } = data; + const { project } = cache.readQuery({ query, variables }); + if (!updatedProvider.isPrimary) { + return; + } + + // when we set a new primary provider, we need to unset the previous one(s) + const updatedProject = produce(project, (draft) => { + draft.securityTrainingProviders.forEach((provider) => { + // eslint-disable-next-line no-param-reassign + provider.isPrimary = provider.id === updatedProvider.id; + }); + }); + + // write to the cache + cache.writeQuery({ + query, + variables, + data: { project: updatedProject }, + }); +}; diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql new file mode 100644 index 00000000000..f0474614dab --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql @@ -0,0 +1,10 @@ +query getSecurityTrainingUrls($projectFullPath: ID!, $identifierExternalIds: [String!]!) { + project(fullPath: $projectFullPath) { + id + securityTrainingUrls(identifierExternalIds: $identifierExternalIds) { + name + status + url + } + } +} |