Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-29 00:08:36 +00:00
parent 669f67690a
commit 1063cd719c
49 changed files with 1962 additions and 330 deletions

View file

@ -1 +1 @@
dfdc9b7725eb710dab8ae9970e98cc5118e65c49
092d4e489d7de1dcc38d57a4c667e85df8b8377f

View file

@ -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);
},
},

View file

@ -0,0 +1,119 @@
<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>

View file

@ -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>

View file

@ -0,0 +1,48 @@
<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>

View file

@ -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',
};

View file

@ -1,6 +1,6 @@
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');
},
});
};

View file

@ -1,8 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { GlLoadingIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants';
@ -17,7 +16,7 @@ import { showLearnGitLabGroupItemPopover } from '~/onboarding_issues';
export default {
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
components: {
GlBadge,
@ -127,11 +126,10 @@ export default {
<div class="group-text flex-grow-1 flex-shrink-1">
<div class="d-flex align-items-center flex-wrap title namespace-title gl-mr-3">
<a
v-tooltip
v-gl-tooltip.bottom
:href="group.relativePath"
:title="group.fullName"
class="no-expand gl-mt-3 gl-mr-3 gl-text-gray-900!"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending

View file

@ -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

View file

@ -0,0 +1,29 @@
# 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

View file

@ -0,0 +1,22 @@
# 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

View file

@ -0,0 +1,58 @@
# 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

View file

@ -0,0 +1,13 @@
# 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

View file

@ -0,0 +1,38 @@
# 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

View file

@ -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,

View file

@ -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

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module AlertManagement
class HttpIntegrationPolicy < ::BasePolicy
delegate { @subject.project }
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class PrometheusServicePolicy < ::BasePolicy
delegate { @subject.project }
end

View file

@ -0,0 +1,5 @@
---
title: Disallow some project routes in robots.txt
merge_request: 46218
author:
type: changed

View file

@ -0,0 +1,7 @@
---
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

View file

@ -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

View file

@ -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",

View file

@ -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.

View file

@ -136,14 +136,26 @@ curl --data "name=foo" --header "PRIVATE-TOKEN: <your_access_token>" "https://gi
### Post data using JSON content
NOTE: **Note:**
In this example we create a new group. Watch carefully the single and double
quotes.
This example creates a new group. Be aware of the use of single (`'`) and double
(`"`) quotes.
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' "https://gitlab.example.com/api/v4/groups"
```
For readability, you can also set up the `--data` by using the following format:
```shell
curl --request POST \
--url "https://gitlab.example.com/api/v4/groups" \
--header "content-type: application/json" \
--header "PRIVATE-TOKEN: <your_access_token>" \
--data '{
"path": "my-group",
"name": "My group"
}'
```
### Post data using form-data
Instead of using JSON or urlencode you can use multipart/form-data which

View file

@ -248,6 +248,11 @@ It is recommended to use the `debug` level for verbose logging that could be
useful when debugging. The default value for `SECURE_LOG_LEVEL` should be set
to `info`.
When executing command lines, scanners should use the `debug` level to log the command line and its output.
For instance, the [bundler-audit](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit) scanner
uses the `debug` level to log the command line `bundle audit check --quiet`,
and what `bundle audit` writes to the standard output.
#### common logutil package
If you are using [go](https://golang.org/) and

View file

@ -4,7 +4,7 @@ group: Health
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Monitor your CI/CD environment's metrics **(CORE)**
# Monitor your environment's metrics **(CORE)**
GitLab helps your team monitor the health and performance of your applications
and infrastructure by turning statistics and log files into charts and graphs

View file

@ -714,10 +714,6 @@ To delete a scanner profile:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218465) in GitLab 13.2.
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/218465) in GitLab 13.3.
> - It's deployed behind a feature flag, enabled by default.
> - It's enabled on GitLab.com.
> - It's able to be enabled or disabled per-project.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-on-demand-scans).
An on-demand DAST scan runs outside the DevOps life cycle. Changes in your repository don't trigger
the scan. You must start it manually.
@ -748,35 +744,6 @@ To run an on-demand DAST scan, you need:
The on-demand DAST scan runs and the project's dashboard shows the results.
### Enable or disable On-demand Scans
The On-demand DAST Scans feature is enabled by default. You can disable on-demand scans
instance-wide, or disable it for specific projects if you prefer.
To run on-demand DAST scans, an administrator must enable the
`security_on_demand_scans_feature_flag` feature flag.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can disable or enable the feature flags.
To disable On-demand DAST Scans:
```ruby
# Instance-wide
Feature.disable(:security_on_demand_scans_feature_flag)
# or by project
Feature.disable(:security_on_demand_scans_feature_flag, Project.find(<project id>))
```
To enable On-demand DAST Scans:
```ruby
# Instance-wide
Feature.enable(:security_on_demand_scans_feature_flag)
# or by project
Feature.enable(:security_on_demand_scans_feature_flag, Project.find(<project ID>))
```
## Reports
The DAST tool outputs a report file in JSON format by default. However, this tool can also generate reports in

View file

@ -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 ""

View file

@ -67,3 +67,16 @@ Disallow: /*/protected_branches
Disallow: /*/uploads/
Disallow: /*/project_members
Disallow: /*/settings
Disallow: /*/-/import
Disallow: /*/-/environments
Disallow: /*/-/jobs
Disallow: /*/-/requirements_management
Disallow: /*/-/pipelines
Disallow: /*/-/pipeline_schedules
Disallow: /*/-/dependencies
Disallow: /*/-/licenses
Disallow: /*/-/metrics
Disallow: /*/-/incidents
Disallow: /*/-/value_stream_analytics
Disallow: /*/-/analytics
Disallow: /*/insights

View file

@ -1,5 +1,11 @@
# frozen_string_literal: true
# When running in CI environment, we need to load a full `spec_helper`
if ENV['CI']
require_relative 'spec_helper'
return
end
require 'bundler/setup'
ENV['GITLAB_ENV'] = 'test'

View file

@ -0,0 +1,64 @@
# 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

View file

@ -1,50 +0,0 @@
// 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>"
`;

View file

@ -0,0 +1,20 @@
// 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>"
`;

View file

@ -0,0 +1,47 @@
// 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>"
`;

View file

@ -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);
});
});

View file

@ -0,0 +1,59 @@
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);
});
});
});

View file

@ -1,19 +1,13 @@
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',
},
);
});

View file

@ -0,0 +1,48 @@
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);
});
});
});

View file

@ -0,0 +1,28 @@
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,
},
};

View file

@ -0,0 +1,42 @@
# 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

View file

@ -0,0 +1,9 @@
# 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

View file

@ -0,0 +1,22 @@
# 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

View file

@ -0,0 +1,21 @@
# 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

View file

@ -0,0 +1,60 @@
# 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

View file

@ -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)

View file

@ -0,0 +1,23 @@
# 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

View file

@ -0,0 +1,23 @@
# 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

View file

@ -0,0 +1,83 @@
# 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

View file

@ -66,7 +66,20 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
'/foo/bar/uploads/foo',
'/foo/bar/project_members',
'/foo/bar/settings',
'/namespace/subnamespace/design.gitlab.com/settings'
'/namespace/subnamespace/design.gitlab.com/settings',
'/foo/bar/-/import',
'/foo/bar/-/environments',
'/foo/bar/-/jobs',
'/foo/bar/-/requirements_management',
'/foo/bar/-/pipelines',
'/foo/bar/-/pipeline_schedules',
'/foo/bar/-/dependencies',
'/foo/bar/-/licenses',
'/foo/bar/-/metrics',
'/foo/bar/-/incidents',
'/foo/bar/-/value_stream_analytics',
'/foo/bar/-/analytics',
'/foo/bar/insights'
]
requests.each do |request|