diff options
Diffstat (limited to 'app/assets/javascripts/incidents_settings')
6 files changed, 544 insertions, 0 deletions
diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue new file mode 100644 index 00000000000..a394f404ee1 --- /dev/null +++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue @@ -0,0 +1,139 @@ +<script> +import { + GlButton, + GlSprintf, + GlLink, + GlIcon, + GlFormGroup, + GlFormCheckbox, + GlNewDropdown, + GlNewDropdownItem, +} from '@gitlab/ui'; +import { + I18N_ALERT_SETTINGS_FORM, + NO_ISSUE_TEMPLATE_SELECTED, + TAKING_INCIDENT_ACTION_DOCS_LINK, + ISSUE_TEMPLATES_DOCS_LINK, +} from '../constants'; + +export default { + components: { + GlButton, + GlSprintf, + GlLink, + GlFormGroup, + GlIcon, + GlFormCheckbox, + GlNewDropdown, + GlNewDropdownItem, + }, + inject: ['service', 'alertSettings'], + data() { + return { + templates: [NO_ISSUE_TEMPLATE_SELECTED, ...this.alertSettings.templates], + createIssueEnabled: this.alertSettings.createIssue, + issueTemplate: this.alertSettings.issueTemplateKey, + sendEmailEnabled: this.alertSettings.sendEmail, + loading: false, + }; + }, + i18n: I18N_ALERT_SETTINGS_FORM, + TAKING_INCIDENT_ACTION_DOCS_LINK, + ISSUE_TEMPLATES_DOCS_LINK, + computed: { + issueTemplateHeader() { + return this.issueTemplate || NO_ISSUE_TEMPLATE_SELECTED.name; + }, + formData() { + return { + create_issue: this.createIssueEnabled, + issue_template_key: this.issueTemplate, + send_email: this.sendEmailEnabled, + }; + }, + }, + methods: { + selectIssueTemplate(templateKey) { + this.issueTemplate = templateKey; + }, + isTemplateSelected(templateKey) { + return templateKey === this.issueTemplate; + }, + updateAlertsIntegrationSettings() { + this.loading = true; + + this.service.updateSettings(this.formData).catch(() => { + this.loading = false; + }); + }, + }, +}; +</script> + +<template> + <div> + <p> + <gl-sprintf :message="$options.i18n.introText"> + <template #docsLink> + <gl-link :href="$options.TAKING_INCIDENT_ACTION_DOCS_LINK" target="_blank"> + <span>{{ $options.i18n.introLinkText }}</span> + </gl-link> + </template> + </gl-sprintf> + </p> + <form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings"> + <gl-form-group class="gl-pl-0"> + <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_issue_checkbox"> + <span>{{ $options.i18n.createIssue.label }}</span> + </gl-form-checkbox> + </gl-form-group> + + <gl-form-group + label-size="sm" + label-for="alert-integration-settings-issue-template" + class="col-8 col-md-9 gl-px-6" + > + <label class="gl-display-inline-flex" for="alert-integration-settings-issue-template"> + {{ $options.i18n.issueTemplate.label }} + <gl-link :href="$options.ISSUE_TEMPLATES_DOCS_LINK" target="_blank"> + <gl-icon name="question" :size="12" /> + </gl-link> + </label> + <gl-new-dropdown + id="alert-integration-settings-issue-template" + data-qa-selector="incident_templates_dropdown" + :text="issueTemplateHeader" + :block="true" + > + <gl-new-dropdown-item + v-for="template in templates" + :key="template.key" + data-qa-selector="incident_templates_item" + :is-check-item="true" + :is-checked="isTemplateSelected(template.key)" + @click="selectIssueTemplate(template.key)" + > + {{ template.name }} + </gl-new-dropdown-item> + </gl-new-dropdown> + </gl-form-group> + + <gl-form-group class="gl-pl-0 gl-mb-5"> + <gl-form-checkbox v-model="sendEmailEnabled"> + <span>{{ $options.i18n.sendEmail.label }}</span> + </gl-form-checkbox> + </gl-form-group> + + <gl-button + ref="submitBtn" + data-qa-selector="save_changes_button" + :disabled="loading" + variant="success" + type="submit" + class="js-no-auto-disable" + > + {{ $options.i18n.saveBtnLabel }} + </gl-button> + </form> + </div> +</template> diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue new file mode 100644 index 00000000000..0623c275c5a --- /dev/null +++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue @@ -0,0 +1,61 @@ +<script> +import { GlButton, GlTabs, GlTab } from '@gitlab/ui'; +import AlertsSettingsForm from './alerts_form.vue'; +import PagerDutySettingsForm from './pagerduty_form.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants'; + +export default { + components: { + GlButton, + GlTabs, + GlTab, + AlertsSettingsForm, + PagerDutySettingsForm, + }, + mixins: [glFeatureFlagMixin()], + tabs: INTEGRATION_TABS_CONFIG, + i18n: I18N_INTEGRATION_TABS, + methods: { + isFeatureFlagEnabled(tab) { + if (tab.featureFlag) { + return this.glFeatures[tab.featureFlag]; + } + return true; + }, + }, +}; +</script> + +<template> + <section + id="incident-management-settings" + data-qa-selector="incidents_settings_content" + class="settings no-animate qa-incident-management-settings" + > + <div class="settings-header"> + <h3 ref="sectionHeader" class="h4"> + {{ $options.i18n.headerText }} + </h3> + <gl-button ref="toggleBtn" class="js-settings-toggle">{{ + $options.i18n.expandBtnLabel + }}</gl-button> + <p ref="sectionSubHeader"> + {{ $options.i18n.subHeaderText }} + </p> + </div> + + <div class="settings-content"> + <gl-tabs> + <gl-tab + v-for="(tab, index) in $options.tabs" + v-if="tab.active && isFeatureFlagEnabled(tab)" + :key="`${tab.title}_${index}`" + :title="tab.title" + > + <component :is="tab.component" class="gl-pt-3" :data-testid="`${tab.component}-tab`" /> + </gl-tab> + </gl-tabs> + </div> + </section> +</template> diff --git a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue new file mode 100644 index 00000000000..027848db6e9 --- /dev/null +++ b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue @@ -0,0 +1,183 @@ +<script> +import { + GlAlert, + GlButton, + GlSprintf, + GlLink, + GlIcon, + GlFormGroup, + GlFormInputGroup, + GlToggle, + GlModal, + GlModalDirective, +} from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { I18N_PAGERDUTY_SETTINGS_FORM, CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK } from '../constants'; +import { isEqual } from 'lodash'; + +export default { + components: { + GlAlert, + GlButton, + GlSprintf, + GlLink, + GlIcon, + GlFormGroup, + GlFormInputGroup, + GlToggle, + GlModal, + ClipboardButton, + }, + directives: { + 'gl-modal': GlModalDirective, + }, + inject: ['service', 'pagerDutySettings'], + data() { + return { + active: this.pagerDutySettings.active, + webhookUrl: this.pagerDutySettings.webhookUrl, + loading: false, + resettingWebhook: false, + webhookUpdateFailed: false, + showAlert: false, + }; + }, + i18n: I18N_PAGERDUTY_SETTINGS_FORM, + CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK, + computed: { + formData() { + return { + pagerduty_active: this.active, + }; + }, + isFormUpdated() { + return isEqual(this.pagerDutySettings, { + active: this.active, + webhookUrl: this.webhookUrl, + }); + }, + isSaveDisabled() { + return this.isFormUpdated || this.loading || this.resettingWebhook; + }, + webhookUpdateAlertMsg() { + return this.webhookUpdateFailed + ? this.$options.i18n.webhookUrl.updateErrMsg + : this.$options.i18n.webhookUrl.updateSuccessMsg; + }, + webhookUpdateAlertVariant() { + return this.webhookUpdateFailed ? 'danger' : 'success'; + }, + }, + methods: { + updatePagerDutyIntegrationSettings() { + this.loading = true; + + this.service.updateSettings(this.formData).catch(() => { + this.loading = false; + }); + }, + resetWebhookUrl() { + this.resettingWebhook = true; + + this.service + .resetWebhookUrl() + .then(({ data: { pagerduty_webhook_url: url } }) => { + this.webhookUrl = url; + this.showAlert = true; + this.webhookUpdateFailed = false; + }) + .catch(() => { + this.showAlert = true; + this.webhookUpdateFailed = true; + }) + .finally(() => { + this.resettingWebhook = false; + }); + }, + }, +}; +</script> + +<template> + <div> + <gl-alert + v-if="showAlert" + class="gl-mb-3" + :variant="webhookUpdateAlertVariant" + @dismiss="showAlert = false" + > + {{ webhookUpdateAlertMsg }} + </gl-alert> + + <p>{{ $options.i18n.introText }}</p> + <form ref="settingsForm" @submit.prevent="updatePagerDutyIntegrationSettings"> + <gl-form-group class="col-8 col-md-9 gl-p-0"> + <gl-toggle + id="active" + v-model="active" + :is-loading="loading" + :label="$options.i18n.activeToggle.label" + /> + </gl-form-group> + + <gl-form-group + class="col-8 col-md-9 gl-p-0" + :label="$options.i18n.webhookUrl.label" + label-for="url" + label-class="label-bold" + > + <gl-form-input-group id="url" data-testid="webhook-url" readonly :value="webhookUrl"> + <template #append> + <clipboard-button + :text="webhookUrl" + :title="$options.i18n.webhookUrl.copyToClipboard" + /> + </template> + </gl-form-input-group> + + <div class="gl-text-gray-400 gl-pt-2"> + <gl-sprintf :message="$options.i18n.webhookUrl.helpText"> + <template #docsLink> + <gl-link + :href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK" + target="_blank" + class="gl-display-inline-flex" + > + <span>{{ $options.i18n.webhookUrl.helpDocsLink }}</span> + <gl-icon name="external-link" /> + </gl-link> + </template> + </gl-sprintf> + </div> + <gl-button + v-gl-modal.resetWebhookModal + class="gl-mt-3" + :disabled="loading" + :loading="resettingWebhook" + data-testid="webhook-reset-btn" + > + {{ $options.i18n.webhookUrl.resetWebhookUrl }} + </gl-button> + <gl-modal + modal-id="resetWebhookModal" + :title="$options.i18n.webhookUrl.resetWebhookUrl" + :ok-title="$options.i18n.webhookUrl.resetWebhookUrl" + ok-variant="danger" + @ok="resetWebhookUrl" + > + {{ $options.i18n.webhookUrl.restKeyInfo }} + </gl-modal> + </gl-form-group> + + <gl-button + ref="submitBtn" + :disabled="isSaveDisabled" + variant="success" + type="submit" + class="js-no-auto-disable" + > + {{ $options.i18n.saveBtnLabel }} + </gl-button> + </form> + </div> +</template> diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js new file mode 100644 index 00000000000..b443c237f0f --- /dev/null +++ b/app/assets/javascripts/incidents_settings/constants.js @@ -0,0 +1,83 @@ +import { __, s__ } from '~/locale'; + +/* Integration tabs constants */ +export const INTEGRATION_TABS_CONFIG = [ + { + title: s__('IncidentSettings|Alert integration'), + component: 'AlertsSettingsForm', + active: true, + }, + { + title: s__('IncidentSettings|PagerDuty integration'), + component: 'PagerDutySettingsForm', + active: true, + featureFlag: 'pagerdutyWebhook', + }, + { + title: s__('IncidentSettings|Grafana integration'), + component: '', + active: false, + }, +]; + +export const I18N_INTEGRATION_TABS = { + headerText: s__('IncidentSettings|Incidents'), + expandBtnLabel: __('Expand'), + subHeaderText: s__( + 'IncidentSettings|Set up integrations with external tools to help better manage incidents.', + ), +}; + +/* Alerts integration settings constants */ + +export const I18N_ALERT_SETTINGS_FORM = { + saveBtnLabel: __('Save changes'), + introText: __('Action to take when receiving an alert. %{docsLink}'), + introLinkText: __('More information.'), + createIssue: { + label: __('Create an issue. Issues are created for each alert triggered.'), + }, + issueTemplate: { + label: __('Issue template (optional)'), + }, + sendEmail: { + label: __('Send a separate email notification to Developers.'), + }, +}; + +export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selected') }; +export const TAKING_INCIDENT_ACTION_DOCS_LINK = + '/help/user/project/integrations/prometheus#taking-action-on-incidents-ultimate'; +export const ISSUE_TEMPLATES_DOCS_LINK = + '/help/user/project/description_templates#creating-issue-templates'; + +/* PagerDuty integration settings constants */ + +export const I18N_PAGERDUTY_SETTINGS_FORM = { + introText: s__( + 'PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.', + ), + activeToggle: { + label: s__('PagerDutySettings|Active'), + }, + webhookUrl: { + label: s__('PagerDutySettings|Webhook URL'), + helpText: s__( + 'PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}', + ), + helpDocsLink: s__('PagerDutySettings|configuring a webhook in PagerDuty'), + resetWebhookUrl: s__('PagerDutySettings|Reset webhook URL'), + copyToClipboard: __('Copy'), + updateErrMsg: s__('PagerDutySettings|Failed to update Webhook URL'), + updateSuccessMsg: s__('PagerDutySettings|Webhook URL update was successful'), + restKeyInfo: s__( + "PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.", + ), + }, + saveBtnLabel: __('Save changes'), +}; + +export const CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK = 'https://support.pagerduty.com/docs/webhooks'; + +/* common constants */ +export const ERROR_MSG = __('There was an error saving your changes.'); diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js new file mode 100644 index 00000000000..bd4f5bb8820 --- /dev/null +++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js @@ -0,0 +1,32 @@ +import axios from '~/lib/utils/axios_utils'; +import { refreshCurrentPage } from '~/lib/utils/url_utility'; +import createFlash from '~/flash'; +import { ERROR_MSG } from './constants'; + +export default class IncidentsSettingsService { + constructor(settingsEndpoint, webhookUpdateEndpoint) { + this.settingsEndpoint = settingsEndpoint; + this.webhookUpdateEndpoint = webhookUpdateEndpoint; + } + + updateSettings(data) { + return axios + .patch(this.settingsEndpoint, { + project: { + incident_management_setting_attributes: data, + }, + }) + .then(() => { + refreshCurrentPage(); + }) + .catch(({ response }) => { + const message = response?.data?.message || ''; + + createFlash(`${ERROR_MSG} ${message}`, 'alert'); + }); + } + + resetWebhookUrl() { + return axios.post(this.webhookUpdateEndpoint); + } +} diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js new file mode 100644 index 00000000000..80e7d07feca --- /dev/null +++ b/app/assets/javascripts/incidents_settings/index.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import SettingsTabs from './components/incidents_settings_tabs.vue'; +import IncidentsSettingsService from './incidents_settings_service'; + +export default () => { + const el = document.querySelector('.js-incidents-settings'); + + if (!el) { + return null; + } + + const { + dataset: { + operationsSettingsEndpoint, + templates, + createIssue, + issueTemplateKey, + sendEmail, + pagerdutyActive, + pagerdutyWebhookUrl, + pagerdutyResetKeyPath, + }, + } = el; + + const service = new IncidentsSettingsService(operationsSettingsEndpoint, pagerdutyResetKeyPath); + return new Vue({ + el, + provide: { + service, + alertSettings: { + templates: JSON.parse(templates), + createIssue: parseBoolean(createIssue), + issueTemplateKey, + sendEmail: parseBoolean(sendEmail), + }, + pagerDutySettings: { + active: parseBoolean(pagerdutyActive), + webhookUrl: pagerdutyWebhookUrl, + }, + }, + render(createElement) { + return createElement(SettingsTabs); + }, + }); +}; |