Commit f7e50f0b authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'sy-read-all-integrations' into 'master'

Add GraphQL query for alert integrations

See merge request gitlab-org/gitlab!45622
parents 93865336 e514c353
......@@ -2,7 +2,7 @@
import { GlTable, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import { trackAlertIntergrationsViewsOptions } from '../constants';
import { trackAlertIntegrationsViewsOptions } from '../constants';
export const i18n = {
title: s__('AlertsIntegrations|Current integrations'),
......@@ -64,7 +64,7 @@ export default {
},
methods: {
trackPageViews() {
const { category, action } = trackAlertIntergrationsViewsOptions;
const { category, action } = trackAlertIntegrationsViewsOptions;
Tracking.event(category, action);
},
},
......
<script>
import {
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormSelect,
GlFormInput,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { integrationTypes } from '../constants';
export default {
i18n: {
integrationsInfo: s__(
'AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}',
),
integrationFormSteps: {
step1: s__('AlertSettings|1. Select integration type'),
step2: s__('AlertSettings|2. Name integration'),
},
},
components: {
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLink,
GlSprintf,
},
data() {
return {
selectedIntegration: integrationTypes[0].value,
options: integrationTypes,
formVisible: false,
form: {
name: '',
},
};
},
methods: {
onIntegrationTypeSelect() {
if (this.selectedIntegration === integrationTypes[0].value) {
this.formVisible = false;
} else {
this.formVisible = true;
}
},
onSubmit() {
// TODO Add GraphQL method
},
onReset() {
this.form.name = '';
},
},
};
</script>
<template>
<gl-form class="gl-mt-6" @submit.prevent="onSubmit" @reset.prevent="onReset">
<h5 class="gl-font-lg gl-my-5">{{ s__('AlertSettings|Add new integrations') }}</h5>
<gl-form-group
id="integration-type"
:label="$options.i18n.integrationFormSteps.step1"
label-for="integration-type"
>
<gl-form-select
id="integration-type"
v-model="selectedIntegration"
:options="options"
@change="onIntegrationTypeSelect"
/>
<span class="gl-text-gray-500">
<gl-sprintf :message="$options.i18n.integrationsInfo">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
href="https://gitlab.com/groups/gitlab-org/-/epics/4390"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</gl-form-group>
<gl-collapse v-model="formVisible" class="gl-mt-3">
<gl-form-group
id="name-integration"
:label="$options.i18n.integrationFormSteps.step2"
label-for="name-integration"
>
<gl-form-input
id="name-integration"
v-model="form.name"
type="text"
:placeholder="s__('AlertSettings|Enter integration name')"
/>
</gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button type="reset" class="gl-mr-3 js-no-auto-disable">{{ __('Cancel') }}</gl-button>
<gl-button
type="submit"
category="secondary"
variant="success"
class="gl-mr-1 js-no-auto-disable"
>{{ __('Save and test payload') }}</gl-button
>
<gl-button type="submit" variant="success" class="js-no-auto-disable">{{
s__('AlertSettings|Save integration')
}}</gl-button>
</div>
</gl-collapse>
</gl-form>
</template>
......@@ -14,16 +14,14 @@ import {
GlFormSelect,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { s__ } from '~/locale';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import IntegrationsList from './alerts_integrations_list.vue';
import csrf from '~/lib/utils/csrf';
import service from '../services';
import {
i18n,
serviceOptions,
integrationTypes,
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
targetOpsgenieUrlPlaceholder,
......@@ -50,7 +48,6 @@ export default {
GlSprintf,
ClipboardButton,
ToggleButton,
IntegrationsList,
},
directives: {
'gl-modal': GlModalDirective,
......@@ -59,8 +56,8 @@ export default {
data() {
return {
loading: false,
selectedEndpoint: serviceOptions[0].value,
options: serviceOptions,
selectedIntegration: integrationTypes[1].value,
options: integrationTypes,
active: false,
authKey: '',
targetUrl: '',
......@@ -91,13 +88,13 @@ export default {
];
},
isPrometheus() {
return this.selectedEndpoint === 'prometheus';
return this.selectedIntegration === 'prometheus';
},
isOpsgenie() {
return this.selectedEndpoint === 'opsgenie';
return this.selectedIntegration === 'opsgenie';
},
selectedService() {
switch (this.selectedEndpoint) {
selectedIntegrationType() {
switch (this.selectedIntegration) {
case 'generic': {
return {
url: this.generic.url,
......@@ -152,27 +149,13 @@ export default {
? this.$options.targetOpsgenieUrlPlaceholder
: this.$options.targetPrometheusUrlPlaceholder;
},
integrations() {
return [
{
name: s__('AlertSettings|HTTP endpoint'),
type: s__('AlertsIntegrations|HTTP endpoint'),
activated: this.generic.activated,
},
{
name: s__('AlertSettings|External Prometheus'),
type: s__('AlertsIntegrations|Prometheus'),
activated: this.prometheus.activated,
},
];
},
},
watch: {
'testAlert.json': debounce(function debouncedJsonValidate() {
this.validateJson();
}, JSON_VALIDATE_DELAY),
targetUrl(oldVal, newVal) {
if (newVal && oldVal !== this.selectedService.targetUrl) {
if (newVal && oldVal !== this.selectedIntegrationType.targetUrl) {
this.canSaveForm = true;
}
},
......@@ -187,8 +170,8 @@ export default {
} else if (this.opsgenie.activated) {
this.setOpsgenieAsDefault();
}
this.active = this.selectedService.activated;
this.authKey = this.selectedService.authKey ?? '';
this.active = this.selectedIntegrationType.activated;
this.authKey = this.selectedIntegrationType.authKey ?? '';
},
methods: {
createUserErrorMessage(errors = {}) {
......@@ -205,9 +188,9 @@ export default {
}
return { ...el, disabled: false };
});
this.selectedEndpoint = this.options.find(({ value }) => value === 'opsgenie').value;
this.selectedIntegration = this.options.find(({ value }) => value === 'opsgenie').value;
if (this.targetUrl === null) {
this.targetUrl = this.selectedService.targetUrl;
this.targetUrl = this.selectedIntegrationType.targetUrl;
}
},
removeOpsGenieOption() {
......@@ -220,8 +203,8 @@ export default {
},
resetFormValues() {
this.testAlert.json = null;
this.targetUrl = this.selectedService.targetUrl;
this.active = this.selectedService.activated;
this.targetUrl = this.selectedIntegrationType.targetUrl;
this.active = this.selectedIntegrationType.activated;
},
dismissFeedback() {
this.serverError = null;
......@@ -261,7 +244,7 @@ export default {
this.loading = true;
return service
.updateGenericActive({
endpoint: this[this.selectedEndpoint].formPath,
endpoint: this[this.selectedIntegration].formPath,
params: this.isOpsgenie
? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
: { service: { active: value } },
......@@ -331,9 +314,9 @@ export default {
this.validateJson();
return service
.updateTestAlert({
endpoint: this.selectedService.url,
endpoint: this.selectedIntegrationType.url,
data: this.testAlert.json,
authKey: this.selectedService.authKey,
authKey: this.selectedIntegrationType.authKey,
})
.then(() => {
this.setFeedback({
......@@ -358,11 +341,11 @@ export default {
onReset() {
this.testAlert.json = null;
this.dismissFeedback();
this.targetUrl = this.selectedService.targetUrl;
this.targetUrl = this.selectedIntegrationType.targetUrl;
if (this.canSaveForm) {
this.canSaveForm = false;
this.active = this.selectedService.activated;
this.active = this.selectedIntegrationType.activated;
}
},
},
......@@ -370,153 +353,144 @@ export default {
</script>
<template>
<div>
<integrations-list :integrations="integrations" />
<gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<h5 class="gl-font-lg gl-my-5">{{ $options.i18n.integrationsLabel }}</h5>
<gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<h5 class="gl-font-lg gl-my-5">{{ $options.i18n.integrationsLabel }}</h5>
<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(active)"
>
{{ __('Save anyway') }}
</gl-button>
</gl-alert>
<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(active)"
>
{{ __('Save anyway') }}
</gl-button>
</gl-alert>
<div data-testid="alert-settings-description">
<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>
<div data-testid="alert-settings-description">
<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-group label-for="integration-type" :label="$options.i18n.integration">
<gl-form-select
id="integration-type"
v-model="selectedEndpoint"
:options="options"
data-testid="alert-settings-select"
@change="resetFormValues"
/>
<gl-form-group label-for="integration-type" :label="$options.i18n.integration">
<gl-form-select
id="integration-type"
v-model="selectedIntegration"
:options="options"
data-testid="alert-settings-select"
@change="resetFormValues"
/>
<span class="gl-text-gray-500">
<gl-sprintf :message="$options.i18n.integrationsInfo">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
href="https://gitlab.com/groups/gitlab-org/-/epics/4390"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.activeLabel" label-for="activated">
<toggle-button
id="activated"
:disabled-input="loading"
:is-loading="loading"
:value="active"
@change="toggleService"
/>
</gl-form-group>
<gl-form-group
v-if="isOpsgenie || isPrometheus"
:label="$options.i18n.apiBaseUrlLabel"
label-for="api-url"
>
<gl-form-input
id="api-url"
v-model="targetUrl"
type="url"
:placeholder="baseUrlPlaceholder"
:disabled="!active"
/>
<span class="gl-text-gray-500">
{{ $options.i18n.apiBaseUrlHelpText }}
</span>
</gl-form-group>
<template v-if="!isOpsgenie">
<gl-form-group :label="$options.i18n.urlLabel" label-for="url">
<gl-form-input-group id="url" readonly :value="selectedIntegrationType.url">
<template #append>
<clipboard-button
:text="selectedIntegrationType.url"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
<span class="gl-text-gray-500">
<gl-sprintf :message="$options.i18n.integrationsInfo">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
href="https://gitlab.com/groups/gitlab-org/-/epics/4390"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
{{ prometheusInfo }}
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.activeLabel" label-for="activated">
<toggle-button
id="activated"
:disabled-input="loading"
:is-loading="loading"
:value="active"
@change="toggleService"
/>
<gl-form-group :label="$options.i18n.authKeyLabel" label-for="authorization-key">
<gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="authKey">
<template #append>
<clipboard-button
:text="authKey"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
<gl-button v-gl-modal.authKeyModal :disabled="!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="selectedIntegrationType.resetKey"
>
{{ $options.i18n.restKeyInfo }}
</gl-modal>
</gl-form-group>
<gl-form-group
v-if="isOpsgenie || isPrometheus"
:label="$options.i18n.apiBaseUrlLabel"
label-for="api-url"
:label="$options.i18n.alertJson"
label-for="alert-json"
:invalid-feedback="testAlert.error"
>
<gl-form-input
id="api-url"
v-model="targetUrl"
type="url"
:placeholder="baseUrlPlaceholder"
<gl-form-textarea
id="alert-json"
v-model.trim="testAlert.json"
:disabled="!active"
:state="jsonIsValid"
:placeholder="$options.i18n.alertJsonPlaceholder"
rows="6"
max-rows="10"
/>
<span class="gl-text-gray-500">
{{ $options.i18n.apiBaseUrlHelpText }}
</span>
</gl-form-group>
<template v-if="!isOpsgenie">
<gl-form-group :label="$options.i18n.urlLabel" label-for="url">
<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-500">
{{ prometheusInfo }}
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.authKeyLabel" label-for="authorization-key">
<gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="authKey">
<template #append>
<clipboard-button
:text="authKey"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
<gl-button v-gl-modal.authKeyModal :disabled="!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"
:invalid-feedback="testAlert.error"
>
<gl-form-textarea
id="alert-json"
v-model.trim="testAlert.json"
:disabled="!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 category="primary" :disabled="!canSaveConfig" @click="onReset">
{{ __('Cancel') }}
</gl-button>
</div>
</gl-form>
</div>
<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 category="primary" :disabled="!canSaveConfig" @click="onReset">
{{ __('Cancel') }}
</gl-button>
</div>
</gl-form>
</template>
<script>
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue';
export default {
components: {
IntegrationsList,
SettingsFormOld,
SettingsFormNew,
},
mixins: [glFeatureFlagsMixin()],
inject: {
generic: {
default: {},
},
prometheus: {
default: {},
},
},
computed: {
integrations() {
return [
{
name: s__('AlertSettings|HTTP endpoint'),
type: s__('AlertsIntegrations|HTTP endpoint'),
activated: this.generic.activated,
},
{
name: s__('AlertSettings|External Prometheus'),
type: s__('AlertsIntegrations|Prometheus'),
activated: this.prometheus.activated,
},
];
},
},
};
</script>
<template>
<div>
<integrations-list :integrations="integrations" />
<settings-form-new v-if="glFeatures.httpIntegrationsList" />
<settings-form-old v-else />
</div>
</template>
......@@ -17,11 +17,10 @@ export const i18n = {
changesSaved: s__('AlertSettings|Your integration was successfully updated.'),
prometheusInfo: s__('AlertSettings|Add URL and auth key to your Prometheus config file'),
integrationsInfo: s__(
'AlertSettings|Learn more about our improvements for %{linkStart}integrations%{linkEnd}',
'AlertSettings|Learn more about our our upcoming %{linkStart}integrations%{linkEnd}',
),
resetKey: s__('AlertSettings|Reset key'),
copyToClipboard: s__('AlertSettings|Copy'),
integrationsLabel: s__('AlertSettings|Add new integrations'),
apiBaseUrlLabel: s__('AlertSettings|API URL'),
authKeyLabel: s__('AlertSettings|Authorization key'),
urlLabel: s__('AlertSettings|Webhook URL'),
......@@ -40,7 +39,8 @@ export const i18n = {
integration: s__('AlertSettings|Integration'),
};
export const serviceOptions = [
export const integrationTypes = [
{ value: '', text: s__('AlertSettings|Select integration type') },
{ value: 'generic', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
{ value: 'opsgenie', text: s__('AlertSettings|Opsgenie') },
......@@ -56,9 +56,9 @@ export const sectionHash = 'js-alert-management-settings';
/* eslint-disable @gitlab/require-i18n-strings */
/**
* Tracks snowplow event when user views alerts intergration list
* Tracks snowplow event when user views alerts integration list
*/
export const trackAlertIntergrationsViewsOptions = {
category: 'Alert Intergrations',
export const trackAlertIntegrationsViewsOptions = {
category: 'Alert Integrations',
action: 'view_alert_integrations_list',
};
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertSettingsForm from './components/alerts_settings_form.vue';
import AlertSettingsWrapper from './components/alerts_settings_wrapper.vue';
export default el => {
if (!el) {
......@@ -26,16 +26,11 @@ export default el => {
opsgenieMvcTargetUrl,
} = el.dataset;
const genericActivated = parseBoolean(activatedStr);
const prometheusIsActivated = parseBoolean(prometheusActivated);
const opsgenieMvcActivated = parseBoolean(opsgenieMvcEnabled);
const opsgenieMvcIsAvailable = parseBoolean(opsgenieMvcAvailable);
return new Vue({
el,
provide: {
prometheus: {
activated: prometheusIsActivated,
activated: parseBoolean(prometheusActivated),
prometheusUrl,
authorizationKey: prometheusAuthorizationKey,
prometheusFormPath,
......@@ -45,23 +40,23 @@ export default el => {
generic: {
alertsSetupUrl,
alertsUsageUrl,
activated: genericActivated,
activated: parseBoolean(activatedStr),
formPath,
authorizationKey,
url,
},
opsgenie: {
formPath: opsgenieMvcFormPath,
activated: opsgenieMvcActivated,
activated: parseBoolean(opsgenieMvcEnabled),
opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable,
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
},
},
components: {
AlertSettingsForm,
AlertSettingsWrapper,
},
render(createElement) {
return createElement('alert-settings-form');
return createElement('alert-settings-wrapper');
},
});
};
......@@ -6,6 +6,10 @@ module Projects
before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
before_action do
push_frontend_feature_flag(:http_integrations_list, @project)
end
respond_to :json, only: [:reset_alerting_token, :reset_pagerduty_token]
helper_method :error_tracking_setting
......
# frozen_string_literal: true
module Resolvers
module AlertManagement
class IntegrationsResolver < BaseResolver
alias_method :project, :synchronized_object
def resolve(**args)
return [] unless Feature.enabled?(:multiple_http_integrations, project)
http_integrations + prometheus_integrations
end
private
def prometheus_integrations
return [] unless Ability.allowed?(current_user, :admin_project, project)
Array(project.prometheus_service)
end
def http_integrations
return [] unless Ability.allowed?(current_user, :admin_operations, project)
::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute
end
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
class HttpIntegrationType < BaseObject
graphql_name 'AlertManagementHttpIntegration'
description 'An endpoint and credentials used to accept alerts for a project'
implements(Types::AlertManagement::IntegrationType)
authorize :admin_operations
def type
:http
end
def api_url
nil
end
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
module IntegrationType
include Types::BaseInterface
graphql_name 'AlertManagementIntegration'
field :id,
GraphQL::ID_TYPE,
null: false,
description: 'ID of the integration'
field :type,
AlertManagement::IntegrationTypeEnum,
null: false,
description: 'Type of integration'
field :name,
GraphQL::STRING_TYPE,
null: true,
description: 'Name of the integration'
field :active,
GraphQL::BOOLEAN_TYPE,
null: true,
description: 'Whether the endpoint is currently accepting alerts'
field :token,
GraphQL::STRING_TYPE,
null: true,
description: 'Token used to authenticate alert notification requests'
field :url,
GraphQL::STRING_TYPE,
null: true,
description: 'Endpoint which accepts alert notifications'
field :api_url,
GraphQL::STRING_TYPE,
null: true,
description: 'URL at which Prometheus metrics can be queried to populate the metrics dashboard'
definition_methods do
def resolve_type(object, context)
if object.is_a?(::PrometheusService)
Types::AlertManagement::PrometheusIntegrationType
else
Types::AlertManagement::HttpIntegrationType
end
end
end
orphan_types Types::AlertManagement::PrometheusIntegrationType,
Types::AlertManagement::HttpIntegrationType
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
class IntegrationTypeEnum < BaseEnum
graphql_name 'AlertManagementIntegrationType'
description 'Values of types of integrations'
value 'PROMETHEUS', 'Prometheus integration', value: :prometheus
value 'HTTP', 'Integration with any monitoring tool', value: :http
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
class PrometheusIntegrationType < BaseObject
include ::Gitlab::Routing
graphql_name 'AlertManagementPrometheusIntegration'
description 'An endpoint and credentials used to accept Prometheus alerts for a project'
implements(Types::AlertManagement::IntegrationType)
authorize :admin_project
alias_method :prometheus_service, :object
def name
prometheus_service.title
end
def type
:prometheus
end
def token
prometheus_service.project&.alerting_setting&.token
end
def url
prometheus_service.project && notify_project_prometheus_alerts_url(prometheus_service.project, format: :json)
end
def active
prometheus_service.manual_configuration?
end
end
end
end
......@@ -267,6 +267,12 @@ module Types
description: 'Counts of alerts by status for the project',
resolver: Resolvers::AlertManagement::AlertStatusCountsResolver
field :alert_management_integrations,
Types::AlertManagement::IntegrationType.connection_type,
null: true,
description: 'Integrations which can receive alerts for the project',
resolver: Resolvers::AlertManagement::IntegrationsResolver
field :releases,
Types::ReleaseType.connection_type,
null: true,
......
......@@ -2,6 +2,7 @@
module AlertManagement
class HttpIntegration < ApplicationRecord
include ::Gitlab::Routing
LEGACY_IDENTIFIER = 'legacy'
DEFAULT_NAME_SLUG = 'http-endpoint'
......@@ -31,9 +32,9 @@ module AlertManagement
scope :ordered_by_id, -> { order(:id) }
def url
return ::Gitlab::Routing.url_helpers.project_alerts_notify_url(project, format: :json) if legacy?
return project_alerts_notify_url(project, format: :json) if legacy?
::Gitlab::Routing.url_helpers.project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json)
project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json)
end
private
......
# frozen_string_literal: true
module AlertManagement
class HttpIntegrationPolicy < ::BasePolicy
delegate { @subject.project }
end
end
# frozen_string_literal: true
class PrometheusServicePolicy < ::BasePolicy
delegate { @subject.project }
end
---
name: http_integrations_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45993
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255502
type: development
group: group::health
default_enabled: false
......@@ -590,6 +590,173 @@ type AlertManagementAlertStatusCountsType {
triggered: Int
}
"""
An endpoint and credentials used to accept alerts for a project
"""
type AlertManagementHttpIntegration implements AlertManagementIntegration {
"""
Whether the endpoint is currently accepting alerts
"""
active: Boolean
"""
URL at which Prometheus metrics can be queried to populate the metrics dashboard
"""
apiUrl: String
"""
ID of the integration
"""
id: ID!
"""
Name of the integration
"""
name: String
"""
Token used to authenticate alert notification requests
"""
token: String
"""
Type of integration
"""
type: AlertManagementIntegrationType!
"""
Endpoint which accepts alert notifications
"""
url: String
}
interface AlertManagementIntegration {
"""
Whether the endpoint is currently accepting alerts
"""
active: Boolean
"""
URL at which Prometheus metrics can be queried to populate the metrics dashboard
"""
apiUrl: String
"""
ID of the integration
"""
id: ID!
"""
Name of the integration
"""
name: String
"""
Token used to authenticate alert notification requests
"""
token: String
"""
Type of integration
"""
type: AlertManagementIntegrationType!
"""
Endpoint which accepts alert notifications
"""
url: String
}
"""
The connection type for AlertManagementIntegration.
"""
type AlertManagementIntegrationConnection {
"""
A list of edges.
"""
edges: [AlertManagementIntegrationEdge]
"""
A list of nodes.
"""
nodes: [AlertManagementIntegration]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type AlertManagementIntegrationEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: AlertManagementIntegration
}
"""
Values of types of integrations
"""
enum AlertManagementIntegrationType {
"""
Integration with any monitoring tool
"""
HTTP
"""
Prometheus integration
"""
PROMETHEUS
}
"""
An endpoint and credentials used to accept Prometheus alerts for a project
"""
type AlertManagementPrometheusIntegration implements AlertManagementIntegration {
"""
Whether the endpoint is currently accepting alerts
"""
active: Boolean
"""
URL at which Prometheus metrics can be queried to populate the metrics dashboard
"""
apiUrl: String
"""
ID of the integration
"""
id: ID!
"""
Name of the integration
"""
name: String
"""
Token used to authenticate alert notification requests
"""
token: String
"""
Type of integration
"""
type: AlertManagementIntegrationType!
"""
Endpoint which accepts alert notifications
"""
url: String
}
"""
Alert severity values
"""
......@@ -13747,6 +13914,31 @@ type Project {
statuses: [AlertManagementStatus!]
): AlertManagementAlertConnection
"""
Integrations which can receive alerts for the project
"""
alertManagementIntegrations(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): AlertManagementIntegrationConnection
"""
If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge
requests of the project can also be merged with skipped jobs
......
......@@ -1480,6 +1480,515 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementHttpIntegration",
"description": "An endpoint and credentials used to accept alerts for a project",
"fields": [
{
"name": "active",
"description": "Whether the endpoint is currently accepting alerts",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "apiUrl",
"description": "URL at which Prometheus metrics can be queried to populate the metrics dashboard",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the integration",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the integration",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "token",
"description": "Token used to authenticate alert notification requests",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of integration",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "AlertManagementIntegrationType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "Endpoint which accepts alert notifications",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "AlertManagementIntegration",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "AlertManagementIntegration",
"description": null,
"fields": [
{
"name": "active",
"description": "Whether the endpoint is currently accepting alerts",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "apiUrl",
"description": "URL at which Prometheus metrics can be queried to populate the metrics dashboard",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the integration",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the integration",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "token",
"description": "Token used to authenticate alert notification requests",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of integration",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "AlertManagementIntegrationType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "Endpoint which accepts alert notifications",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "AlertManagementHttpIntegration",
"ofType": null
},
{
"kind": "OBJECT",
"name": "AlertManagementPrometheusIntegration",
"ofType": null
}
]
},
{
"kind": "OBJECT",
"name": "AlertManagementIntegrationConnection",
"description": "The connection type for AlertManagementIntegration.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AlertManagementIntegrationEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "INTERFACE",
"name": "AlertManagementIntegration",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementIntegrationEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "AlertManagementIntegration",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "AlertManagementIntegrationType",
"description": "Values of types of integrations",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "PROMETHEUS",
"description": "Prometheus integration",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "HTTP",
"description": "Integration with any monitoring tool",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementPrometheusIntegration",
"description": "An endpoint and credentials used to accept Prometheus alerts for a project",
"fields": [
{
"name": "active",
"description": "Whether the endpoint is currently accepting alerts",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "apiUrl",
"description": "URL at which Prometheus metrics can be queried to populate the metrics dashboard",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the integration",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the integration",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "token",
"description": "Token used to authenticate alert notification requests",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of integration",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "AlertManagementIntegrationType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "Endpoint which accepts alert notifications",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "AlertManagementIntegration",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "AlertManagementSeverity",
......@@ -40456,6 +40965,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertManagementIntegrations",
"description": "Integrations which can receive alerts for the project",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementIntegrationConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "allowMergeOnSkippedPipeline",
"description": "If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs",
......@@ -106,6 +106,34 @@ Represents total number of alerts for the represented categories.
| `resolved` | Int | Number of alerts with status RESOLVED for the project |
| `triggered` | Int | Number of alerts with status TRIGGERED for the project |
### AlertManagementHttpIntegration
An endpoint and credentials used to accept alerts for a project.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `active` | Boolean | Whether the endpoint is currently accepting alerts |
| `apiUrl` | String | URL at which Prometheus metrics can be queried to populate the metrics dashboard |
| `id` | ID! | ID of the integration |
| `name` | String | Name of the integration |
| `token` | String | Token used to authenticate alert notification requests |
| `type` | AlertManagementIntegrationType! | Type of integration |
| `url` | String | Endpoint which accepts alert notifications |
### AlertManagementPrometheusIntegration
An endpoint and credentials used to accept Prometheus alerts for a project.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `active` | Boolean | Whether the endpoint is currently accepting alerts |
| `apiUrl` | String | URL at which Prometheus metrics can be queried to populate the metrics dashboard |
| `id` | ID! | ID of the integration |
| `name` | String | Name of the integration |
| `token` | String | Token used to authenticate alert notification requests |
| `type` | AlertManagementIntegrationType! | Type of integration |
| `url` | String | Endpoint which accepts alert notifications |
### AlertSetAssigneesPayload
Autogenerated return type of AlertSetAssignees.
......@@ -3241,6 +3269,15 @@ Values for sorting alerts.
| `updated_asc` **{warning-solid}** | **Deprecated:** Use UPDATED_ASC. Deprecated in 13.5 |
| `updated_desc` **{warning-solid}** | **Deprecated:** Use UPDATED_DESC. Deprecated in 13.5 |
### AlertManagementIntegrationType
Values of types of integrations.
| Value | Description |
| ----- | ----------- |
| `HTTP` | Integration with any monitoring tool |
| `PROMETHEUS` | Prometheus integration |
### AlertManagementSeverity
Alert severity values.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting Alert Management Integrations' do
include ::Gitlab::Routing.url_helpers
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:prometheus_service) { create(:prometheus_service, project: project) }
let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('AlertManagementIntegration')}
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementIntegrations', {}, fields)
)
end
before do
stub_licensed_features(multiple_alert_http_integrations: true)
end
context 'with integrations' do
let(:integrations) { graphql_data.dig('project', 'alertManagementIntegrations', 'nodes') }
context 'without project permissions' do
let(:user) { create(:user) }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
specify { expect(integrations).to be_nil }
end
context 'with project permissions' do
before do
project.add_maintainer(current_user)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
specify { expect(integrations.size).to eq(3) }
it 'returns the correct properties of the integrations' do
expect(integrations).to include(
{
'id' => GitlabSchema.id_from_object(active_http_integration).to_s,
'type' => 'HTTP',
'name' => active_http_integration.name,
'active' => active_http_integration.active,
'token' => active_http_integration.token,
'url' => active_http_integration.url,
'apiUrl' => nil
},
{
'id' => GitlabSchema.id_from_object(inactive_http_integration).to_s,
'type' => 'HTTP',
'name' => inactive_http_integration.name,
'active' => inactive_http_integration.active,
'token' => inactive_http_integration.token,
'url' => inactive_http_integration.url,
'apiUrl' => nil
},
{
'id' => GitlabSchema.id_from_object(prometheus_service).to_s,
'type' => 'PROMETHEUS',
'name' => 'Prometheus',
'active' => prometheus_service.manual_configuration?,
'token' => project_alerting_setting.token,
'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
'apiUrl' => prometheus_service.api_url
}
)
end
end
end
end
......@@ -2486,6 +2486,12 @@ 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|1. Select integration type"
msgstr ""
msgid "AlertSettings|2. Name integration"
msgstr ""
msgid "AlertSettings|API URL"
msgstr ""
......@@ -2510,6 +2516,9 @@ msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
msgid "AlertSettings|Enter integration name"
msgstr ""
msgid "AlertSettings|Enter test alert JSON...."
msgstr ""
......@@ -2525,7 +2534,10 @@ msgstr ""
msgid "AlertSettings|Integration"
msgstr ""
msgid "AlertSettings|Learn more about our improvements for %{linkStart}integrations%{linkEnd}"
msgid "AlertSettings|Learn more about our our upcoming %{linkStart}integrations%{linkEnd}"
msgstr ""
msgid "AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}"
msgstr ""
msgid "AlertSettings|Opsgenie"
......@@ -2540,6 +2552,12 @@ msgstr ""
msgid "AlertSettings|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 "AlertSettings|Save integration"
msgstr ""
msgid "AlertSettings|Select integration type"
msgstr ""
msgid "AlertSettings|Test alert payload"
msgstr ""
......@@ -23040,6 +23058,9 @@ msgstr ""
msgid "Save Push Rules"
msgstr ""
msgid "Save and test payload"
msgstr ""
msgid "Save anyway"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Alert integrations settings form', :js do
let_it_be(:project) { create(:project) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
before_all do
project.add_maintainer(maintainer)
project.add_developer(developer)
end
before do
sign_in(maintainer)
end
describe 'when viewing alert integrations as a maintainer' do
context 'with feature flag enabled' do
before do
visit project_settings_operations_path(project, anchor: 'js-alert-management-settings')
wait_for_requests
end
it 'shows the alerts setting form title' do
page.within('#js-alert-management-settings') do
expect(find('h3')).to have_content('Alerts')
end
end
it 'shows the new alerts setting form' do
expect(page).to have_content('1. Select integration type')
end
end
context 'with feature flag disabled' do
before do
stub_feature_flags(http_integrations_list: false)
visit project_settings_operations_path(project, anchor: 'js-alert-management-settings')
wait_for_requests
end
it 'shows the old alerts setting form' do
expect(page).to have_content('Webhook URL')
end
end
end
describe 'when viewing alert integrations as a developer' do
before do
sign_in(developer)
visit project_settings_operations_path(project, anchor: 'js-alert-management-settings')
wait_for_requests
end
it 'shows the old alerts setting form' do
expect(page).not_to have_selector('.incident-management-list')
expect(page).not_to have_selector('#js-alert-management-settings')
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
"<div>
<integrations-list-stub integrations=\\"[object Object],[object Object]\\"></integrations-list-stub>
<gl-form-stub>
<h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5>
<!---->
<div data-testid=\\"alert-settings-description\\">
<p>
<gl-sprintf-stub message=\\"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.\\"></gl-sprintf-stub>
</p>
<p>
<gl-sprintf-stub message=\\"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.\\"></gl-sprintf-stub>
</p>
</div>
<gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\">
<gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our improvements for %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\">
<toggle-button-stub id=\\"activated\\"></toggle-button-stub>
</gl-form-group-stub>
<!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\">
<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-500\\">
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\">
<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=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" 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>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Alert test payload\\" label-for=\\"alert-json\\">
<gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub>
</gl-form-group-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" 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=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
Save changes
</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
Cancel
</gl-button-stub>
</div>
</gl-form-stub>
</div>"
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = `
"<gl-form-stub class=\\"gl-mt-6\\">
<h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5>
<gl-form-group-stub id=\\"integration-type\\" label=\\"1. Select integration type\\" label-for=\\"integration-type\\">
<gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object],[object Object]\\" value=\\"\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span>
</gl-form-group-stub>
<b-collapse-stub tag=\\"div\\" class=\\"gl-mt-3\\">
<gl-form-group-stub id=\\"name-integration\\" label=\\"2. Name integration\\" label-for=\\"name-integration\\">
<b-form-input-stub id=\\"name-integration\\" value=\\"\\" placeholder=\\"Enter integration name\\" debounce=\\"0\\" type=\\"text\\" class=\\"gl-form-input\\"></b-form-input-stub>
</gl-form-group-stub>
<div class=\\"gl-display-flex gl-justify-content-end\\">
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"reset\\" class=\\"gl-mr-3 js-no-auto-disable\\">Cancel</gl-button-stub>
<gl-button-stub category=\\"secondary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"submit\\" class=\\"gl-mr-1 js-no-auto-disable\\">Save and test payload</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"submit\\" class=\\"js-no-auto-disable\\">Save integration</gl-button-stub>
</div>
</b-collapse-stub>
</gl-form-stub>"
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
"<gl-form-stub>
<h5 class=\\"gl-font-lg gl-my-5\\"></h5>
<!---->
<div data-testid=\\"alert-settings-description\\">
<p>
<gl-sprintf-stub message=\\"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.\\"></gl-sprintf-stub>
</p>
<p>
<gl-sprintf-stub message=\\"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.\\"></gl-sprintf-stub>
</p>
</div>
<gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\">
<gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\">
<toggle-button-stub id=\\"activated\\"></toggle-button-stub>
</gl-form-group-stub>
<!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\">
<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-500\\">
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\">
<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=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" 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>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Alert test payload\\" label-for=\\"alert-json\\">
<gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub>
</gl-form-group-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" 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=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
Save changes
</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
Cancel
</gl-button-stub>
</div>
</gl-form-stub>"
`;
......@@ -4,7 +4,7 @@ import Tracking from '~/tracking';
import AlertIntegrationsList, {
i18n,
} from '~/alerts_settings/components/alerts_integrations_list.vue';
import { trackAlertIntergrationsViewsOptions } from '~/alerts_settings/constants';
import { trackAlertIntegrationsViewsOptions } from '~/alerts_settings/constants';
const mockIntegrations = [
{
......@@ -82,7 +82,7 @@ describe('AlertIntegrationsList', () => {
});
it('should track alert list page views', () => {
const { category, action } = trackAlertIntergrationsViewsOptions;
const { category, action } = trackAlertIntegrationsViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
......
import { shallowMount } from '@vue/test-utils';
import { GlForm, GlFormSelect, GlCollapse, GlFormInput } from '@gitlab/ui';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue';
import { defaultAlertSettingsConfig } from './util';
jest.mock('~/alerts_settings/services');
describe('AlertsSettingsFormNew', () => {
let wrapper;
const createComponent = ({ methods } = {}, data) => {
wrapper = shallowMount(AlertsSettingsForm, {
data() {
return { ...data };
},
provide: {
...defaultAlertSettingsConfig,
},
methods,
stubs: { GlCollapse, GlFormInput },
});
};
const findForm = () => wrapper.find(GlForm);
const findSelect = () => wrapper.find(GlFormSelect);
const findFormSteps = () => wrapper.find(GlCollapse);
const findFormName = () => wrapper.find(GlFormInput);
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('with default values', () => {
beforeEach(() => {
createComponent();
});
it('renders the initial template', () => {
expect(wrapper.html()).toMatchSnapshot();
});
it('render the initial form with only an integration type dropdown', () => {
expect(findForm().exists()).toBe(true);
expect(findSelect().exists()).toBe(true);
expect(findFormSteps().attributes('visible')).toBeUndefined();
});
it('shows the rest of the form when the dropdown is used', async () => {
findSelect().vm.$emit('change', 'prometheus');
await wrapper.vm.$nextTick();
expect(findFormName().isVisible()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlAlert } from '@gitlab/ui';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_old.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import { i18n } from '~/alerts_settings/constants';
import service from '~/alerts_settings/services';
import { defaultAlertSettingsConfig } from './util';
jest.mock('~/alerts_settings/services');
const PROMETHEUS_URL = '/prometheus/alerts/notify.json';
const GENERIC_URL = '/alerts/notify.json';
const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid';
const ACTIVATED = false;
describe('AlertsSettingsForm', () => {
let wrapper;
......@@ -23,26 +17,7 @@ describe('AlertsSettingsForm', () => {
return { ...data };
},
provide: {
generic: {
authorizationKey: KEY,
formPath: INVALID_URL,
url: GENERIC_URL,
alertsSetupUrl: INVALID_URL,
alertsUsageUrl: INVALID_URL,
activated: ACTIVATED,
},
prometheus: {
authorizationKey: KEY,
prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL,
activated: ACTIVATED,
},
opsgenie: {
opsgenieMvcIsAvailable: true,
formPath: INVALID_URL,
activated: ACTIVATED,
opsgenieMvcTargetUrl: GENERIC_URL,
},
...defaultAlertSettingsConfig,
},
methods,
});
......@@ -63,7 +38,10 @@ describe('AlertsSettingsForm', () => {
});
afterEach(() => {
wrapper.destroy();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('with default values', () => {
......@@ -76,11 +54,6 @@ describe('AlertsSettingsForm', () => {
});
});
it('renders alerts integrations list', () => {
createComponent();
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
});
describe('reset key', () => {
it('triggers resetKey method', () => {
const resetKey = jest.fn();
......@@ -140,7 +113,7 @@ describe('AlertsSettingsForm', () => {
createComponent(
{},
{
selectedEndpoint: 'prometheus',
selectedIntegration: 'prometheus',
},
);
});
......@@ -154,7 +127,9 @@ describe('AlertsSettingsForm', () => {
});
it('shows the correct default API URL', () => {
expect(findUrl().attributes('value')).toBe(PROMETHEUS_URL);
expect(findUrl().attributes('value')).toBe(
defaultAlertSettingsConfig.prometheus.prometheusUrl,
);
});
});
......@@ -163,7 +138,7 @@ describe('AlertsSettingsForm', () => {
createComponent(
{},
{
selectedEndpoint: 'opsgenie',
selectedIntegration: 'opsgenie',
},
);
});
......
import { shallowMount } from '@vue/test-utils';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import { defaultAlertSettingsConfig } from './util';
jest.mock('~/alerts_settings/services');
describe('AlertsSettingsFormWrapper', () => {
let wrapper;
const createComponent = (data = {}, provide = {}) => {
wrapper = shallowMount(AlertsSettingsWrapper, {
data() {
return { ...data };
},
provide: {
...defaultAlertSettingsConfig,
glFeatures: { httpIntegrationsList: false },
...provide,
},
});
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('with default values', () => {
it('renders alerts integrations list and old form by default', () => {
createComponent();
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
expect(wrapper.find(AlertsSettingsFormOld).exists()).toBe(true);
expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(false);
});
it('renders alerts integrations list and new form when httpIntegrationsList feature flag is enabled', () => {
createComponent({}, { glFeatures: { httpIntegrationsList: true } });
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
expect(wrapper.find(AlertsSettingsFormOld).exists()).toBe(false);
expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(true);
});
});
});
const PROMETHEUS_URL = '/prometheus/alerts/notify.json';
const GENERIC_URL = '/alerts/notify.json';
const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid';
const ACTIVATED = false;
export const defaultAlertSettingsConfig = {
generic: {
authorizationKey: KEY,
formPath: INVALID_URL,
url: GENERIC_URL,
alertsSetupUrl: INVALID_URL,
alertsUsageUrl: INVALID_URL,
activated: ACTIVATED,
},
prometheus: {
authorizationKey: KEY,
prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL,
activated: ACTIVATED,
},
opsgenie: {
opsgenieMvcIsAvailable: true,
formPath: INVALID_URL,
activated: ACTIVATED,
opsgenieMvcTargetUrl: GENERIC_URL,
},
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:prometheus_integration) { create(:prometheus_service, project: project) }
let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_proj_integration) { create(:alert_management_http_integration) }
subject { sync(resolve_http_integrations) }
context 'user does not have permission' do
it { is_expected.to be_empty }
end
context 'user has permission' do
before do
project.add_maintainer(current_user)
end
it { is_expected.to contain_exactly(active_http_integration, prometheus_integration) }
context 'feature flag is not enabled' do
before do
stub_feature_flags(multiple_http_integrations: false)
end
it { is_expected.to be_empty }
end
end
private
def resolve_http_integrations(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['AlertManagementHttpIntegration'] do
specify { expect(described_class.graphql_name).to eq('AlertManagementHttpIntegration') }
specify { expect(described_class).to require_graphql_authorizations(:admin_operations) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['AlertManagementIntegrationType'] do
specify { expect(described_class.graphql_name).to eq('AlertManagementIntegrationType') }
describe 'statuses' do
using RSpec::Parameterized::TableSyntax
where(:name, :value) do
'PROMETHEUS' | :prometheus
'HTTP' | :http
end
with_them do
it 'exposes a type with the correct value' do
expect(described_class.values[name].value).to eq(value)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['AlertManagementIntegration'] do
specify { expect(described_class.graphql_name).to eq('AlertManagementIntegration') }
it 'exposes the expected fields' do
expected_fields = %i[
id
type
name
active
token
url
api_url
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do
include GraphqlHelpers
specify { expect(described_class.graphql_name).to eq('AlertManagementPrometheusIntegration') }
specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
describe 'resolvers' do
shared_examples_for 'has field with value' do |field_name|
it 'correctly renders the field' do
expect(resolve_field(field_name, integration)).to eq(value)
end
end
let_it_be_with_reload(:integration) { create(:prometheus_service) }
it_behaves_like 'has field with value', 'name' do
let(:value) { integration.title }
end
it_behaves_like 'has field with value', 'type' do
let(:value) { :prometheus }
end
it_behaves_like 'has field with value', 'token' do
let(:value) { nil }
end
it_behaves_like 'has field with value', 'url' do
let(:value) { "http://localhost/#{integration.project.full_path}/prometheus/alerts/notify.json" }
end
it_behaves_like 'has field with value', 'active' do
let(:value) { integration.manual_configuration? }
end
context 'with alerting setting' do
let_it_be(:alerting_setting) { create(:project_alerting_setting, project: integration.project) }
it_behaves_like 'has field with value', 'token' do
let(:value) { alerting_setting.token }
end
end
context 'without project' do
let_it_be(:integration) { create(:prometheus_service, project: nil, group: create(:group)) }
it_behaves_like 'has field with value', 'token' do
let(:value) { nil }
end
it_behaves_like 'has field with value', 'url' do
let(:value) { nil }
end
end
end
end
......@@ -27,7 +27,8 @@ RSpec.describe GitlabSchema.types['Project'] do
environment boards jira_import_status jira_imports services releases release
alert_management_alerts alert_management_alert alert_management_alert_status_counts
container_expiration_policy service_desk_enabled service_desk_address
issue_status_counts terraform_states
issue_status_counts terraform_states alert_management_integrations
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::HttpIntegrationPolicy, :models do
let(:integration) { create(:alert_management_http_integration) }
let(:project) { integration.project }
let(:user) { create(:user) }
subject(:policy) { described_class.new(user, integration) }
describe 'rules' do
it { is_expected.to be_disallowed :admin_operations }
context 'when maintainer' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_allowed :admin_operations }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PrometheusServicePolicy, :models do
let(:integration) { create(:prometheus_service) }
let(:project) { integration.project }
let(:user) { create(:user) }
subject(:policy) { described_class.new(user, integration) }
describe 'rules' do
it { is_expected.to be_disallowed :admin_project }
context 'when maintainer' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_allowed :admin_project }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting Alert Management Integrations' do
include ::Gitlab::Routing
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:prometheus_service) { create(:prometheus_service, project: project) }
let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('AlertManagementIntegration')}
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementIntegrations', {}, fields)
)
end
context 'with integrations' do
let(:integrations) { graphql_data.dig('project', 'alertManagementIntegrations', 'nodes') }
context 'without project permissions' do
let(:user) { create(:user) }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(integrations).to be_nil }
end
context 'with project permissions' do
before do
project.add_maintainer(current_user)
post_graphql(query, current_user: current_user)
end
let(:http_integration) { integrations.first }
let(:prometheus_integration) { integrations.second }
it_behaves_like 'a working graphql query'
it { expect(integrations.size).to eq(2) }
it 'returns the correct properties of the integrations' do
expect(http_integration).to include(
'id' => GitlabSchema.id_from_object(active_http_integration).to_s,
'type' => 'HTTP',
'name' => active_http_integration.name,
'active' => active_http_integration.active,
'token' => active_http_integration.token,
'url' => active_http_integration.url,
'apiUrl' => nil
)
expect(prometheus_integration).to include(
'id' => GitlabSchema.id_from_object(prometheus_service).to_s,
'type' => 'PROMETHEUS',
'name' => 'Prometheus',
'active' => prometheus_service.manual_configuration?,
'token' => project_alerting_setting.token,
'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
'apiUrl' => prometheus_service.api_url
)
end
end
end
end
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