Commit e4e5f091 authored by David O'Regan's avatar David O'Regan Committed by Gabriel Mazetto

Implement Opsgenie MVC in AlertsService

This is just an MVC and we already know that it's unlikely that
future iterations will be using any of store data.

Therefor, we decided to store the data needed as service properties
and not in a separate database table to avoid churn.
parent 842f1257
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
emptyState: {
opsgenie: {
title: s__('AlertManagement|Opsgenie is enabled'),
info: s__(
'AlertManagement|You have enabled the Opsgenie integration. Your alerts will be visible directly in Opsgenie.',
),
buttonText: s__('AlertManagement|View alerts in Opsgenie'),
},
gitlab: {
title: s__('AlertManagement|Surface alerts in GitLab'),
info: s__(
'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.',
),
buttonText: s__('AlertManagement|Authorize external service'),
},
},
moreInformation: s__('AlertManagement|More information'),
},
components: {
GlEmptyState,
GlButton,
......@@ -19,29 +39,49 @@ export default {
type: String,
required: true,
},
opsgenieMvcEnabled: {
type: Boolean,
required: false,
default: false,
},
opsgenieMvcTargetUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
emptyState() {
return {
...(this.opsgenieMvcEnabled
? this.$options.i18n.emptyState.opsgenie
: this.$options.i18n.emptyState.gitlab),
link: this.opsgenieMvcEnabled ? this.opsgenieMvcTargetUrl : this.enableAlertManagementPath,
};
},
alertsCanBeEnabled() {
return this.userCanEnableAlertManagement || this.opsgenieMvcEnabled;
},
},
};
</script>
<template>
<div>
<gl-empty-state
:title="s__('AlertManagement|Surface alerts in GitLab')"
:svg-path="emptyAlertSvgPath"
>
<gl-empty-state :title="emptyState.title" :svg-path="emptyAlertSvgPath">
<template #description>
<div class="d-block">
<span>{{
s__(
'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.',
)
}}</span>
<a href="/help/user/project/operations/alert_management.html" target="_blank">
{{ s__('AlertManagement|More information') }}
<div class="gl-display-block">
<span>{{ emptyState.info }}</span>
<a
v-if="!opsgenieMvcEnabled"
href="/help/user/project/operations/alert_management.html"
target="_blank"
>
{{ $options.i18n.moreInformation }}
</a>
</div>
<div v-if="userCanEnableAlertManagement" class="d-block center pt-4">
<gl-button category="primary" variant="success" :href="enableAlertManagementPath">
{{ s__('AlertManagement|Authorize external service') }}
<div v-if="alertsCanBeEnabled" class="gl-display-block center gl-pt-4">
<gl-button category="primary" variant="success" :href="emptyState.link">
{{ emptyState.buttonText }}
</gl-button>
</div>
</template>
......
......@@ -34,6 +34,16 @@ export default {
type: String,
required: true,
},
opsgenieMvcEnabled: {
type: Boolean,
required: false,
default: false,
},
opsgenieMvcTargetUrl: {
type: String,
required: false,
default: '',
},
},
mounted() {
this.trackPageViews();
......@@ -58,6 +68,8 @@ export default {
:empty-alert-svg-path="emptyAlertSvgPath"
:enable-alert-management-path="enableAlertManagementPath"
:user-can-enable-alert-management="userCanEnableAlertManagement"
:opsgenie-mvc-enabled="opsgenieMvcEnabled"
:opsgenie-mvc-target-url="opsgenieMvcTargetUrl"
/>
</div>
</template>
......@@ -16,11 +16,13 @@ export default () => {
enableAlertManagementPath,
emptyAlertSvgPath,
populatingAlertsHelpUrl,
opsgenieMvcTargetUrl,
} = domEl.dataset;
let { alertManagementEnabled, userCanEnableAlertManagement } = domEl.dataset;
let { alertManagementEnabled, userCanEnableAlertManagement, opsgenieMvcEnabled } = domEl.dataset;
alertManagementEnabled = parseBoolean(alertManagementEnabled);
userCanEnableAlertManagement = parseBoolean(userCanEnableAlertManagement);
opsgenieMvcEnabled = parseBoolean(opsgenieMvcEnabled);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
......@@ -54,6 +56,8 @@ export default () => {
emptyAlertSvgPath,
alertManagementEnabled,
userCanEnableAlertManagement,
opsgenieMvcTargetUrl,
opsgenieMvcEnabled,
},
});
},
......
......@@ -14,15 +14,24 @@ import {
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 } from '../constants';
import {
i18n,
serviceOptions,
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
targetOpsgenieUrlPlaceholder,
} from '../constants';
export default {
i18n,
csrf,
targetOpsgenieUrlPlaceholder,
targetPrometheusUrlPlaceholder,
components: {
GlAlert,
GlButton,
......@@ -41,12 +50,13 @@ export default {
directives: {
'gl-modal': GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
prometheus: {
type: Object,
required: true,
validator: ({ prometheusIsActivated }) => {
return prometheusIsActivated !== undefined;
validator: ({ activated }) => {
return activated !== undefined;
},
},
generic: {
......@@ -56,21 +66,26 @@ export default {
return formPath !== undefined;
},
},
opsgenie: {
type: Object,
required: true,
},
},
data() {
return {
activated: {
generic: this.generic.initialActivated,
prometheus: this.prometheus.prometheusIsActivated,
generic: this.generic.activated,
prometheus: this.prometheus.activated,
opsgenie: this.opsgenie?.activated,
},
loading: false,
authorizationKey: {
generic: this.generic.initialAuthorizationKey,
prometheus: this.prometheus.prometheusAuthorizationKey,
},
selectedEndpoint: null,
selectedEndpoint: serviceOptions[0].value,
options: serviceOptions,
prometheusApiKey: this.prometheus.prometheusApiUrl,
targetUrl: null,
feedback: {
variant: 'danger',
feedbackMessage: null,
......@@ -96,23 +111,41 @@ export default {
},
];
},
isGeneric() {
return this.selectedEndpoint === 'generic';
isPrometheus() {
return this.selectedEndpoint === 'prometheus';
},
isOpsgenie() {
return this.selectedEndpoint === 'opsgenie';
},
selectedService() {
return this.isGeneric
? {
switch (this.selectedEndpoint) {
case 'generic': {
return {
url: this.generic.url,
authKey: this.authorizationKey.generic,
active: this.activated.generic,
resetKey: this.resetGenericKey.bind(this),
}
: {
authKey: this.authorizationKey.prometheus,
};
}
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;
......@@ -124,10 +157,7 @@ export default {
);
},
prometheusInfo() {
return !this.isGeneric ? this.$options.i18n.prometheusInfo : '';
},
prometheusFeatureEnabled() {
return !this.isGeneric;
return this.isPrometheus ? this.$options.i18n.prometheusInfo : '';
},
jsonIsValid() {
return this.testAlert.error === null;
......@@ -138,20 +168,58 @@ export default {
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;
}
},
},
created() {
this.selectedEndpoint = this.prometheus.prometheusIsActivated
? this.options[1].value
: this.options[0].value;
mounted() {
if (
this.activated.prometheus ||
this.activated.generic ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
this.removeOpsGenieOption();
} else if (this.activated.opsgenie) {
this.setOpsgenieAsDefault();
}
},
methods: {
clearJson() {
setOpsgenieAsDefault() {
this.options = this.options.map(el => {
if (el.value !== 'opsgenie') {
return { ...el, disabled: true };
}
return { ...el, disabled: false };
});
const [selected] = this.options;
this.selectedEndpoint = selected.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.feedback = { ...this.feedback, feedbackMessage: null };
......@@ -181,28 +249,42 @@ export default {
},
toggleService(value) {
this.canSaveForm = true;
if (this.isGeneric) {
this.activated.generic = value;
} else {
if (this.isPrometheus) {
this.activated.prometheus = value;
} else {
this.activated[this.selectedEndpoint] = value;
}
},
toggleActivated(value) {
return this.isGeneric
? this.toggleGenericActivated(value)
: this.togglePrometheusActive(value);
toggle(value) {
return this.isPrometheus ? this.togglePrometheusActive(value) : this.toggleActivated(value);
},
toggleGenericActivated(value) {
toggleActivated(value) {
this.loading = true;
return service
.updateGenericActive({
endpoint: this.generic.formPath,
params: { service: { active: value } },
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.generic = value;
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(() => {
this.setFeedback({
......@@ -212,6 +294,7 @@ export default {
})
.finally(() => {
this.loading = false;
this.canSaveForm = false;
});
},
togglePrometheusActive(value) {
......@@ -221,14 +304,15 @@ export default {
endpoint: this.prometheus.prometheusFormPath,
params: {
token: this.$options.csrf.token,
config: value ? 1 : 0,
url: this.prometheusApiKey,
config: value,
url: this.targetUrl,
redirect: window.location,
},
})
.then(() => {
this.activated.prometheus = value;
this.toggleSuccess(value);
this.removeOpsGenieOption();
})
.catch(() => {
this.setFeedback({
......@@ -238,6 +322,7 @@ export default {
})
.finally(() => {
this.loading = false;
this.canSaveForm = false;
});
},
toggleSuccess(value) {
......@@ -290,11 +375,17 @@ export default {
});
},
onSubmit() {
this.toggleActivated(this.selectedService.active);
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;
}
},
},
};
......@@ -309,7 +400,7 @@ export default {
variant="danger"
category="primary"
class="gl-display-block gl-mt-3"
@click="toggleActivated(selectedService.active)"
@click="toggle(selectedService.active)"
>
{{ __('Save anyway') }}
</gl-button>
......@@ -333,7 +424,7 @@ export default {
v-model="selectedEndpoint"
:options="options"
data-testid="alert-settings-select"
@change="clearJson"
@change="resetFormValues"
/>
<span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.integrationsInfo">
......@@ -362,90 +453,99 @@ export default {
/>
</gl-form-group>
<gl-form-group
v-if="prometheusFeatureEnabled"
v-if="isOpsgenie || isPrometheus"
:label="$options.i18n.apiBaseUrlLabel"
label-for="api-url"
label-class="label-bold"
>
<gl-form-input
id="api-url"
v-model="prometheusApiKey"
v-model="targetUrl"
type="url"
:value="prometheusApiKey"
:placeholder="$options.i18n.prometheusApiPlaceholder"
:placeholder="baseUrlPlaceholder"
:disabled="!selectedService.active"
/>
<span class="gl-text-gray-400">
{{ $options.i18n.apiBaseUrlHelpText }}
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.urlLabel" label-for="url" label-class="label-bold">
<gl-form-input-group id="url" :readonly="true" :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="true"
:value="selectedService.authKey"
<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"
>
<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 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"
<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"
>
{{ $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>
<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 type="submit" variant="success" category="primary" :disabled="!canSaveConfig">
<gl-button
variant="success"
category="primary"
:disabled="!canSaveConfig"
@click="onSubmit"
>
{{ __('Save changes') }}
</gl-button>
<gl-button type="reset" variant="default" category="primary">
<gl-button variant="default" category="primary" :disabled="!canSaveConfig" @click="onReset">
{{ __('Cancel') }}
</gl-button>
</div>
......
......@@ -16,7 +16,6 @@ export const i18n = {
errorApiUrlMsg: s__(
'AlertSettings|There was an error while trying to enable the alert settings. Please ensure you are using a valid URL.',
),
prometheusApiPlaceholder: s__('AlertSettings|http://prometheus.example.com/'),
restKeyInfo: s__(
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
......@@ -29,7 +28,7 @@ export const i18n = {
resetKey: s__('AlertSettings|Reset key'),
copyToClipboard: s__('AlertSettings|Copy'),
integrationsLabel: s__('AlertSettings|Integrations'),
apiBaseUrlLabel: s__('AlertSettings|Prometheus API Base URL'),
apiBaseUrlLabel: s__('AlertSettings|API URL'),
authKeyLabel: s__('AlertSettings|Authorization key'),
urlLabel: s__('AlertSettings|Webhook URL'),
activeLabel: s__('AlertSettings|Active'),
......@@ -47,6 +46,10 @@ export const i18n = {
export const serviceOptions = [
{ value: 'generic', text: s__('AlertSettings|Generic') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
{ value: 'opsgenie', text: s__('AlertSettings|Opsgenie') },
];
export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';
export const targetOpsgenieUrlPlaceholder = 'https://app.opsgenie.com/alert/list/';
......@@ -20,33 +20,47 @@ export default el => {
formPath,
authorizationKey,
url,
opsgenieMvcAvailable,
opsgenieMvcFormPath,
opsgenieMvcEnabled,
opsgenieMvcTargetUrl,
} = el.dataset;
const activated = parseBoolean(activatedStr);
const genericActivated = parseBoolean(activatedStr);
const prometheusIsActivated = parseBoolean(prometheusActivated);
const opsgenieMvcActivated = parseBoolean(opsgenieMvcEnabled);
const opsgenieMvcIsAvailable = parseBoolean(opsgenieMvcAvailable);
const props = {
prometheus: {
activated: prometheusIsActivated,
prometheusUrl,
prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
},
generic: {
alertsSetupUrl,
alertsUsageUrl,
activated: genericActivated,
formPath,
initialAuthorizationKey: authorizationKey,
url,
},
opsgenie: {
formPath: opsgenieMvcFormPath,
activated: opsgenieMvcActivated,
opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable,
},
};
return new Vue({
el,
render(createElement) {
return createElement(AlertSettingsForm, {
props: {
prometheus: {
prometheusIsActivated,
prometheusUrl,
prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
},
generic: {
alertsSetupUrl,
alertsUsageUrl,
initialActivated: activated,
formPath,
initialAuthorizationKey: authorizationKey,
url,
},
},
props,
});
},
});
......
......@@ -27,3 +27,5 @@ module Projects::AlertManagementHelper
!!(project.alerts_service_activated? || project.prometheus_service_active?)
end
end
Projects::AlertManagementHelper.prepend_if_ee('EE::Projects::AlertManagementHelper')
......@@ -78,3 +78,5 @@ class AlertsService < Service
Gitlab::Routing.url_helpers
end
end
AlertsService.prepend_if_ee('EE::AlertsService')
......@@ -10,6 +10,8 @@ module EE
:multiproject_enabled,
:pass_unstable,
:project_name,
:opsgenie_mvc_enabled,
:opsgenie_mvc_target_url,
:repository_url,
:static_context
].freeze
......
......@@ -2,6 +2,8 @@
module EE
module OperationsHelper
extend ::Gitlab::Utils::Override
def operations_data
{
'add-path' => add_operations_project_path,
......@@ -32,5 +34,23 @@ module EE
'aws-secret-key' => status_page_setting.masked_aws_secret_key
}
end
override :alerts_settings_data
def alerts_settings_data(disabled: false)
super.merge(opsgenie_mvc_data)
end
private
def opsgenie_mvc_data
return {} unless alerts_service.opsgenie_mvc_available?
{
'opsgenie_mvc_available' => 'true',
'opsgenie_mvc_form_path' => scoped_integration_path(alerts_service),
'opsgenie_mvc_enabled' => alerts_service.opsgenie_mvc_enabled?.to_s,
'opsgenie_mvc_target_url' => alerts_service.opsgenie_mvc_target_url.to_s
}
end
end
end
# frozen_string_literal: true
module EE
module Projects
module AlertManagementHelper
extend ::Gitlab::Utils::Override
override :alert_management_data
def alert_management_data(current_user, project)
super.merge(
alert_management_opsgenie_mvc_data(project.alerts_service)
)
end
private
def alert_management_opsgenie_mvc_data(alerts_service)
return {} unless alerts_service&.opsgenie_mvc_available?
{
'opsgenie_mvc_available' => 'true',
'opsgenie_mvc_enabled' => alerts_service.opsgenie_mvc_enabled?.to_s,
'opsgenie_mvc_target_url' => alerts_service.opsgenie_mvc_target_url.to_s
}
end
end
end
end
# frozen_string_literal: true
module EE
module AlertsService
extend ActiveSupport::Concern
prepended do
boolean_accessor :opsgenie_mvc_enabled
prop_accessor :opsgenie_mvc_target_url
validates :opsgenie_mvc_target_url, presence: true, public_url: true,
if: :opsgenie_mvc_enabled?
end
def opsgenie_mvc_available?
return false if instance? || template?
project.feature_available?(:opsgenie_integration)
end
end
end
......@@ -92,6 +92,7 @@ class License < ApplicationRecord
multiple_group_issue_boards
object_storage
operations_dashboard
opsgenie_integration
packages
pages_size_limit
productivity_analytics
......
---
title: Add MVC for Opsgenie integration
merge_request: 36199
author:
type: added
......@@ -2,16 +2,20 @@
require 'spec_helper'
RSpec.describe OperationsHelper do
RSpec.describe OperationsHelper, :routing do
let_it_be(:project) { create(:project, :private) }
before do
helper.instance_variable_set(:@project, project)
end
describe '#status_page_settings_data' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:status_page_setting) { project.build_status_page_setting }
subject { helper.status_page_settings_data }
before do
helper.instance_variable_set(:@project, project)
allow(helper).to receive(:status_page_setting) { status_page_setting }
allow(helper).to receive(:current_user) { user }
allow(helper)
......@@ -52,7 +56,7 @@ RSpec.describe OperationsHelper do
end
context 'setting exists' do
let(:status_page_setting) { create(:status_page_setting) }
let(:status_page_setting) { create(:status_page_setting, project: project) }
it 'returns the correct values' do
expect(subject).to eq(
......@@ -67,4 +71,49 @@ RSpec.describe OperationsHelper do
end
end
end
describe '#alerts_settings_data' do
subject { helper.alerts_settings_data }
describe 'Opsgenie MVC attributes' do
let_it_be(:alerts_service) do
create(:alerts_service,
project: project,
opsgenie_mvc_enabled: false,
opsgenie_mvc_target_url: 'https://appname.app.opsgenie.com/alert/list'
)
end
let_it_be(:prometheus_service) { build_stubbed(:prometheus_service) }
before do
allow(helper).to receive(:alerts_service).and_return(alerts_service)
allow(helper).to receive(:prometheus_service).and_return(prometheus_service)
allow(alerts_service).to receive(:opsgenie_mvc_available?).and_return(opsgenie_available)
end
context 'when available' do
let(:opsgenie_available) { true }
it do
is_expected.to include(
'opsgenie_mvc_available' => 'true',
'opsgenie_mvc_form_path' => project_service_path(project, alerts_service),
'opsgenie_mvc_enabled' => 'false',
'opsgenie_mvc_target_url' => 'https://appname.app.opsgenie.com/alert/list'
)
end
end
context 'when not available' do
let(:opsgenie_keys) do
%w[opsgenie_mvc_available opsgenie_mvc_enabled opsgenie_mvc_form_path opsgenie_mvc_target_url]
end
let(:opsgenie_available) { false }
it { is_expected.not_to include(opsgenie_keys) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::AlertManagementHelper do
let(:project) { build_stubbed(:project) }
let(:current_user) { build_stubbed(:user) }
before do
allow(helper).to receive(:can?)
.with(current_user, :admin_operations, project) { true }
end
describe '#alert_management_data' do
let(:alerts_service) do
build_stubbed(:alerts_service,
project: project,
opsgenie_mvc_enabled: false,
opsgenie_mvc_target_url: 'https://appname.app.opsgenie.com/alert/list'
)
end
subject { helper.alert_management_data(current_user, project) }
before do
allow(project).to receive(:alerts_service).and_return(alerts_service)
allow(alerts_service).to receive(:opsgenie_mvc_available?)
.and_return(opsgenie_available)
end
context 'when available' do
let(:opsgenie_available) { true }
it do
is_expected.to include(
'opsgenie_mvc_available' => 'true',
'opsgenie_mvc_enabled' => 'false',
'opsgenie_mvc_target_url' => 'https://appname.app.opsgenie.com/alert/list'
)
end
end
context 'when not available' do
let(:opsgenie_keys) do
%w[opsgenie_mvc_available opsgenie_mvc_enabled opsgenie_mvc_target_url]
end
let(:opsgenie_available) { false }
it { is_expected.not_to include(opsgenie_keys) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertsService do
let(:service) { build_stubbed(:alerts_service) }
describe 'Opsgenie MVC' do
describe '#opsgenie_mvc_target_url' do
context 'when enabled' do
before do
service.opsgenie_mvc_enabled = true
end
it 'validates presence' do
expect(service).to validate_presence_of(:opsgenie_mvc_target_url)
end
describe 'enforces public urls' do
where(:url, :valid) do
[
['https://appname.app.opsgenie.com/alert/list', true],
['https://example.com', true],
['http://example.com', true],
['http://0.0.0.0', false],
['http://127.0.0.1', false],
['ftp://example.com', false],
['invalid url', false]
]
end
with_them do
before do
service.opsgenie_mvc_target_url = url
end
if params[:valid]
it { expect(service).to be_valid }
else
it { expect(service).to be_invalid }
end
end
end
end
context 'when disabled' do
before do
service.opsgenie_mvc_enabled = false
end
it 'does not validate presence' do
expect(service).not_to validate_presence_of(:opsgenie_mvc_target_url)
end
it 'allows any value' do
service.opsgenie_mvc_target_url = 'any value'
expect(service).to be_valid
end
end
end
describe '#opsgenie_mvc_available?' do
subject { service.opsgenie_mvc_available? }
before do
stub_licensed_features(opsgenie_integration: true)
end
context 'when license is available' do
it { is_expected.to eq(true) }
end
context 'when license is not available' do
before do
stub_licensed_features(opsgenie_integration: false)
end
it { is_expected.to eq(false) }
end
context 'when template service' do
let(:service) { build_stubbed(:alerts_service, :template) }
it { is_expected.to eq(false) }
end
context 'when instance service' do
let(:service) { build_stubbed(:alerts_service, :instance) }
it { is_expected.to eq(false) }
end
end
end
end
......@@ -2060,6 +2060,9 @@ msgstr ""
msgid "AlertManagement|Open"
msgstr ""
msgid "AlertManagement|Opsgenie is enabled"
msgstr ""
msgid "AlertManagement|Overview"
msgstr ""
......@@ -2117,15 +2120,24 @@ msgstr ""
msgid "AlertManagement|Unknown"
msgstr ""
msgid "AlertManagement|View alerts in Opsgenie"
msgstr ""
msgid "AlertManagement|View issue"
msgstr ""
msgid "AlertManagement|You have enabled the Opsgenie integration. Your alerts will be visible directly in Opsgenie."
msgstr ""
msgid "AlertService|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
msgid "AlertService|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page."
msgstr ""
msgid "AlertSettings|API URL"
msgstr ""
msgid "AlertSettings|Active"
msgstr ""
......@@ -2162,7 +2174,7 @@ msgstr ""
msgid "AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}"
msgstr ""
msgid "AlertSettings|Prometheus API Base URL"
msgid "AlertSettings|Opsgenie"
msgstr ""
msgid "AlertSettings|Reset key"
......@@ -2207,9 +2219,6 @@ msgstr ""
msgid "AlertSettings|Your changes were successfully updated."
msgstr ""
msgid "AlertSettings|http://prometheus.example.com/"
msgstr ""
msgid "Alerts"
msgstr ""
......
......@@ -10,6 +10,7 @@ describe('AlertManagementEmptyState', () => {
alertManagementEnabled: false,
userCanEnableAlertManagement: false,
},
stubs = {},
} = {}) {
wrapper = shallowMount(AlertManagementEmptyState, {
propsData: {
......@@ -17,6 +18,7 @@ describe('AlertManagementEmptyState', () => {
emptyAlertSvgPath: 'illustration/path',
...props,
},
stubs,
});
}
......@@ -30,9 +32,23 @@ describe('AlertManagementEmptyState', () => {
}
});
const EmptyState = () => wrapper.find(GlEmptyState);
describe('Empty state', () => {
it('shows empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
expect(EmptyState().exists()).toBe(true);
});
it('show OpsGenie integration state when OpsGenie mcv is true', () => {
mountComponent({
props: {
alertManagementEnabled: false,
userCanEnableAlertManagement: false,
opsgenieMvcEnabled: true,
opsgenieMvcTargetUrl: 'https://opsgenie-url.com',
},
});
expect(EmptyState().props('title')).toBe('Opsgenie is enabled');
});
});
});
......@@ -13,20 +13,20 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</div>
<gl-form-stub>
<gl-form-group-stub label=\\"Integrations\\" label-for=\\"integrations\\" label-class=\\"label-bold\\">
<gl-form-select-stub options=\\"[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-400\\"><gl-sprintf-stub message=\\"Learn more about our %{linkStart}upcoming integrations%{linkEnd}\\"></gl-sprintf-stub></span>
<gl-form-select-stub options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-400\\"><gl-sprintf-stub message=\\"Learn more about our %{linkStart}upcoming integrations%{linkEnd}\\"></gl-sprintf-stub></span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\" label-class=\\"label-bold\\">
<toggle-button-stub id=\\"activated\\"></toggle-button-stub>
</gl-form-group-stub>
<!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\" label-class=\\"label-bold\\">
<gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"true\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-400\\">
<gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-400\\">
</span>
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"true\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
</gl-modal-stub>
......@@ -36,10 +36,10 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</gl-form-group-stub>
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\">
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" type=\\"submit\\">
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">
Save changes
</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" type=\\"reset\\">
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">
Cancel
</gl-button-stub>
</div>
......
......@@ -18,13 +18,19 @@ const defaultProps = {
url: GENERIC_URL,
alertsSetupUrl: INVALID_URL,
alertsUsageUrl: INVALID_URL,
initialActivated: ACTIVATED,
activated: ACTIVATED,
},
prometheus: {
prometheusAuthorizationKey: KEY,
prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL,
prometheusIsActivated: ACTIVATED,
activated: ACTIVATED,
},
opsgenie: {
opsgenieMvcIsAvailable: true,
formPath: INVALID_URL,
activated: ACTIVATED,
opsgenieMvcTargetUrl: GENERIC_URL,
},
};
......@@ -139,7 +145,9 @@ describe('AlertsSettingsForm', () => {
createComponent(
{ prometheus: { ...defaultProps.prometheus, prometheusIsActivated: true } },
{},
true,
{
selectedEndpoint: 'prometheus',
},
);
});
......@@ -151,12 +159,27 @@ describe('AlertsSettingsForm', () => {
expect(findApiUrl().exists()).toBe(true);
});
it('show a valid Alert URL', () => {
expect(findUrl().exists()).toBe(true);
it('shows the correct default API URL', () => {
expect(findUrl().attributes('value')).toBe(PROMETHEUS_URL);
});
});
describe('opsgenie is active', () => {
beforeEach(() => {
createComponent(
{ opsgenie: { ...defaultProps.opsgenie, opsgenieMvcActivated: true } },
{},
{
selectedEndpoint: 'opsgenie',
},
);
});
it('shows a input for the opsgenie target URL', () => {
expect(findApiUrl().exists()).toBe(true);
});
});
describe('trigger test alert', () => {
beforeEach(() => {
createComponent({ generic: { ...defaultProps.generic, initialActivated: true } }, {}, true);
......@@ -168,7 +191,7 @@ describe('AlertsSettingsForm', () => {
});
it('should validate JSON input', () => {
createComponent({ generic: { ...defaultProps.generic } }, {}, true, {
createComponent({ generic: { ...defaultProps.generic } }, true, {
testAlertJson: '{ "value": "test" }',
});
......@@ -186,7 +209,7 @@ describe('AlertsSettingsForm', () => {
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.toggleGenericActivated(toggleService).then(() => {
return wrapper.vm.toggleActivated(toggleService).then(() => {
expect(wrapper.find(GlAlert).attributes('variant')).toBe('info');
});
});
......@@ -198,7 +221,7 @@ describe('AlertsSettingsForm', () => {
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.toggleGenericActivated(toggleService).then(() => {
return wrapper.vm.toggleActivated(toggleService).then(() => {
expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger');
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment