summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/security_configuration
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 20:02:30 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 20:02:30 +0000
commit41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch)
tree9c8d89a8624828992f06d892cd2f43818ff5dcc8 /app/assets/javascripts/security_configuration
parent0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js34
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue5
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue139
-rw-r--r--app/assets/javascripts/security_configuration/constants.js6
-rw-r--r--app/assets/javascripts/security_configuration/graphql/cache_utils.js40
-rw-r--r--app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql10
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
+ }
+ }
+}