summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/incidents_settings
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/incidents_settings')
-rw-r--r--app/assets/javascripts/incidents_settings/components/alerts_form.vue139
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue61
-rw-r--r--app/assets/javascripts/incidents_settings/components/pagerduty_form.vue183
-rw-r--r--app/assets/javascripts/incidents_settings/constants.js83
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js32
-rw-r--r--app/assets/javascripts/incidents_settings/index.js46
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);
+ },
+ });
+};