Commit e5107a23 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'alert-endpoint-dropdown' into 'master'

Alert endpoint dropdown

See merge request gitlab-org/gitlab!35474
parents 8e91088d 22fb024a
...@@ -123,9 +123,6 @@ export default { ...@@ -123,9 +123,6 @@ export default {
.then(() => { .then(() => {
this.activated = value; this.activated = value;
this.loadingActivated = false; this.loadingActivated = false;
if (value) {
window.location.reload();
}
}) })
.catch(() => { .catch(() => {
createFlash(__('Update failed. Please try again.')); createFlash(__('Update failed. Please try again.'));
......
<script>
import {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlModal,
GlModalDirective,
GlSprintf,
GlFormSelect,
} from '@gitlab/ui';
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 } from '../constants';
export default {
i18n,
csrf,
components: {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLink,
GlModal,
GlSprintf,
ClipboardButton,
ToggleButton,
},
directives: {
'gl-modal': GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
prometheus: {
type: Object,
required: true,
validator: ({ prometheusIsActivated }) => {
return prometheusIsActivated !== undefined;
},
},
generic: {
type: Object,
required: true,
validator: ({ formPath }) => {
return formPath !== undefined;
},
},
},
data() {
return {
activated: {
generic: this.generic.initialActivated,
prometheus: this.prometheus.prometheusIsActivated,
},
loading: false,
authorizationKey: {
generic: this.generic.initialAuthorizationKey,
prometheus: this.prometheus.prometheusAuthorizationKey,
},
selectedEndpoint: null,
options: serviceOptions,
prometheusApiKey: this.prometheus.prometheusApiUrl,
feedback: {
variant: 'danger',
feedbackMessage: null,
isFeedbackDismissed: false,
},
};
},
computed: {
sections() {
return [
{
text: this.$options.i18n.usageSection,
url: this.generic.alertsUsageUrl,
},
{
text: this.$options.i18n.setupSection,
url: this.generic.alertsSetupUrl,
},
];
},
isGeneric() {
return this.selectedEndpoint === 'generic';
},
selectedService() {
return this.isGeneric
? {
url: this.generic.url,
authKey: this.authorizationKey.generic,
active: this.activated.generic,
resetKey: this.resetGenericKey.bind(this),
}
: {
authKey: this.authorizationKey.prometheus,
url: this.prometheus.prometheusUrl,
active: this.activated.prometheus,
resetKey: this.resetPrometheusKey.bind(this),
};
},
showFeedbackMsg() {
return this.feedback.feedbackMessage && !this.isFeedbackDismissed;
},
prometheusInfo() {
return !this.isGeneric ? this.$options.i18n.prometheusInfo : '';
},
prometheusFeatureEnabled() {
return !this.isGeneric && this.glFeatures.alertIntegrationsDropdown;
},
},
created() {
if (this.glFeatures.alertIntegrationsDropdown) {
this.selectedEndpoint = this.prometheus.prometheusIsActivated
? this.options[1].value
: this.options[0].value;
} else {
this.selectedEndpoint = this.options[0].value;
}
},
methods: {
dismissFeedback() {
this.feedback = { ...this.feedback, feedbackMessage: null };
this.isFeedbackDismissed = false;
},
resetGenericKey() {
return service
.updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } })
.then(({ data: { token } }) => {
this.authorizationKey.generic = token;
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
resetPrometheusKey() {
return service
.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath })
.then(({ data: { token } }) => {
this.authorizationKey.prometheus = token;
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
toggleActivated(value) {
return this.isGeneric
? this.toggleGenericActivated(value)
: this.togglePrometheusActive(value);
},
toggleGenericActivated(value) {
this.loading = true;
return service
.updateGenericActive({
endpoint: this.generic.formPath,
params: { service: { active: value } },
})
.then(() => {
this.activated.generic = value;
if (value) {
this.setFeedback({
feedbackMessage: this.$options.i18n.endPointActivated,
variant: 'success',
});
}
})
.catch(() => {})
.finally(() => {
this.loading = false;
});
},
togglePrometheusActive(value) {
this.loading = true;
return service
.updatePrometheusActive({
endpoint: this.prometheus.prometheusFormPath,
params: {
token: this.$options.csrf.token,
config: value ? 1 : 0,
url: this.prometheusApiKey,
redirect: window.location,
},
})
.then(() => {
this.activated.prometheus = value;
if (value) {
this.setFeedback({
feedbackMessage: this.$options.i18n.endPointActivated,
variant: 'success',
});
}
})
.catch(() => {
this.setFeedback({
feedbackMessage: this.$options.i18n.errorApiUrlMsg,
variant: 'danger',
});
})
.finally(() => {
this.loading = false;
});
},
setFeedback({ feedbackMessage, variant }) {
this.feedback = { feedbackMessage, variant };
},
onSubmit(evt) {
// TODO: Add form submit as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356
evt.preventDefault();
},
onReset(evt) {
// TODO: Add form reset as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356
evt.preventDefault();
},
},
};
</script>
<template>
<div>
<gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback">
{{ feedback.feedbackMessage }}
</gl-alert>
<div data-testid="alert-settings-description" class="gl-mt-5">
<p v-for="section in sections" :key="section.text">
<gl-sprintf :message="section.text">
<template #link="{ content }">
<gl-link :href="section.url" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<gl-form @submit="onSubmit" @reset="onReset">
<gl-form-group
v-if="glFeatures.alertIntegrationsDropdown"
:label="$options.i18n.integrationsLabel"
label-for="integrations"
label-class="label-bold"
>
<gl-form-select
v-model="selectedEndpoint"
:options="options"
data-testid="alert-settings-select"
/>
<span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.integrationsInfo">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
href="https://gitlab.com/groups/gitlab-org/-/epics/3362"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</gl-form-group>
<gl-form-group
:label="$options.i18n.activeLabel"
label-for="activated"
label-class="label-bold"
>
<toggle-button
id="activated"
:disabled-input="loading"
:is-loading="loading"
:value="selectedService.active"
@change="toggleActivated"
/>
</gl-form-group>
<gl-form-group
v-if="prometheusFeatureEnabled"
:label="$options.i18n.apiBaseUrlLabel"
label-for="api-url"
label-class="label-bold"
>
<gl-form-input
id="api-url"
v-model="prometheusApiKey"
type="url"
:value="prometheusApiKey"
:placeholder="$options.i18n.prometheusApiPlaceholder"
/>
<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">
<div class="input-group">
<gl-form-input id="url" :readonly="true" :value="selectedService.url" />
<span class="input-group-append">
<clipboard-button :text="selectedService.url" :title="$options.i18n.copyToClipboard" />
</span>
</div>
<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"
>
<div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="selectedService.authKey" />
<span class="input-group-append">
<clipboard-button
:text="selectedService.authKey"
:title="$options.i18n.copyToClipboard"
/>
</span>
</div>
<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"
>
{{ $options.i18n.restKeyInfo }}
</gl-modal>
</gl-form-group>
<div
class="footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none"
>
<gl-button type="submit" variant="success" category="primary">
{{ __('Save and test changes') }}
</gl-button>
<gl-button type="reset" variant="default" category="primary">
{{ __('Cancel') }}
</gl-button>
</div>
</gl-form>
</div>
</template>
import { s__ } from '~/locale';
export const i18n = {
usageSection: s__(
'AlertSettings|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.',
),
setupSection: s__(
"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.",
),
errorMsg: s__(
'AlertSettings|There was an error updating the the alert settings. Please refresh the page to try again.',
),
errorKeyMsg: s__(
'AlertSettings|There was an error while trying to reset the key. Please refresh the page to try again.',
),
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.',
),
endPointActivated: s__('AlertSettings|Alerts endpoint successfully activated.'),
prometheusInfo: s__('AlertSettings|Add URL and auth key to your Prometheus config file'),
integrationsInfo: s__(
'AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}',
),
resetKey: s__('AlertSettings|Reset key'),
copyToClipboard: s__('AlertSettings|Copy'),
integrationsLabel: s__('AlertSettings|Integrations'),
apiBaseUrlLabel: s__('AlertSettings|Prometheus API Base URL'),
authKeyLabel: s__('AlertSettings|Authorization key'),
urlLabel: s__('AlertSettings|Webhook URL'),
activeLabel: s__('AlertSettings|Active'),
apiBaseUrlHelpText: s__(' AlertSettings|URL cannot be blank and must start with http or https'),
};
export const serviceOptions = [
{ value: 'generic', text: s__('AlertSettings|Generic') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
];
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertSettingsForm from './components/alerts_settings_form.vue';
export default el => {
if (!el) {
return null;
}
const {
prometheusActivated,
prometheusUrl,
prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
activated: activatedStr,
alertsSetupUrl,
alertsUsageUrl,
formPath,
authorizationKey,
url,
} = el.dataset;
const activated = parseBoolean(activatedStr);
const prometheusIsActivated = parseBoolean(prometheusActivated);
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,
},
},
});
},
});
};
import axios from '~/lib/utils/axios_utils';
export default {
updateGenericKey({ endpoint, params }) {
return axios.put(endpoint, params);
},
updatePrometheusKey({ endpoint }) {
return axios.post(endpoint);
},
updateGenericActive({ endpoint, params }) {
return axios.put(endpoint, params);
},
updatePrometheusActive({ endpoint, params: { token, config, url, redirect } }) {
const data = new FormData();
data.set('_method', 'put');
data.set('authenticity_token', token);
data.set('service[manual_configuration]', config);
data.set('service[api_url]', url);
data.set('redirect_to', redirect);
return axios.post(endpoint, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
},
};
import mountErrorTrackingForm from '~/error_tracking_settings'; import mountErrorTrackingForm from '~/error_tracking_settings';
import initAlertsSettings from '~/alerts_service_settings'; import mountAlertsSettings from '~/alerts_settings';
import mountOperationSettings from '~/operation_settings'; import mountOperationSettings from '~/operation_settings';
import mountGrafanaIntegration from '~/grafana_integration'; import mountGrafanaIntegration from '~/grafana_integration';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
...@@ -11,5 +11,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -11,5 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
if (!IS_EE) { if (!IS_EE) {
initSettingsPanels(); initSettingsPanels();
} }
initAlertsSettings(document.querySelector('.js-alerts-service-settings')); mountAlertsSettings(document.querySelector('.js-alerts-settings'));
}); });
...@@ -41,6 +41,11 @@ export default { ...@@ -41,6 +41,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
disabled: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -88,7 +93,11 @@ export default { ...@@ -88,7 +93,11 @@ export default {
<div class="input-group"> <div class="input-group">
<gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" /> <gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" />
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button :text="notifyUrl" :title="$options.copyToClipboard" /> <clipboard-button
:text="notifyUrl"
:title="$options.copyToClipboard"
:disabled="disabled"
/>
</span> </span>
</div> </div>
</gl-form-group> </gl-form-group>
...@@ -100,7 +109,11 @@ export default { ...@@ -100,7 +109,11 @@ export default {
<div class="input-group"> <div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" /> <gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button :text="authorizationKey" :title="$options.copyToClipboard" /> <clipboard-button
:text="authorizationKey"
:title="$options.copyToClipboard"
:disabled="disabled"
/>
</span> </span>
</div> </div>
</gl-form-group> </gl-form-group>
...@@ -118,13 +131,20 @@ export default { ...@@ -118,13 +131,20 @@ export default {
) )
}} }}
</gl-modal> </gl-modal>
<gl-deprecated-button v-gl-modal.authKeyModal class="js-reset-auth-key">{{ <gl-deprecated-button
__('Reset key') v-gl-modal.authKeyModal
}}</gl-deprecated-button> class="js-reset-auth-key"
:disabled="disabled"
>{{ __('Reset key') }}</gl-deprecated-button
>
</template> </template>
<gl-deprecated-button v-else class="js-reset-auth-key" @click="resetKey">{{ <gl-deprecated-button
__('Generate key') v-else
}}</gl-deprecated-button> :disabled="disabled"
class="js-reset-auth-key"
@click="resetKey"
>{{ __('Generate key') }}</gl-deprecated-button
>
</div> </div>
</div> </div>
</template> </template>
...@@ -8,7 +8,7 @@ export default () => { ...@@ -8,7 +8,7 @@ export default () => {
return; return;
} }
const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl } = el.dataset; const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl, disabled } = el.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
...@@ -20,6 +20,7 @@ export default () => { ...@@ -20,6 +20,7 @@ export default () => {
changeKeyUrl, changeKeyUrl,
notifyUrl, notifyUrl,
learnMoreUrl, learnMoreUrl,
disabled,
}, },
}); });
}, },
......
...@@ -5,15 +5,14 @@ module Projects ...@@ -5,15 +5,14 @@ module Projects
class OperationsController < Projects::ApplicationController class OperationsController < Projects::ApplicationController
before_action :authorize_admin_operations! before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token] before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
before_action do
push_frontend_feature_flag(:alert_integrations_dropdown, project)
end
respond_to :json, only: [:reset_alerting_token] respond_to :json, only: [:reset_alerting_token]
helper_method :error_tracking_setting helper_method :error_tracking_setting
def show
render locals: { prometheus_service: prometheus_service, alerts_service: alerts_service }
end
def update def update
result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute
...@@ -48,14 +47,6 @@ module Projects ...@@ -48,14 +47,6 @@ module Projects
{ alerting_setting_attributes: { regenerate_token: true } } { alerting_setting_attributes: { regenerate_token: true } }
end end
def prometheus_service
project.find_or_initialize_service(::PrometheusService.to_param)
end
def alerts_service
project.find_or_initialize_service(::AlertsService.to_param)
end
def render_update_response(result) def render_update_response(result)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
# frozen_string_literal: true
module OperationsHelper
include Gitlab::Utils::StrongMemoize
def prometheus_service
strong_memoize(:prometheus_service) do
@project.find_or_initialize_service(::PrometheusService.to_param)
end
end
def alerts_service
strong_memoize(:alerts_service) do
@project.find_or_initialize_service(::AlertsService.to_param)
end
end
def alerts_settings_data
{
'prometheus_activated' => prometheus_service.activated?.to_s,
'activated' => alerts_service.activated?.to_s,
'prometheus_form_path' => scoped_integration_path(prometheus_service),
'form_path' => scoped_integration_path(alerts_service),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(@project),
'prometheus_authorization_key' => @project.alerting_setting&.token,
'prometheus_api_url' => prometheus_service.api_url,
'authorization_key' => alerts_service.token,
'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json),
'url' => alerts_service.url,
'alerts_setup_url' => help_page_path('user/project/integrations/generic_alerts.html', anchor: 'setting-up-generic-alerts'),
'alerts_usage_url' => project_alert_management_index_path(@project)
}
end
end
OperationsHelper.prepend_if_ee('EE::OperationsHelper')
...@@ -48,8 +48,8 @@ module ServicesHelper ...@@ -48,8 +48,8 @@ module ServicesHelper
end end
end end
def service_save_button def service_save_button(disabled: false)
button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do button_tag(class: 'btn btn-success', type: 'submit', disabled: disabled, data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') + icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label') content_tag(:span, 'Save changes', class: 'js-btn-label')
end end
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= render 'shared/service_settings', form: form, integration: @service = render 'shared/service_settings', form: form, integration: @service
.footer-block.row-content-block{ :class => "#{'gl-display-none' if @service.is_a?(AlertsService)}" } .footer-block.row-content-block{ :class => "#{'gl-display-none' if @service.is_a?(AlertsService)}" }
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer } %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= service_save_button = service_save_button(disabled: @service.is_a?(AlertsService))
&nbsp; &nbsp;
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel' = link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
......
- return unless show_alerts_moved_alert?
.row .row
.col-lg-12 .col-lg-12
.gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert', data: { feature_id: UserCalloutsHelper::ALERTS_MOVED, dismiss_endpoint: user_callouts_path } } .gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
.gl-alert-body .gl-alert-body
= _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.') = _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.')
.gl-alert-actions .gl-alert-actions
......
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
- authorization_key = @project.alerting_setting.try(:token) - authorization_key = @project.alerting_setting.try(:token)
- learn_more_url = help_page_path('user/project/integrations/prometheus', anchor: 'external-prometheus-instances') - learn_more_url = help_page_path('user/project/integrations/prometheus', anchor: 'external-prometheus-instances')
#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url } } #js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: Feature.enabled?(:alert_integrations_dropdown, @service.project) && @service.manual_configuration? } }
- return unless Feature.enabled?(:alert_integrations_dropdown, @service.project) && @service.manual_configuration?
.row
.col-lg-12
.gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
.gl-alert-actions
= link_to _('Visit settings page'), project_settings_operations_path(@project), class: 'btn gl-alert-action btn-info gl-button'
...@@ -10,9 +10,4 @@ ...@@ -10,9 +10,4 @@
= _('Display alerts from all your monitoring tools directly within GitLab.') = _('Display alerts from all your monitoring tools directly within GitLab.')
= link_to _('More information'), help_page_path('user/project/operations/alert_management'), target: '_blank', rel: 'noopener noreferrer' = link_to _('More information'), help_page_path('user/project/operations/alert_management'), target: '_blank', rel: 'noopener noreferrer'
.settings-content .settings-content
.js-alerts-service-settings{ data: { activated: service.activated?.to_s, .js-alerts-settings{ data: alerts_settings_data }
form_path: scoped_integration_path(service),
authorization_key: service.token,
url: service.url || _('<namespace / project>'),
alerts_setup_url: help_page_path('user/project/integrations/generic_alerts.html', anchor: 'setting-up-generic-alerts'),
alerts_usage_url: project_alert_management_index_path(@project) } }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- page_title _('Operations Settings') - page_title _('Operations Settings')
- breadcrumb_title _('Operations Settings') - breadcrumb_title _('Operations Settings')
= render 'projects/settings/operations/alert_management', service: alerts_service = render 'projects/settings/operations/alert_management', alerts_service: alerts_service, prometheus_service: prometheus_service
= render 'projects/settings/operations/incidents' = render 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking' = render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/prometheus', service: prometheus_service if Feature.enabled?(:settings_operations_prometheus_service) = render 'projects/settings/operations/prometheus', service: prometheus_service if Feature.enabled?(:settings_operations_prometheus_service)
......
# frozen_string_literal: true
module EE
module OperationsHelper
def operations_data
{
'add-path' => add_operations_project_path,
'list-path' => operations_list_path,
'empty-dashboard-svg-path' => image_path('illustrations/operations-dashboard_empty.svg'),
'empty-dashboard-help-path' => help_page_path('user/operations_dashboard/index.md')
}
end
def environments_data
{
'add-path' => add_operations_project_path,
'list-path' => operations_environments_list_path,
'empty-dashboard-svg-path' => image_path('illustrations/operations-dashboard_empty.svg'),
'empty-dashboard-help-path' => help_page_path('ci/environments/environments_dashboard.md'),
'environments-dashboard-help-path' => help_page_path('ci/environments/environments_dashboard.md')
}
end
def status_page_settings_data
{
'operations-settings-endpoint' => project_settings_operations_path(@project),
'enabled' => status_page_setting.enabled?.to_s,
'url' => status_page_setting&.status_page_url,
'bucket-name' => status_page_setting.aws_s3_bucket_name,
'region' => status_page_setting.aws_region,
'aws-access-key' => status_page_setting.aws_access_key,
'aws-secret-key' => status_page_setting.masked_aws_secret_key
}
end
end
end
# frozen_string_literal: true
module OperationsHelper
def operations_data
{
'add-path' => add_operations_project_path,
'list-path' => operations_list_path,
'empty-dashboard-svg-path' => image_path('illustrations/operations-dashboard_empty.svg'),
'empty-dashboard-help-path' => help_page_path('user/operations_dashboard/index.md')
}
end
def environments_data
{
'add-path' => add_operations_project_path,
'list-path' => operations_environments_list_path,
'empty-dashboard-svg-path' => image_path('illustrations/operations-dashboard_empty.svg'),
'empty-dashboard-help-path' => help_page_path('ci/environments/environments_dashboard.md'),
'environments-dashboard-help-path' => help_page_path('ci/environments/environments_dashboard.md')
}
end
def status_page_settings_data
{
'operations-settings-endpoint' => project_settings_operations_path(@project),
'enabled' => status_page_setting.enabled?.to_s,
'url' => status_page_setting&.status_page_url,
'bucket-name' => status_page_setting.aws_s3_bucket_name,
'region' => status_page_setting.aws_region,
'aws-access-key' => status_page_setting.aws_access_key,
'aws-secret-key' => status_page_setting.masked_aws_secret_key
}
end
end
...@@ -16,6 +16,9 @@ msgstr "" ...@@ -16,6 +16,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid " AlertSettings|URL cannot be blank and must start with http or https"
msgstr ""
msgid " %{start} to %{end}" msgid " %{start} to %{end}"
msgstr "" msgstr ""
...@@ -2059,6 +2062,66 @@ msgstr "" ...@@ -2059,6 +2062,66 @@ 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." 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 "" msgstr ""
msgid "AlertSettings|Active"
msgstr ""
msgid "AlertSettings|Add URL and auth key to your Prometheus config file"
msgstr ""
msgid "AlertSettings|Alerts endpoint successfully activated."
msgstr ""
msgid "AlertSettings|Authorization key"
msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
msgid "AlertSettings|External Prometheus"
msgstr ""
msgid "AlertSettings|Generic"
msgstr ""
msgid "AlertSettings|Integrations"
msgstr ""
msgid "AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}"
msgstr ""
msgid "AlertSettings|Prometheus API Base URL"
msgstr ""
msgid "AlertSettings|Reset key"
msgstr ""
msgid "AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in."
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|There was an error updating the the alert settings. Please refresh the page to try again."
msgstr ""
msgid "AlertSettings|There was an error while trying to enable the alert settings. Please ensure you are using a valid URL."
msgstr ""
msgid "AlertSettings|There was an error while trying to reset the key. Please refresh the page to try again."
msgstr ""
msgid "AlertSettings|Webhook URL"
msgstr ""
msgid "AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated."
msgstr ""
msgid "AlertSettings|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|http://prometheus.example.com/"
msgstr ""
msgid "Alerts" msgid "Alerts"
msgstr "" msgstr ""
...@@ -19717,6 +19780,9 @@ msgstr "" ...@@ -19717,6 +19780,9 @@ msgstr ""
msgid "Save Changes" msgid "Save Changes"
msgstr "" msgstr ""
msgid "Save and test changes"
msgstr ""
msgid "Save anyway" msgid "Save anyway"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm prometheus is active renders a valid "select" 1`] = `"<gl-form-select-stub options=\\"[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"prometheus\\"></gl-form-select-stub>"`;
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
"<div>
<!---->
<div data-testid=\\"alert-settings-description\\" class=\\"gl-mt-5\\">
<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-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\\">
<div class=\\"input-group\\">
<gl-form-input-stub id=\\"url\\" readonly=\\"true\\" value=\\"/alerts/notify.json\\"></gl-form-input-stub> <span class=\\"input-group-append\\"><clipboard-button-stub text=\\"/alerts/notify.json\\" title=\\"Copy\\" tooltipplacement=\\"top\\" cssclass=\\"btn-default\\"></clipboard-button-stub></span>
</div> <span class=\\"gl-text-gray-400\\">
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<div class=\\"input-group\\">
<gl-form-input-stub id=\\"authorization-key\\" readonly=\\"true\\" value=\\"abcedfg123\\"></gl-form-input-stub> <span class=\\"input-group-append\\"><clipboard-button-stub text=\\"abcedfg123\\" title=\\"Copy\\" tooltipplacement=\\"top\\" cssclass=\\"btn-default\\"></clipboard-button-stub></span>
</div>
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" 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>
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none\\">
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" type=\\"submit\\">
Save and test changes
</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" type=\\"reset\\">
Cancel
</gl-button-stub>
</div>
</gl-form-stub>
</div>"
`;
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlAlert } from '@gitlab/ui';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
const PROMETHEUS_URL = '/prometheus/alerts/notify.json';
const GENERIC_URL = '/alerts/notify.json';
const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid';
const ACTIVATED = false;
const defaultProps = {
generic: {
initialAuthorizationKey: KEY,
formPath: INVALID_URL,
url: GENERIC_URL,
alertsSetupUrl: INVALID_URL,
alertsUsageUrl: INVALID_URL,
initialActivated: ACTIVATED,
},
prometheus: {
prometheusAuthorizationKey: KEY,
prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL,
prometheusIsActivated: ACTIVATED,
},
};
describe('AlertsSettingsForm', () => {
let wrapper;
let mockAxios;
const createComponent = (
props = defaultProps,
{ methods } = {},
alertIntegrationsDropdown = false,
) => {
wrapper = shallowMount(AlertsSettingsForm, {
propsData: {
...defaultProps,
...props,
},
methods,
provide: {
glFeatures: {
alertIntegrationsDropdown,
},
},
});
};
const findSelect = () => wrapper.find('[data-testid="alert-settings-select"]');
const findUrl = () => wrapper.find('#url');
const findAuthorizationKey = () => wrapper.find('#authorization-key');
const findApiUrl = () => wrapper.find('#api-url');
beforeEach(() => {
mockAxios = new MockAdapter(axios);
setFixtures(`
<div>
<span class="js-service-active-status fa fa-circle" data-value="true"></span>
<span class="js-service-active-status fa fa-power-off" data-value="false"></span>
</div>`);
});
afterEach(() => {
wrapper.destroy();
mockAxios.restore();
});
describe('with default values', () => {
beforeEach(() => {
createComponent();
});
it('renders the initial template', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('reset key', () => {
it('triggers resetKey method', () => {
const resetGenericKey = jest.fn();
const methods = { resetGenericKey };
createComponent(defaultProps, { methods });
wrapper.find(GlModal).vm.$emit('ok');
expect(resetGenericKey).toHaveBeenCalled();
});
it('updates the authorization key on success', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' });
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.resetGenericKey().then(() => {
expect(findAuthorizationKey().attributes('value')).toBe('newToken');
});
});
it('shows a alert message on error', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(404);
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.resetGenericKey().then(() => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
});
});
});
describe('activate toggle', () => {
it('triggers toggleActivated method', () => {
const toggleActivated = jest.fn();
const methods = { toggleActivated };
createComponent(defaultProps, { methods });
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(toggleActivated).toHaveBeenCalled();
});
describe('error is encountered', () => {
beforeEach(() => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(500);
});
it('restores previous value', () => {
createComponent({ generic: { ...defaultProps.generic, initialActivated: false } });
return wrapper.vm.resetGenericKey().then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(false);
});
});
});
});
describe('prometheus is active', () => {
beforeEach(() => {
createComponent(
{ prometheus: { ...defaultProps.prometheus, prometheusIsActivated: true } },
{},
true,
);
});
it('renders a valid "select"', () => {
expect(findSelect().html()).toMatchSnapshot();
});
it('shows the API URL input', () => {
expect(findApiUrl().exists()).toBe(true);
});
it('show a valid Alert URL', () => {
expect(findUrl().exists()).toBe(true);
expect(findUrl().attributes('value')).toBe(PROMETHEUS_URL);
});
it('should not show a footer block', () => {
expect(wrapper.find('.footer-block').classes('d-none')).toBe(true);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe OperationsHelper do
include Gitlab::Routing
describe '#alerts_settings_data' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project) }
subject { helper.alerts_settings_data }
before do
helper.instance_variable_set(:@project, project)
allow(helper).to receive(:current_user) { user }
allow(helper).to receive(:can?).with(user, :admin_operations, project) { true }
end
context 'initial service configuration' do
let_it_be(:alerts_service) { AlertsService.new(project: project) }
let_it_be(:prometheus_service) { PrometheusService.new(project: project) }
before do
allow(project).to receive(:find_or_initialize_service).with('alerts').and_return(alerts_service)
allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return(prometheus_service)
end
it 'returns the correct values' do
expect(subject).to eq(
'activated' => 'false',
'url' => alerts_service.url,
'authorization_key' => nil,
'form_path' => project_service_path(project, alerts_service),
'alerts_setup_url' => help_page_path('user/project/integrations/generic_alerts.html', anchor: 'setting-up-generic-alerts'),
'alerts_usage_url' => project_alert_management_index_path(project),
'prometheus_form_path' => project_service_path(project, prometheus_service),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project),
'prometheus_authorization_key' => nil,
'prometheus_api_url' => nil,
'prometheus_activated' => 'false',
'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json)
)
end
end
context 'with external Prometheus configured' do
let_it_be(:prometheus_service, reload: true) { create(:prometheus_service, project: project) }
context 'with external Prometheus enabled' do
it 'returns the correct values' do
expect(subject).to include(
'prometheus_activated' => 'true',
'prometheus_api_url' => prometheus_service.api_url
)
end
end
context 'with external Prometheus disabled' do
before do
# Prometheus services uses manual_configuration as an alias for active, beware
prometheus_service.update!(manual_configuration: false)
end
it 'returns the correct values' do
expect(subject).to include(
'prometheus_activated' => 'false',
'prometheus_api_url' => prometheus_service.api_url
)
end
end
context 'with project alert setting' do
let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
it 'returns the correct values' do
expect(subject).to include(
'prometheus_authorization_key' => project_alerting_setting.token,
'prometheus_api_url' => prometheus_service.api_url
)
end
end
end
context 'with generic alerts service configured' do
let_it_be(:alerts_service) { create(:alerts_service, project: project) }
context 'with generic alerts enabled' do
it 'returns the correct values' do
expect(subject).to include(
'activated' => 'true',
'authorization_key' => alerts_service.token,
'url' => alerts_service.url
)
end
end
context 'with generic alerts disabled' do
before do
alerts_service.update!(active: false)
end
it 'returns the correct values' do
expect(subject).to include(
'activated' => 'false',
'authorization_key' => alerts_service.token
)
end
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