gitlab-org--gitlab-foss/app/assets/javascripts/security_configuration/components/training_provider_list.vue

284 lines
8.6 KiB
Vue

<script>
import {
GlAlert,
GlTooltipDirective,
GlCard,
GlFormRadio,
GlToggle,
GlLink,
GlSkeletonLoader,
GlIcon,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
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 '~/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: __(
'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.',
),
primaryTraining: s__('SecurityTraining|Primary Training'),
primaryTrainingDescription: s__(
'SecurityTraining|Training from this partner takes precedence when more than one training partner is enabled.',
),
};
export default {
components: {
GlAlert,
GlCard,
GlFormRadio,
GlToggle,
GlLink,
GlSkeletonLoader,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
mixins: [Tracking.mixin()],
inject: ['projectFullPath'],
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
variables() {
return {
fullPath: this.projectFullPath,
};
},
update({ project }) {
return project?.securityTrainingProviders;
},
error() {
this.errorMessage = this.$options.i18n.providerQueryErrorMessage;
},
},
},
data() {
return {
errorMessage: '',
securityTrainingProviders: [],
hasTouchedConfiguration: false,
};
},
computed: {
primaryProviderId() {
return this.securityTrainingProviders.find(({ isPrimary }) => isPrimary)?.id;
},
enabledProviders() {
return this.securityTrainingProviders.filter(({ isEnabled }) => isEnabled);
},
isLoading() {
return this.$apollo.queries.securityTrainingProviders.loading;
},
},
created() {
const unwatchConfigChance = this.$watch('hasTouchedConfiguration', () => {
this.dismissFeaturePromotionCallout();
unwatchConfigChance();
});
},
methods: {
async dismissFeaturePromotionCallout() {
try {
const {
data: {
userCalloutCreate: { errors },
},
} = await this.$apollo.mutate({
mutation: dismissUserCalloutMutation,
variables: {
input: {
featureName: 'security_training_feature_promotion',
},
},
});
// handle errors reported from the backend
if (errors?.length > 0) {
throw new Error(errors[0]);
}
} catch (e) {
Sentry.captureException(e);
}
},
async toggleProvider(provider) {
const { isEnabled, isPrimary } = provider;
const toggledIsEnabled = !isEnabled;
this.trackProviderToggle(provider.id, 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,
});
},
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 {
data: {
securityTrainingUpdate: { errors = [] },
},
} = await this.$apollo.mutate({
mutation: configureSecurityTrainingProvidersMutation,
variables: {
input: {
projectPath: this.projectFullPath,
providerId: id,
isEnabled,
isPrimary: nextIsPrimary,
},
},
optimisticResponse: updateSecurityTrainingOptimisticResponse({
id,
isEnabled,
isPrimary: nextIsPrimary,
}),
update: updateSecurityTrainingCache({
query: securityTrainingProvidersQuery,
variables: { fullPath: this.projectFullPath },
}),
});
if (errors.length > 0) {
// throwing an error here means we can handle scenarios within the `catch` block below
throw new Error();
}
this.hasTouchedConfiguration = true;
} catch {
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
}
},
trackProviderToggle(providerId, providerIsEnabled) {
this.track(TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, {
label: TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
property: providerId,
extra: {
providerIsEnabled,
},
});
},
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>
<template>
<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-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="provider in securityTrainingProviders" :key="provider.id" class="gl-mb-6">
<gl-card>
<div class="gl-display-flex">
<gl-toggle
:value="provider.isEnabled"
:label="__('Training mode')"
label-position="hidden"
@change="toggleProvider(provider)"
/>
<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
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>
<gl-form-radio
:checked="primaryProviderId"
:disabled="!provider.isEnabled"
:value="provider.id"
@change="setPrimaryProvider(provider)"
>
{{ $options.i18n.primaryTraining }}
<gl-icon
v-gl-tooltip="$options.i18n.primaryTrainingDescription"
name="information-o"
class="gl-ml-2 gl-cursor-help"
/>
</gl-form-radio>
</div>
</div>
</gl-card>
</li>
</ul>
</div>
</template>