diff options
Diffstat (limited to 'app/assets/javascripts/alerts_settings/components')
-rw-r--r-- | app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue new file mode 100644 index 00000000000..18c9f82f052 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -0,0 +1,563 @@ +<script> +import { + GlAlert, + GlButton, + GlForm, + GlFormGroup, + GlFormInput, + GlFormInputGroup, + GlFormTextarea, + GlLink, + GlModal, + GlModalDirective, + GlSprintf, + GlFormSelect, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ToggleButton from '~/vue_shared/components/toggle_button.vue'; +import csrf from '~/lib/utils/csrf'; +import service from '../services'; +import { + i18n, + serviceOptions, + JSON_VALIDATE_DELAY, + targetPrometheusUrlPlaceholder, + targetOpsgenieUrlPlaceholder, +} from '../constants'; + +export default { + i18n, + csrf, + targetOpsgenieUrlPlaceholder, + targetPrometheusUrlPlaceholder, + components: { + GlAlert, + GlButton, + GlForm, + GlFormGroup, + GlFormInput, + GlFormInputGroup, + GlFormSelect, + GlFormTextarea, + GlLink, + GlModal, + GlSprintf, + ClipboardButton, + ToggleButton, + }, + directives: { + 'gl-modal': GlModalDirective, + }, + mixins: [glFeatureFlagsMixin()], + props: { + prometheus: { + type: Object, + required: true, + validator: ({ activated }) => { + return activated !== undefined; + }, + }, + generic: { + type: Object, + required: true, + validator: ({ formPath }) => { + return formPath !== undefined; + }, + }, + opsgenie: { + type: Object, + required: true, + }, + }, + data() { + return { + activated: { + generic: this.generic.activated, + prometheus: this.prometheus.activated, + opsgenie: this.opsgenie?.activated, + }, + loading: false, + authorizationKey: { + generic: this.generic.initialAuthorizationKey, + prometheus: this.prometheus.prometheusAuthorizationKey, + }, + selectedEndpoint: serviceOptions[0].value, + options: serviceOptions, + targetUrl: null, + feedback: { + variant: 'danger', + feedbackMessage: null, + isFeedbackDismissed: false, + }, + serverError: null, + testAlert: { + json: null, + error: null, + }, + canSaveForm: false, + }; + }, + computed: { + sections() { + return [ + { + text: this.$options.i18n.usageSection, + url: this.generic.alertsUsageUrl, + }, + { + text: this.$options.i18n.setupSection, + url: this.generic.alertsSetupUrl, + }, + ]; + }, + isPrometheus() { + return this.selectedEndpoint === 'prometheus'; + }, + isOpsgenie() { + return this.selectedEndpoint === 'opsgenie'; + }, + selectedService() { + switch (this.selectedEndpoint) { + case 'generic': { + return { + url: this.generic.url, + authKey: this.authorizationKey.generic, + active: this.activated.generic, + resetKey: this.resetGenericKey.bind(this), + }; + } + case 'prometheus': { + return { + url: this.prometheus.prometheusUrl, + authKey: this.authorizationKey.prometheus, + active: this.activated.prometheus, + resetKey: this.resetPrometheusKey.bind(this), + targetUrl: this.prometheus.prometheusApiUrl, + }; + } + case 'opsgenie': { + return { + targetUrl: this.opsgenie.opsgenieMvcTargetUrl, + active: this.activated.opsgenie, + }; + } + default: { + return {}; + } + } + }, + showFeedbackMsg() { + return this.feedback.feedbackMessage && !this.isFeedbackDismissed; + }, + showAlertSave() { + return ( + this.feedback.feedbackMessage === this.$options.i18n.testAlertFailed && + !this.isFeedbackDismissed + ); + }, + prometheusInfo() { + return this.isPrometheus ? this.$options.i18n.prometheusInfo : ''; + }, + jsonIsValid() { + return this.testAlert.error === null; + }, + canTestAlert() { + return this.selectedService.active && this.testAlert.json !== null; + }, + canSaveConfig() { + return !this.loading && this.canSaveForm; + }, + baseUrlPlaceholder() { + return this.isOpsgenie + ? this.$options.targetOpsgenieUrlPlaceholder + : this.$options.targetPrometheusUrlPlaceholder; + }, + }, + watch: { + 'testAlert.json': debounce(function debouncedJsonValidate() { + this.validateJson(); + }, JSON_VALIDATE_DELAY), + targetUrl(oldVal, newVal) { + if (newVal && oldVal !== this.selectedService.targetUrl) { + this.canSaveForm = true; + } + }, + }, + mounted() { + if ( + this.activated.prometheus || + this.activated.generic || + !this.opsgenie.opsgenieMvcIsAvailable + ) { + this.removeOpsGenieOption(); + } else if (this.activated.opsgenie) { + this.setOpsgenieAsDefault(); + } + }, + methods: { + createUserErrorMessage(errors) { + // eslint-disable-next-line prefer-destructuring + this.serverError = Object.values(errors)[0][0]; + }, + setOpsgenieAsDefault() { + this.options = this.options.map(el => { + if (el.value !== 'opsgenie') { + return { ...el, disabled: true }; + } + return { ...el, disabled: false }; + }); + this.selectedEndpoint = this.options.find(({ value }) => value === 'opsgenie').value; + if (this.targetUrl === null) { + this.targetUrl = this.selectedService.targetUrl; + } + }, + removeOpsGenieOption() { + this.options = this.options.map(el => { + if (el.value !== 'opsgenie') { + return { ...el, disabled: false }; + } + return { ...el, disabled: true }; + }); + }, + resetFormValues() { + this.testAlert.json = null; + this.targetUrl = this.selectedService.targetUrl; + }, + dismissFeedback() { + this.serverError = null; + this.feedback = { ...this.feedback, feedbackMessage: null }; + this.isFeedbackDismissed = false; + }, + resetGenericKey() { + return service + .updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } }) + .then(({ data: { token } }) => { + this.authorizationKey.generic = token; + this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' }); + }) + .catch(() => { + this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' }); + }); + }, + resetPrometheusKey() { + return service + .updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath }) + .then(({ data: { token } }) => { + this.authorizationKey.prometheus = token; + this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' }); + }) + .catch(() => { + this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' }); + }); + }, + toggleService(value) { + this.canSaveForm = true; + if (this.isPrometheus) { + this.activated.prometheus = value; + } else { + this.activated[this.selectedEndpoint] = value; + } + }, + toggle(value) { + return this.isPrometheus ? this.togglePrometheusActive(value) : this.toggleActivated(value); + }, + toggleActivated(value) { + this.loading = true; + return service + .updateGenericActive({ + endpoint: this[this.selectedEndpoint].formPath, + params: this.isOpsgenie + ? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } } + : { service: { active: value } }, + }) + .then(() => { + this.activated[this.selectedEndpoint] = value; + this.toggleSuccess(value); + + if (!this.isOpsgenie && value) { + if (!this.selectedService.authKey) { + return window.location.reload(); + } + + return this.removeOpsGenieOption(); + } + + if (this.isOpsgenie && value) { + return this.setOpsgenieAsDefault(); + } + + // eslint-disable-next-line no-return-assign + return (this.options = serviceOptions); + }) + .catch(({ response: { data: { errors } = {} } = {} }) => { + this.createUserErrorMessage(errors); + this.setFeedback({ + feedbackMessage: `${this.$options.i18n.errorMsg}.`, + variant: 'danger', + }); + }) + .finally(() => { + this.loading = false; + this.canSaveForm = false; + }); + }, + togglePrometheusActive(value) { + this.loading = true; + return service + .updatePrometheusActive({ + endpoint: this.prometheus.prometheusFormPath, + params: { + token: this.$options.csrf.token, + config: value, + url: this.targetUrl, + redirect: window.location, + }, + }) + .then(() => { + this.activated.prometheus = value; + this.toggleSuccess(value); + this.removeOpsGenieOption(); + }) + .catch(({ response: { data: { errors } = {} } = {} }) => { + this.createUserErrorMessage(errors); + this.setFeedback({ + feedbackMessage: `${this.$options.i18n.errorMsg}.`, + variant: 'danger', + }); + }) + .finally(() => { + this.loading = false; + this.canSaveForm = false; + }); + }, + toggleSuccess(value) { + if (value) { + this.setFeedback({ + feedbackMessage: this.$options.i18n.endPointActivated, + variant: 'info', + }); + } else { + this.setFeedback({ + feedbackMessage: this.$options.i18n.changesSaved, + variant: 'info', + }); + } + }, + setFeedback({ feedbackMessage, variant }) { + this.feedback = { feedbackMessage, variant }; + }, + validateJson() { + this.testAlert.error = null; + try { + JSON.parse(this.testAlert.json); + } catch (e) { + this.testAlert.error = JSON.stringify(e.message); + } + }, + validateTestAlert() { + this.loading = true; + this.validateJson(); + return service + .updateTestAlert({ + endpoint: this.selectedService.url, + data: this.testAlert.json, + authKey: this.selectedService.authKey, + }) + .then(() => { + this.setFeedback({ + feedbackMessage: this.$options.i18n.testAlertSuccess, + variant: 'success', + }); + }) + .catch(() => { + this.setFeedback({ + feedbackMessage: this.$options.i18n.testAlertFailed, + variant: 'danger', + }); + }) + .finally(() => { + this.loading = false; + }); + }, + onSubmit() { + this.toggle(this.selectedService.active); + }, + onReset() { + this.testAlert.json = null; + this.dismissFeedback(); + this.targetUrl = this.selectedService.targetUrl; + + if (this.canSaveForm) { + this.canSaveForm = false; + this.activated[this.selectedEndpoint] = this[this.selectedEndpoint].activated; + } + }, + }, +}; +</script> + +<template> + <div> + <gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback"> + {{ feedback.feedbackMessage }} + <br /> + <i v-if="serverError">{{ __('Error message:') }} {{ serverError }}</i> + <gl-button + v-if="showAlertSave" + variant="danger" + category="primary" + class="gl-display-block gl-mt-3" + @click="toggle(selectedService.active)" + > + {{ __('Save anyway') }} + </gl-button> + </gl-alert> + <div data-testid="alert-settings-description" class="gl-mt-5"> + <p v-for="section in sections" :key="section.text"> + <gl-sprintf :message="section.text"> + <template #link="{ content }"> + <gl-link :href="section.url" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + </div> + <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset"> + <gl-form-group + :label="$options.i18n.integrationsLabel" + label-for="integrations" + label-class="label-bold" + > + <gl-form-select + v-model="selectedEndpoint" + :options="options" + data-testid="alert-settings-select" + @change="resetFormValues" + /> + <span class="gl-text-gray-400"> + <gl-sprintf :message="$options.i18n.integrationsInfo"> + <template #link="{ content }"> + <gl-link + class="gl-display-inline-block" + href="https://gitlab.com/groups/gitlab-org/-/epics/3362" + target="_blank" + >{{ content }}</gl-link + > + </template> + </gl-sprintf> + </span> + </gl-form-group> + <gl-form-group + :label="$options.i18n.activeLabel" + label-for="activated" + label-class="label-bold" + > + <toggle-button + id="activated" + :disabled-input="loading" + :is-loading="loading" + :value="selectedService.active" + @change="toggleService" + /> + </gl-form-group> + <gl-form-group + v-if="isOpsgenie || isPrometheus" + :label="$options.i18n.apiBaseUrlLabel" + label-for="api-url" + label-class="label-bold" + > + <gl-form-input + id="api-url" + v-model="targetUrl" + type="url" + :placeholder="baseUrlPlaceholder" + :disabled="!selectedService.active" + /> + <span class="gl-text-gray-400"> + {{ $options.i18n.apiBaseUrlHelpText }} + </span> + </gl-form-group> + <template v-if="!isOpsgenie"> + <gl-form-group :label="$options.i18n.urlLabel" label-for="url" label-class="label-bold"> + <gl-form-input-group id="url" readonly :value="selectedService.url"> + <template #append> + <clipboard-button + :text="selectedService.url" + :title="$options.i18n.copyToClipboard" + class="gl-m-0!" + /> + </template> + </gl-form-input-group> + <span class="gl-text-gray-400"> + {{ prometheusInfo }} + </span> + </gl-form-group> + <gl-form-group + :label="$options.i18n.authKeyLabel" + label-for="authorization-key" + label-class="label-bold" + > + <gl-form-input-group + id="authorization-key" + class="gl-mb-2" + readonly + :value="selectedService.authKey" + > + <template #append> + <clipboard-button + :text="selectedService.authKey || ''" + :title="$options.i18n.copyToClipboard" + class="gl-m-0!" + /> + </template> + </gl-form-input-group> + <gl-button v-gl-modal.authKeyModal :disabled="!selectedService.active" class="gl-mt-3">{{ + $options.i18n.resetKey + }}</gl-button> + <gl-modal + modal-id="authKeyModal" + :title="$options.i18n.resetKey" + :ok-title="$options.i18n.resetKey" + ok-variant="danger" + @ok="selectedService.resetKey" + > + {{ $options.i18n.restKeyInfo }} + </gl-modal> + </gl-form-group> + <gl-form-group + :label="$options.i18n.alertJson" + label-for="alert-json" + label-class="label-bold" + :invalid-feedback="testAlert.error" + > + <gl-form-textarea + id="alert-json" + v-model.trim="testAlert.json" + :disabled="!selectedService.active" + :state="jsonIsValid" + :placeholder="$options.i18n.alertJsonPlaceholder" + rows="6" + max-rows="10" + /> + </gl-form-group> + <gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{ + $options.i18n.testAlertInfo + }}</gl-button> + </template> + <div class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"> + <gl-button + variant="success" + category="primary" + :disabled="!canSaveConfig" + @click="onSubmit" + > + {{ __('Save changes') }} + </gl-button> + <gl-button variant="default" category="primary" :disabled="!canSaveConfig" @click="onReset"> + {{ __('Cancel') }} + </gl-button> + </div> + </gl-form> + </div> +</template> |