Commit 2e02846d authored by Savas Vedova's avatar Savas Vedova

Merge branch '300827-split-alert-integrations-form' into 'master'

UX cleanup: Adding an alert integration - split form into tabs

See merge request gitlab-org/gitlab!54842
parents 62a9ffe2 bbec990d
<script> <script>
import { import {
GlButton, GlButton,
GlCollapse,
GlForm, GlForm,
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
...@@ -11,10 +10,11 @@ import { ...@@ -11,10 +10,11 @@ import {
GlModal, GlModal,
GlModalDirective, GlModalDirective,
GlToggle, GlToggle,
GlTabs,
GlTab,
} from '@gitlab/ui'; } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { isEmpty, omit } from 'lodash'; import { isEmpty, omit } from 'lodash';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { import {
...@@ -22,74 +22,13 @@ import { ...@@ -22,74 +22,13 @@ import {
JSON_VALIDATE_DELAY, JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder, targetPrometheusUrlPlaceholder,
typeSet, typeSet,
i18n,
} from '../constants'; } from '../constants';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql'; import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql';
import MappingBuilder from './alert_mapping_builder.vue'; import MappingBuilder from './alert_mapping_builder.vue';
import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue'; import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
export const i18n = {
integrationFormSteps: {
step1: {
label: s__('AlertSettings|1. Select integration type'),
enterprise: s__(
'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.',
),
},
step2: {
label: s__('AlertSettings|2. Name integration'),
placeholder: s__('AlertSettings|Enter integration name'),
prometheus: s__('AlertSettings|Prometheus'),
},
step3: {
label: s__('AlertSettings|3. Set up webhook'),
help: s__(
"AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.",
),
prometheusHelp: s__(
'AlertSettings|Utilize the URL and authorization key below to authorize Prometheus to send alerts to GitLab. Review the Prometheus documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.',
),
info: s__('AlertSettings|Authorization key'),
reset: s__('AlertSettings|Reset Key'),
},
step4: {
label: s__('AlertSettings|4. Sample alert payload (optional)'),
help: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional), or to test the integration (also optional).',
),
prometheusHelp: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).',
),
placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
resetHeader: s__('AlertSettings|Reset the mapping'),
resetBody: s__(
"AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields.",
),
resetOk: s__('AlertSettings|Proceed with editing'),
editPayload: s__('AlertSettings|Edit payload'),
submitPayload: s__('AlertSettings|Submit payload'),
payloadParsedSucessMsg: s__(
'AlertSettings|Sample payload has been parsed. You can now map the fields.',
),
},
step5: {
label: s__('AlertSettings|5. Map fields (optional)'),
intro: s__(
"AlertSettings|If you've provided a sample alert payload, you can create a custom mapping for your endpoint. The default GitLab alert keys are listed below. Please define which payload key should map to the specified GitLab key.",
),
},
prometheusFormUrl: {
label: s__('AlertSettings|Prometheus API base URL'),
help: s__('AlertSettings|URL cannot be blank and must start with http or https'),
},
restKeyInfo: {
label: s__(
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
},
},
};
export default { export default {
placeholders: { placeholders: {
prometheus: targetPrometheusUrlPlaceholder, prometheus: targetPrometheusUrlPlaceholder,
...@@ -100,7 +39,6 @@ export default { ...@@ -100,7 +39,6 @@ export default {
components: { components: {
ClipboardButton, ClipboardButton,
GlButton, GlButton,
GlCollapse,
GlForm, GlForm,
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
...@@ -109,6 +47,8 @@ export default { ...@@ -109,6 +47,8 @@ export default {
GlFormSelect, GlFormSelect,
GlModal, GlModal,
GlToggle, GlToggle,
GlTabs,
GlTab,
AlertSettingsFormHelpBlock, AlertSettingsFormHelpBlock,
MappingBuilder, MappingBuilder,
}, },
...@@ -155,8 +95,11 @@ export default { ...@@ -155,8 +95,11 @@ export default {
integrationTypesOptions: Object.values(integrationTypes), integrationTypesOptions: Object.values(integrationTypes),
selectedIntegration: integrationTypes.none.value, selectedIntegration: integrationTypes.none.value,
active: false, active: false,
formVisible: false, samplePayload: {
integrationTestPayload: { json: null,
error: null,
},
testPayload: {
json: null, json: null,
error: null, error: null,
}, },
...@@ -171,8 +114,17 @@ export default { ...@@ -171,8 +114,17 @@ export default {
isPrometheus() { isPrometheus() {
return this.selectedIntegration === this.$options.typeSet.prometheus; return this.selectedIntegration === this.$options.typeSet.prometheus;
}, },
jsonIsValid() { isHttp() {
return this.integrationTestPayload.error === null; return this.selectedIntegration === this.$options.typeSet.http;
},
isCreating() {
return !this.currentIntegration;
},
isSampePayloadValid() {
return this.samplePayload.error === null;
},
isTestPayloadValid() {
return this.testPayload.error === null;
}, },
selectedIntegrationType() { selectedIntegrationType() {
switch (this.selectedIntegration) { switch (this.selectedIntegration) {
...@@ -199,7 +151,7 @@ export default { ...@@ -199,7 +151,7 @@ export default {
}, },
testAlertPayload() { testAlertPayload() {
return { return {
data: this.integrationTestPayload.json, data: this.testPayload.json,
endpoint: this.integrationForm.url, endpoint: this.integrationForm.url,
token: this.integrationForm.token, token: this.integrationForm.token,
}; };
...@@ -208,7 +160,7 @@ export default { ...@@ -208,7 +160,7 @@ export default {
return ( return (
this.multiIntegrations && this.multiIntegrations &&
this.glFeatures.multipleHttpIntegrationsCustomMapping && this.glFeatures.multipleHttpIntegrationsCustomMapping &&
this.selectedIntegration === typeSet.http && this.isHttp &&
this.alertFields?.length this.alertFields?.length
); );
}, },
...@@ -227,18 +179,11 @@ export default { ...@@ -227,18 +179,11 @@ export default {
: !this.active; : !this.active;
}, },
isSubmitTestPayloadDisabled() { isSubmitTestPayloadDisabled() {
return ( return !this.active || Boolean(this.samplePayload.error) || this.samplePayload.json === '';
!this.active ||
Boolean(this.integrationTestPayload.error) ||
this.integrationTestPayload.json === ''
);
}, },
isSelectDisabled() { isSelectDisabled() {
return this.currentIntegration !== null || !this.canAddIntegration; return this.currentIntegration !== null || !this.canAddIntegration;
}, },
savedMapping() {
return this.mapping;
},
}, },
watch: { watch: {
currentIntegration(val) { currentIntegration(val) {
...@@ -250,21 +195,14 @@ export default { ...@@ -250,21 +195,14 @@ export default {
this.selectedIntegration = type; this.selectedIntegration = type;
this.active = active; this.active = active;
if (type === typeSet.prometheus) {
this.integrationTestPayload.json = null;
}
if (type === typeSet.http && this.showMappingBuilder) { if (type === typeSet.http && this.showMappingBuilder) {
this.parsedPayload = payloadAlertFields; this.parsedPayload = payloadAlertFields;
this.integrationTestPayload.json = this.isValidNonEmptyJSON(payloadExample) this.samplePayload.json = this.isValidNonEmptyJSON(payloadExample) ? payloadExample : null;
? payloadExample
: null;
const mapping = payloadAttributeMappings.map((mappingItem) => const mapping = payloadAttributeMappings.map((mappingItem) =>
omit(mappingItem, '__typename'), omit(mappingItem, '__typename'),
); );
this.updateMapping(mapping); this.updateMapping(mapping);
} }
this.toggleFormVisibility();
}, },
}, },
methods: { methods: {
...@@ -280,19 +218,15 @@ export default { ...@@ -280,19 +218,15 @@ export default {
} }
return false; return false;
}, },
toggleFormVisibility() { sendTestAlert() {
this.formVisible = this.selectedIntegration !== integrationTypes.none.value; this.$emit('test-alert-payload', this.testAlertPayload);
},
submitWithTestPayload() {
this.$emit('set-test-alert-payload', this.testAlertPayload);
this.submit();
}, },
submit() { submit() {
const { name, apiUrl } = this.integrationForm; const { name, apiUrl } = this.integrationForm;
const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
? { ? {
payloadAttributeMappings: this.mapping, payloadAttributeMappings: this.mapping,
payloadExample: this.integrationTestPayload.json || '{}', payloadExample: this.samplePayload.json || '{}',
} }
: {}; : {};
...@@ -300,7 +234,9 @@ export default { ...@@ -300,7 +234,9 @@ export default {
this.selectedIntegration === typeSet.http this.selectedIntegration === typeSet.http
? { name, active: this.active, ...customMappingVariables } ? { name, active: this.active, ...customMappingVariables }
: { apiUrl, active: this.active }; : { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables }; const integrationPayload = { type: this.selectedIntegration, variables };
if (this.currentIntegration) { if (this.currentIntegration) {
return this.$emit('update-integration', integrationPayload); return this.$emit('update-integration', integrationPayload);
} }
...@@ -309,20 +245,15 @@ export default { ...@@ -309,20 +245,15 @@ export default {
return this.$emit('create-new-integration', integrationPayload); return this.$emit('create-new-integration', integrationPayload);
}, },
reset() { reset() {
this.selectedIntegration = integrationTypes.none.value; this.resetFormValues();
this.toggleFormVisibility();
this.resetPayloadAndMapping(); this.resetPayloadAndMapping();
this.$emit('clear-current-integration', { type: this.currentIntegration?.type });
if (this.currentIntegration) {
return this.$emit('clear-current-integration', { type: this.currentIntegration.type });
}
return this.resetFormValues();
}, },
resetFormValues() { resetFormValues() {
this.selectedIntegration = integrationTypes.none.value;
this.integrationForm.name = ''; this.integrationForm.name = '';
this.integrationForm.apiUrl = ''; this.integrationForm.apiUrl = '';
this.integrationTestPayload = { this.samplePayload = {
json: null, json: null,
error: null, error: null,
}; };
...@@ -338,16 +269,18 @@ export default { ...@@ -338,16 +269,18 @@ export default {
variables: { id: this.currentIntegration.id }, variables: { id: this.currentIntegration.id },
}); });
}, },
validateJson() { validateJson(isSamplePayload = true) {
this.integrationTestPayload.error = null; const payload = isSamplePayload ? this.samplePayload : this.testPayload;
if (this.integrationTestPayload.json === '') {
payload.error = null;
if (payload.json === '') {
return; return;
} }
try { try {
JSON.parse(this.integrationTestPayload.json); JSON.parse(payload.json);
} catch (e) { } catch (e) {
this.integrationTestPayload.error = JSON.stringify(e.message); payload.error = JSON.stringify(e.message);
} }
}, },
parseMapping() { parseMapping() {
...@@ -356,7 +289,7 @@ export default { ...@@ -356,7 +289,7 @@ export default {
return this.$apollo return this.$apollo
.query({ .query({
query: parseSamplePayloadQuery, query: parseSamplePayloadQuery,
variables: { projectPath: this.projectPath, payload: this.integrationTestPayload.json }, variables: { projectPath: this.projectPath, payload: this.samplePayload.json },
}) })
.then( .then(
({ ({
...@@ -367,11 +300,13 @@ export default { ...@@ -367,11 +300,13 @@ export default {
this.parsedPayload = alertManagementPayloadFields; this.parsedPayload = alertManagementPayloadFields;
this.resetPayloadAndMappingConfirmed = false; this.resetPayloadAndMappingConfirmed = false;
this.$toast.show(this.$options.i18n.integrationFormSteps.step4.payloadParsedSucessMsg); this.$toast.show(
this.$options.i18n.integrationFormSteps.setSamplePayload.payloadParsedSucessMsg,
);
}, },
) )
.catch(({ message }) => { .catch(({ message }) => {
this.integrationTestPayload.error = message; this.samplePayload.error = message;
}) })
.finally(() => { .finally(() => {
this.parsingPayload = false; this.parsingPayload = false;
...@@ -391,55 +326,49 @@ export default { ...@@ -391,55 +326,49 @@ export default {
<template> <template>
<gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset"> <gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
<h5 class="gl-font-lg gl-my-5">{{ s__('AlertSettings|Add new integrations') }}</h5> <gl-tabs>
<gl-form-group <gl-tab :title="$options.i18n.integrationTabs.configureDetails">
id="integration-type"
:label="$options.i18n.integrationFormSteps.step1.label"
label-for="integration-type"
>
<gl-form-select
v-model="selectedIntegration"
:disabled="isSelectDisabled"
class="mw-100"
:options="integrationTypesOptions"
@change="toggleFormVisibility"
/>
<div v-if="!canAddIntegration" class="gl-my-4" data-testid="multi-integrations-not-supported">
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.step1.enterprise"
link="https://about.gitlab.com/pricing"
/>
</div>
</gl-form-group>
<gl-collapse v-model="formVisible" class="gl-mt-3">
<div>
<gl-form-group <gl-form-group
id="name-integration" v-if="isCreating"
:label="$options.i18n.integrationFormSteps.step2.label" id="integration-type"
label-for="name-integration" :label="$options.i18n.integrationFormSteps.selectType.label"
label-for="integration-type"
> >
<gl-form-input <gl-form-select
v-model="integrationForm.name" v-model="selectedIntegration"
:disabled="isPrometheus" :disabled="isSelectDisabled"
type="text" class="gl-max-w-full"
:placeholder=" :options="integrationTypesOptions"
isPrometheus />
? $options.i18n.integrationFormSteps.step2.prometheus
: $options.i18n.integrationFormSteps.step2.placeholder <alert-settings-form-help-block
" v-if="!canAddIntegration"
disabled="true"
class="gl-display-inline-block gl-my-4"
:message="$options.i18n.integrationFormSteps.selectType.enterprise"
link="https://about.gitlab.com/pricing"
data-testid="multi-integrations-not-supported"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group <div class="gl-mt-3">
id="integration-webhook" <gl-form-group
:label="$options.i18n.integrationFormSteps.step3.label" v-if="isHttp"
label-for="integration-webhook" id="name-integration"
> :label="$options.i18n.integrationFormSteps.nameIntegration.label"
label-for="name-integration"
>
<gl-form-input
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
/>
</gl-form-group>
<alert-settings-form-help-block <alert-settings-form-help-block
:message=" :message="
isPrometheus isPrometheus
? $options.i18n.integrationFormSteps.step3.prometheusHelp ? $options.i18n.integrationFormSteps.setupCredentials.prometheusHelp
: $options.i18n.integrationFormSteps.step3.help : $options.i18n.integrationFormSteps.setupCredentials.help
" "
link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html" link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
/> />
...@@ -447,7 +376,7 @@ export default { ...@@ -447,7 +376,7 @@ export default {
<gl-toggle <gl-toggle
v-model="active" v-model="active"
:is-loading="loading" :is-loading="loading"
:label="__('Active')" :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
class="gl-my-4 gl-font-weight-normal" class="gl-my-4 gl-font-weight-normal"
/> />
...@@ -468,16 +397,110 @@ export default { ...@@ -468,16 +397,110 @@ export default {
</span> </span>
</div> </div>
<gl-form-group
v-if="isHttp"
data-testid="sample-payload-section"
:label="$options.i18n.integrationFormSteps.setSamplePayload.label"
label-for="sample-payload"
:class="{ 'gl-mb-0!': showMappingBuilder }"
:invalid-feedback="samplePayload.error"
>
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelpHttp"
:link="generic.alertsUsageUrl"
/>
<gl-form-textarea
id="sample-payload"
v-model.trim="samplePayload.json"
:disabled="isPayloadEditDisabled"
:state="isSampePayloadValid"
:placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
@input="validateJson"
/>
</gl-form-group>
<template v-if="showMappingBuilder">
<gl-button
v-if="canEditPayload"
v-gl-modal.resetPayloadModal
data-testid="payload-action-btn"
:disabled="!active"
class="gl-mt-3"
>
{{ $options.i18n.integrationFormSteps.setSamplePayload.editPayload }}
</gl-button>
<gl-button
v-else
data-testid="payload-action-btn"
:class="{ 'gl-mt-3': samplePayload.error }"
:disabled="!active || !isSampePayloadValid"
:loading="parsingPayload"
@click="parseMapping"
>
{{ $options.i18n.integrationFormSteps.setSamplePayload.parsePayload }}
</gl-button>
<gl-modal
modal-id="resetPayloadModal"
:title="$options.i18n.integrationFormSteps.setSamplePayload.resetHeader"
:ok-title="$options.i18n.integrationFormSteps.setSamplePayload.resetOk"
ok-variant="danger"
@ok="resetPayloadAndMapping"
>
{{ $options.i18n.integrationFormSteps.setSamplePayload.resetBody }}
</gl-modal>
</template>
<gl-form-group
v-if="showMappingBuilder"
id="mapping-builder"
class="gl-mt-5"
:label="$options.i18n.integrationFormSteps.mapFields.label"
label-for="mapping-builder"
>
<span>{{ $options.i18n.integrationFormSteps.mapFields.intro }}</span>
<mapping-builder
:parsed-payload="parsedPayload"
:saved-mapping="mapping"
:alert-fields="alertFields"
@onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
<div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button
type="submit"
variant="confirm"
class="js-no-auto-disable"
data-testid="integration-form-submit"
>
{{ $options.i18n.saveIntegration }}
</gl-button>
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
$options.i18n.cancelAndClose
}}</gl-button>
</div>
</gl-tab>
<gl-tab :title="$options.i18n.integrationTabs.viewCredentials" :disabled="isCreating">
<gl-form-group id="integration-webhook">
<div class="gl-my-4"> <div class="gl-my-4">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ s__('AlertSettings|Webhook URL') }} {{ $options.i18n.integrationFormSteps.setupCredentials.webhookUrl }}
</span> </span>
<gl-form-input-group id="url" readonly :value="integrationForm.url"> <gl-form-input-group id="url" readonly :value="integrationForm.url">
<template #append> <template #append>
<clipboard-button <clipboard-button
:text="integrationForm.url || ''" :text="integrationForm.url || ''"
:title="__('Copy')" :title="$options.i18n.copy"
class="gl-m-0!" class="gl-m-0!"
/> />
</template> </template>
...@@ -486,7 +509,7 @@ export default { ...@@ -486,7 +509,7 @@ export default {
<div class="gl-my-4"> <div class="gl-my-4">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ $options.i18n.integrationFormSteps.step3.info }} {{ $options.i18n.integrationFormSteps.setupCredentials.authorizationKey }}
</span> </span>
<gl-form-input-group <gl-form-input-group
...@@ -498,124 +521,67 @@ export default { ...@@ -498,124 +521,67 @@ export default {
<template #append> <template #append>
<clipboard-button <clipboard-button
:text="integrationForm.token || ''" :text="integrationForm.token || ''"
:title="__('Copy')" :title="$options.i18n.copy"
class="gl-m-0!" class="gl-m-0!"
/> />
</template> </template>
</gl-form-input-group> </gl-form-input-group>
<gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled">
{{ $options.i18n.integrationFormSteps.step3.reset }}
</gl-button>
<gl-modal
modal-id="authKeyModal"
:title="$options.i18n.integrationFormSteps.step3.reset"
:ok-title="$options.i18n.integrationFormSteps.step3.reset"
ok-variant="danger"
@ok="resetAuthKey"
>
{{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
</gl-modal>
</div> </div>
</gl-form-group> </gl-form-group>
<gl-form-group <gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled" variant="danger">
id="test-integration" {{ $options.i18n.integrationFormSteps.setupCredentials.reset }}
:label="$options.i18n.integrationFormSteps.step4.label" </gl-button>
label-for="test-integration"
:class="{ 'gl-mb-0!': showMappingBuilder }" <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
:invalid-feedback="integrationTestPayload.error" $options.i18n.cancelAndClose
}}</gl-button>
<gl-modal
modal-id="authKeyModal"
:title="$options.i18n.integrationFormSteps.setupCredentials.reset"
:ok-title="$options.i18n.integrationFormSteps.setupCredentials.reset"
ok-variant="danger"
@ok="resetAuthKey"
> >
{{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
</gl-modal>
</gl-tab>
<gl-tab :title="$options.i18n.integrationTabs.sendTestAlert" :disabled="isCreating">
<gl-form-group id="test-integration" :invalid-feedback="testPayload.error">
<alert-settings-form-help-block <alert-settings-form-help-block
:message=" :message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelp"
isPrometheus || !showMappingBuilder
? $options.i18n.integrationFormSteps.step4.prometheusHelp
: $options.i18n.integrationFormSteps.step4.help
"
:link="generic.alertsUsageUrl" :link="generic.alertsUsageUrl"
/> />
<gl-form-textarea <gl-form-textarea
id="test-payload" id="test-payload"
v-model.trim="integrationTestPayload.json" v-model.trim="testPayload.json"
:disabled="isPayloadEditDisabled" :state="isTestPayloadValid"
:state="jsonIsValid" :placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
:placeholder="$options.i18n.integrationFormSteps.step4.placeholder"
class="gl-my-3" class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY" :debounce="$options.JSON_VALIDATE_DELAY"
rows="6" rows="6"
max-rows="10" max-rows="10"
@input="validateJson" @input="validateJson(false)"
/> />
</gl-form-group> </gl-form-group>
<template v-if="showMappingBuilder">
<gl-button
v-if="canEditPayload"
v-gl-modal.resetPayloadModal
data-testid="payload-action-btn"
:disabled="!active"
class="gl-mt-3"
>
{{ $options.i18n.integrationFormSteps.step4.editPayload }}
</gl-button>
<gl-button
v-else
data-testid="payload-action-btn"
:class="{ 'gl-mt-3': integrationTestPayload.error }"
:disabled="!active"
:loading="parsingPayload"
@click="parseMapping"
>
{{ $options.i18n.integrationFormSteps.step4.submitPayload }}
</gl-button>
<gl-modal
modal-id="resetPayloadModal"
:title="$options.i18n.integrationFormSteps.step4.resetHeader"
:ok-title="$options.i18n.integrationFormSteps.step4.resetOk"
ok-variant="danger"
@ok="resetPayloadAndMapping"
>
{{ $options.i18n.integrationFormSteps.step4.resetBody }}
</gl-modal>
</template>
<gl-form-group
v-if="showMappingBuilder"
id="mapping-builder"
class="gl-mt-5"
:label="$options.i18n.integrationFormSteps.step5.label"
label-for="mapping-builder"
>
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<mapping-builder
:parsed-payload="parsedPayload"
:saved-mapping="savedMapping"
:alert-fields="alertFields"
@onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
<div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button <gl-button
type="submit" :disabled="!isTestPayloadValid"
variant="success" data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable" class="js-no-auto-disable"
data-testid="integration-form-submit" @click="sendTestAlert"
>{{ s__('AlertSettings|Save integration') }}
</gl-button>
<gl-button
data-testid="integration-test-and-submit"
:disabled="isSubmitTestPayloadDisabled"
category="secondary"
variant="success"
class="gl-mx-3 js-no-auto-disable"
@click="submitWithTestPayload"
>{{ s__('AlertSettings|Save and test payload') }}</gl-button
> >
<gl-button type="reset" class="js-no-auto-disable">{{ __('Cancel') }}</gl-button> {{ $options.i18n.send }}
</div> </gl-button>
</gl-collapse>
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
$options.i18n.cancelAndClose
}}</gl-button>
</gl-tab>
</gl-tabs>
</gl-form> </gl-form>
</template> </template>
<script> <script>
import { GlButton } from '@gitlab/ui';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import createFlash, { FLASH_TYPES } from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
...@@ -31,20 +32,24 @@ import { ...@@ -31,20 +32,24 @@ import {
import IntegrationsList from './alerts_integrations_list.vue'; import IntegrationsList from './alerts_integrations_list.vue';
import AlertSettingsForm from './alerts_settings_form.vue'; import AlertSettingsForm from './alerts_settings_form.vue';
export const i18n = {
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
alertSent: s__(
'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.',
),
addNewIntegration: s__('AlertSettings|Add new integration'),
};
export default { export default {
typeSet, typeSet,
i18n: { i18n,
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
alertSent: s__(
'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.',
),
},
components: { components: {
IntegrationsList, IntegrationsList,
AlertSettingsForm, AlertSettingsForm,
GlButton,
}, },
inject: { inject: {
generic: { generic: {
...@@ -116,10 +121,10 @@ export default { ...@@ -116,10 +121,10 @@ export default {
data() { data() {
return { return {
isUpdating: false, isUpdating: false,
testAlertPayload: null,
integrations: {}, integrations: {},
httpIntegrations: {}, httpIntegrations: {},
currentIntegration: null, currentIntegration: null,
formVisible: false,
}; };
}, },
computed: { computed: {
...@@ -161,18 +166,6 @@ export default { ...@@ -161,18 +166,6 @@ export default {
return createFlash({ message: error }); return createFlash({ message: error });
} }
if (this.testAlertPayload) {
const integration =
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
const payload = {
...this.testAlertPayload,
endpoint: integration.url,
token: integration.token,
};
return this.validateAlertPayload(payload);
}
return createFlash({ return createFlash({
message: this.$options.i18n.changesSaved, message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS, type: FLASH_TYPES.SUCCESS,
...@@ -203,10 +196,6 @@ export default { ...@@ -203,10 +196,6 @@ export default {
return createFlash({ message: error }); return createFlash({ message: error });
} }
if (this.testAlertPayload) {
return this.validateAlertPayload();
}
this.clearCurrentIntegration({ type }); this.clearCurrentIntegration({ type });
return createFlash({ return createFlash({
...@@ -219,7 +208,6 @@ export default { ...@@ -219,7 +208,6 @@ export default {
}) })
.finally(() => { .finally(() => {
this.isUpdating = false; this.isUpdating = false;
this.testAlertPayload = null;
}); });
}, },
resetToken({ type, variables }) { resetToken({ type, variables }) {
...@@ -276,6 +264,7 @@ export default { ...@@ -276,6 +264,7 @@ export default {
: updateCurrentPrometheusIntegrationMutation, : updateCurrentPrometheusIntegrationMutation,
variables: currentIntegration, variables: currentIntegration,
}); });
this.setFormVisibility(true);
}, },
deleteIntegration({ id, type }) { deleteIntegration({ id, type }) {
const { projectPath } = this; const { projectPath } = this;
...@@ -308,19 +297,19 @@ export default { ...@@ -308,19 +297,19 @@ export default {
}); });
}, },
clearCurrentIntegration({ type }) { clearCurrentIntegration({ type }) {
this.$apollo.mutate({ if (type) {
mutation: this.isHttp(type) this.$apollo.mutate({
? updateCurrentHttpIntegrationMutation mutation: this.isHttp(type)
: updateCurrentPrometheusIntegrationMutation, ? updateCurrentHttpIntegrationMutation
variables: {}, : updateCurrentPrometheusIntegrationMutation,
}); variables: {},
}, });
setTestAlertPayload(payload) { }
this.testAlertPayload = payload; this.setFormVisibility(false);
}, },
validateAlertPayload(payload) { testAlertPayload(payload) {
return service return service
.updateTestAlert(payload ?? this.testAlertPayload) .updateTestAlert(payload)
.then(() => { .then(() => {
return createFlash({ return createFlash({
message: this.$options.i18n.alertSent, message: this.$options.i18n.alertSent,
...@@ -331,6 +320,9 @@ export default { ...@@ -331,6 +320,9 @@ export default {
createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
}); });
}, },
setFormVisibility(visible) {
this.formVisible = visible;
},
}, },
}; };
</script> </script>
...@@ -343,7 +335,18 @@ export default { ...@@ -343,7 +335,18 @@ export default {
@edit-integration="editIntegration" @edit-integration="editIntegration"
@delete-integration="deleteIntegration" @delete-integration="deleteIntegration"
/> />
<gl-button
v-if="canAddIntegration && !formVisible"
category="secondary"
variant="confirm"
data-testid="add-integration-btn"
class="gl-mt-3"
@click="setFormVisibility(true)"
>
{{ $options.i18n.addNewIntegration }}
</gl-button>
<alert-settings-form <alert-settings-form
v-if="formVisible"
:loading="isUpdating" :loading="isUpdating"
:can-add-integration="canAddIntegration" :can-add-integration="canAddIntegration"
:alert-fields="alertFields" :alert-fields="alertFields"
...@@ -351,7 +354,7 @@ export default { ...@@ -351,7 +354,7 @@ export default {
@update-integration="updateIntegration" @update-integration="updateIntegration"
@reset-token="resetToken" @reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration" @clear-current-integration="clearCurrentIntegration"
@set-test-alert-payload="setTestAlertPayload" @test-alert-payload="testAlertPayload"
/> />
</div> </div>
</template> </template>
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
// TODO: Remove this as part of the form old removal // TODO: Remove this as part of the form old removal
export const i18n = { export const i18n = {
...@@ -38,6 +38,74 @@ export const i18n = { ...@@ -38,6 +38,74 @@ export const i18n = {
'AlertSettings|Authorization key has been successfully reset. Please save your changes now.', 'AlertSettings|Authorization key has been successfully reset. Please save your changes now.',
), ),
integration: s__('AlertSettings|Integration'), integration: s__('AlertSettings|Integration'),
integrationTabs: {
configureDetails: s__('AlertSettings|Configure details'),
viewCredentials: s__('AlertSettings|View credentials'),
sendTestAlert: s__('AlertSettings|Send test alert'),
},
integrationFormSteps: {
selectType: {
label: s__('AlertSettings|Select integration type'),
enterprise: s__(
'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.',
),
},
nameIntegration: {
label: s__('AlertSettings|Name integration'),
placeholder: s__('AlertSettings|Enter integration name'),
activeToggle: __('Active'),
},
setupCredentials: {
help: s__(
"AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.",
),
prometheusHelp: s__(
'AlertSettings|Utilize the URL and authorization key below to authorize Prometheus to send alerts to GitLab. Review the Prometheus documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.',
),
webhookUrl: s__('AlertSettings|Webhook URL'),
authorizationKey: s__('AlertSettings|Authorization key'),
reset: s__('AlertSettings|Reset Key'),
},
setSamplePayload: {
label: s__('AlertSettings|Sample alert payload (optional)'),
testPayloadHelpHttp: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional).',
),
testPayloadHelp: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.',
),
placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
resetHeader: s__('AlertSettings|Reset the mapping'),
resetBody: s__(
"AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields.",
),
resetOk: s__('AlertSettings|Proceed with editing'),
editPayload: s__('AlertSettings|Edit payload'),
parsePayload: s__('AlertSettings|Parse payload for custom mapping'),
payloadParsedSucessMsg: s__(
'AlertSettings|Sample payload has been parsed. You can now map the fields.',
),
},
mapFields: {
label: s__('AlertSettings|Map fields (optional)'),
intro: s__(
"AlertSettings|If you've provided a sample alert payload, you can create a custom mapping for your endpoint. The default GitLab alert keys are listed below. Please define which payload key should map to the specified GitLab key.",
),
},
prometheusFormUrl: {
label: s__('AlertSettings|Prometheus API base URL'),
help: s__('AlertSettings|URL cannot be blank and must start with http or https'),
},
restKeyInfo: {
label: s__(
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
},
},
saveIntegration: s__('AlertSettings|Save integration'),
cancelAndClose: __('Cancel and close'),
send: s__('AlertSettings|Send'),
copy: __('Copy'),
}; };
export const integrationTypes = { export const integrationTypes = {
......
...@@ -17,5 +17,5 @@ export const RESET_INTEGRATION_TOKEN_ERROR = s__( ...@@ -17,5 +17,5 @@ export const RESET_INTEGRATION_TOKEN_ERROR = s__(
); );
export const INTEGRATION_PAYLOAD_TEST_ERROR = s__( export const INTEGRATION_PAYLOAD_TEST_ERROR = s__(
'AlertsIntegrations|Integration payload is invalid. You can still save your changes.', 'AlertsIntegrations|Integration payload is invalid.',
); );
---
title: Regroup alerts integration form into tabs
merge_request: 54842
author:
type: changed
...@@ -2820,21 +2820,6 @@ msgstr "" ...@@ -2820,21 +2820,6 @@ msgstr ""
msgid "AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. " msgid "AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. "
msgstr "" msgstr ""
msgid "AlertSettings|1. Select integration type"
msgstr ""
msgid "AlertSettings|2. Name integration"
msgstr ""
msgid "AlertSettings|3. Set up webhook"
msgstr ""
msgid "AlertSettings|4. Sample alert payload (optional)"
msgstr ""
msgid "AlertSettings|5. Map fields (optional)"
msgstr ""
msgid "AlertSettings|API URL" msgid "AlertSettings|API URL"
msgstr "" msgstr ""
...@@ -2844,7 +2829,7 @@ msgstr "" ...@@ -2844,7 +2829,7 @@ msgstr ""
msgid "AlertSettings|Add URL and auth key to your Prometheus config file" msgid "AlertSettings|Add URL and auth key to your Prometheus config file"
msgstr "" msgstr ""
msgid "AlertSettings|Add new integrations" msgid "AlertSettings|Add new integration"
msgstr "" msgstr ""
msgid "AlertSettings|Alert test payload" msgid "AlertSettings|Alert test payload"
...@@ -2856,6 +2841,9 @@ msgstr "" ...@@ -2856,6 +2841,9 @@ msgstr ""
msgid "AlertSettings|Authorization key has been successfully reset. Please save your changes now." msgid "AlertSettings|Authorization key has been successfully reset. Please save your changes now."
msgstr "" msgstr ""
msgid "AlertSettings|Configure details"
msgstr ""
msgid "AlertSettings|Copy" msgid "AlertSettings|Copy"
msgstr "" msgstr ""
...@@ -2892,19 +2880,25 @@ msgstr "" ...@@ -2892,19 +2880,25 @@ msgstr ""
msgid "AlertSettings|Learn more about our our upcoming %{linkStart}integrations%{linkEnd}" msgid "AlertSettings|Learn more about our our upcoming %{linkStart}integrations%{linkEnd}"
msgstr "" msgstr ""
msgid "AlertSettings|Proceed with editing" msgid "AlertSettings|Map fields (optional)"
msgstr "" msgstr ""
msgid "AlertSettings|Prometheus" msgid "AlertSettings|Name integration"
msgstr ""
msgid "AlertSettings|Parse payload for custom mapping"
msgstr ""
msgid "AlertSettings|Proceed with editing"
msgstr "" msgstr ""
msgid "AlertSettings|Prometheus API base URL" msgid "AlertSettings|Prometheus API base URL"
msgstr "" msgstr ""
msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional), or to test the integration (also optional)." msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional)."
msgstr "" msgstr ""
msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional)." msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point."
msgstr "" msgstr ""
msgid "AlertSettings|Reset Key" msgid "AlertSettings|Reset Key"
...@@ -2922,10 +2916,10 @@ msgstr "" ...@@ -2922,10 +2916,10 @@ 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." 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 "" msgstr ""
msgid "AlertSettings|Sample payload has been parsed. You can now map the fields." msgid "AlertSettings|Sample alert payload (optional)"
msgstr "" msgstr ""
msgid "AlertSettings|Save and test payload" msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
msgstr "" msgstr ""
msgid "AlertSettings|Save integration" msgid "AlertSettings|Save integration"
...@@ -2934,7 +2928,10 @@ msgstr "" ...@@ -2934,7 +2928,10 @@ msgstr ""
msgid "AlertSettings|Select integration type" msgid "AlertSettings|Select integration type"
msgstr "" msgstr ""
msgid "AlertSettings|Submit payload" msgid "AlertSettings|Send"
msgstr ""
msgid "AlertSettings|Send test alert"
msgstr "" msgstr ""
msgid "AlertSettings|Test alert payload" msgid "AlertSettings|Test alert payload"
...@@ -2961,6 +2958,9 @@ msgstr "" ...@@ -2961,6 +2958,9 @@ msgstr ""
msgid "AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint." msgid "AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr "" msgstr ""
msgid "AlertSettings|View credentials"
msgstr ""
msgid "AlertSettings|Webhook URL" msgid "AlertSettings|Webhook URL"
msgstr "" msgstr ""
...@@ -2991,7 +2991,7 @@ msgstr "" ...@@ -2991,7 +2991,7 @@ msgstr ""
msgid "AlertsIntegrations|Integration Name" msgid "AlertsIntegrations|Integration Name"
msgstr "" msgstr ""
msgid "AlertsIntegrations|Integration payload is invalid. You can still save your changes." msgid "AlertsIntegrations|Integration payload is invalid."
msgstr "" msgstr ""
msgid "AlertsIntegrations|No integrations have been added yet" msgid "AlertsIntegrations|No integrations have been added yet"
...@@ -5418,6 +5418,9 @@ msgstr "" ...@@ -5418,6 +5418,9 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
msgid "Cancel and close"
msgstr ""
msgid "Cancel index deletion" msgid "Cancel index deletion"
msgstr "" msgstr ""
......
...@@ -30,8 +30,8 @@ RSpec.describe 'Alert integrations settings form', :js do ...@@ -30,8 +30,8 @@ RSpec.describe 'Alert integrations settings form', :js do
end end
end end
it 'shows the new alerts setting form' do it 'shows the integrations list title' do
expect(page).to have_content('1. Select integration type') expect(page).to have_content('Current integrations')
end end
end end
end end
...@@ -44,7 +44,7 @@ RSpec.describe 'Alert integrations settings form', :js do ...@@ -44,7 +44,7 @@ RSpec.describe 'Alert integrations settings form', :js do
wait_for_requests wait_for_requests
end end
it 'shows the old alerts setting form' do it 'does not have rights to access the setting form' do
expect(page).not_to have_selector('.incident-management-list') expect(page).not_to have_selector('.incident-management-list')
expect(page).not_to have_selector('#js-alert-management-settings') expect(page).not_to have_selector('#js-alert-management-settings')
end end
......
...@@ -4,113 +4,152 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] ...@@ -4,113 +4,152 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
<form <form
class="gl-mt-6" class="gl-mt-6"
> >
<h5
class="gl-font-lg gl-my-5"
>
Add new integrations
</h5>
<div <div
class="form-group gl-form-group" class="tabs gl-tabs"
id="integration-type" id="__BVID__6"
role="group"
> >
<label <!---->
class="d-block col-form-label"
for="integration-type"
id="integration-type__BV_label_"
>
1. Select integration type
</label>
<div <div
class="bv-no-focus-ring" class=""
> >
<select <ul
class="gl-form-select mw-100 custom-select" class="nav gl-tabs-nav"
id="__BVID__8" id="__BVID__6__BV_tab_controls_"
role="tablist"
> >
<option <!---->
value="" <li
class="nav-item"
role="presentation"
> >
Select integration type <a
</option> aria-controls="__BVID__8"
<option aria-posinset="1"
value="HTTP" aria-selected="true"
aria-setsize="3"
class="nav-link active gl-tab-nav-item gl-tab-nav-item-active gl-tab-nav-item-active-indigo"
href="#"
id="__BVID__8___BV_tab_button__"
role="tab"
target="_self"
>
Configure details
</a>
</li>
<li
class="nav-item"
role="presentation"
> >
HTTP Endpoint <a
</option> aria-controls="__BVID__22"
<option aria-disabled="true"
value="PROMETHEUS" aria-posinset="2"
aria-selected="false"
aria-setsize="3"
class="nav-link disabled disabled gl-tab-nav-item"
href="#"
id="__BVID__22___BV_tab_button__"
role="tab"
tabindex="-1"
target="_self"
>
View credentials
</a>
</li>
<li
class="nav-item"
role="presentation"
> >
External Prometheus <a
</option> aria-controls="__BVID__41"
</select> aria-disabled="true"
aria-posinset="3"
<!----> aria-selected="false"
<!----> aria-setsize="3"
<!----> class="nav-link disabled disabled gl-tab-nav-item"
<!----> href="#"
id="__BVID__41___BV_tab_button__"
role="tab"
tabindex="-1"
target="_self"
>
Send test alert
</a>
</li>
<!---->
</ul>
</div> </div>
</div>
<transition-stub
class="gl-mt-3"
css="true"
enteractiveclass="collapsing"
enterclass=""
entertoclass="collapse show"
leaveactiveclass="collapsing"
leaveclass="collapse show"
leavetoclass="collapse"
>
<div <div
class="collapse" class="tab-content gl-tab-content"
id="__BVID__10" id="__BVID__6__BV_tab_container_"
style="display: none;"
> >
<div> <transition-stub
css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
leaveactiveclass=""
leaveclass="show"
leavetoclass=""
mode="out-in"
name=""
>
<div <div
class="form-group gl-form-group" aria-hidden="false"
id="name-integration" aria-labelledby="__BVID__8___BV_tab_button__"
role="group" class="tab-pane active"
id="__BVID__8"
role="tabpanel"
style=""
> >
<label
class="d-block col-form-label"
for="name-integration"
id="name-integration__BV_label_"
>
2. Name integration
</label>
<div <div
class="bv-no-focus-ring" class="form-group gl-form-group"
id="integration-type"
role="group"
> >
<input <label
class="gl-form-input form-control" class="d-block col-form-label"
id="__BVID__15" for="integration-type"
placeholder="Enter integration name" id="integration-type__BV_label_"
type="text" >
/> Select integration type
<!----> </label>
<!----> <div
<!----> class="bv-no-focus-ring"
>
<select
class="gl-form-select gl-max-w-full custom-select"
id="__BVID__13"
>
<option
value=""
>
Select integration type
</option>
<option
value="HTTP"
>
HTTP Endpoint
</option>
<option
value="PROMETHEUS"
>
External Prometheus
</option>
</select>
<!---->
<!---->
<!---->
<!---->
</div>
</div> </div>
</div>
<div
class="form-group gl-form-group"
id="integration-webhook"
role="group"
>
<label
class="d-block col-form-label"
for="integration-webhook"
id="integration-webhook__BV_label_"
>
3. Set up webhook
</label>
<div <div
class="bv-no-focus-ring" class="gl-mt-3"
> >
<!---->
<span> <span>
Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the
<a <a
...@@ -166,241 +205,324 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] ...@@ -166,241 +205,324 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
<!----> <!---->
<div <!---->
class="gl-my-4"
<!---->
<!---->
</div>
<div
class="gl-display-flex gl-justify-content-start gl-py-3"
>
<button
class="btn js-no-auto-disable btn-confirm btn-md gl-button"
data-testid="integration-form-submit"
type="submit"
> >
<!---->
<!---->
<span <span
class="gl-font-weight-bold" class="gl-button-text"
> >
Webhook URL Save integration
</span> </span>
</button>
<button
class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
type="reset"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Cancel and close
</span>
</button>
</div>
</div>
</transition-stub>
<transition-stub
css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
leaveactiveclass=""
leaveclass="show"
leavetoclass=""
mode="out-in"
name=""
>
<div
aria-hidden="true"
aria-labelledby="__BVID__22___BV_tab_button__"
class="tab-pane disabled"
id="__BVID__22"
role="tabpanel"
style="display: none;"
>
<fieldset
class="form-group gl-form-group"
id="integration-webhook"
>
<!---->
<div
class="bv-no-focus-ring"
role="group"
tabindex="-1"
>
<div <div
id="url" class="gl-my-4"
readonly="readonly"
> >
<span
class="gl-font-weight-bold"
>
Webhook URL
</span>
<div <div
class="input-group" id="url"
role="group" readonly="readonly"
> >
<!---->
<!---->
<input
class="gl-form-input form-control"
id="url"
readonly="readonly"
type="text"
/>
<div <div
class="input-group-append" class="input-group"
role="group"
> >
<button <!---->
aria-label="Copy this value" <!---->
class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
data-clipboard-text="" <input
title="Copy" class="gl-form-input form-control"
type="button" id="url"
readonly="readonly"
type="text"
/>
<div
class="input-group-append"
> >
<!----> <button
aria-label="Copy this value"
<svg class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
aria-hidden="true" data-clipboard-text=""
class="gl-button-icon gl-icon s16" title="Copy"
data-testid="copy-to-clipboard-icon" type="button"
> >
<use <!---->
href="#copy-to-clipboard"
/> <svg
</svg> aria-hidden="true"
class="gl-button-icon gl-icon s16"
<!----> data-testid="copy-to-clipboard-icon"
</button> >
<use
href="#copy-to-clipboard"
/>
</svg>
<!---->
</button>
</div>
<!---->
</div> </div>
<!---->
</div> </div>
</div> </div>
</div>
<div
class="gl-my-4"
>
<span
class="gl-font-weight-bold"
>
Authorization key
</span>
<div <div
class="gl-mb-3" class="gl-my-4"
id="authorization-key"
readonly="readonly"
> >
<span
class="gl-font-weight-bold"
>
Authorization key
</span>
<div <div
class="input-group" class="gl-mb-3"
role="group" id="authorization-key"
readonly="readonly"
> >
<!---->
<!---->
<input
class="gl-form-input form-control"
id="authorization-key"
readonly="readonly"
type="text"
/>
<div <div
class="input-group-append" class="input-group"
role="group"
> >
<button <!---->
aria-label="Copy this value" <!---->
class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
data-clipboard-text="" <input
title="Copy" class="gl-form-input form-control"
type="button" id="authorization-key"
readonly="readonly"
type="text"
/>
<div
class="input-group-append"
> >
<!----> <button
aria-label="Copy this value"
<svg class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
aria-hidden="true" data-clipboard-text=""
class="gl-button-icon gl-icon s16" title="Copy"
data-testid="copy-to-clipboard-icon" type="button"
> >
<use <!---->
href="#copy-to-clipboard"
/> <svg
</svg> aria-hidden="true"
class="gl-button-icon gl-icon s16"
<!----> data-testid="copy-to-clipboard-icon"
</button> >
<use
href="#copy-to-clipboard"
/>
</svg>
<!---->
</button>
</div>
<!---->
</div> </div>
<!---->
</div> </div>
</div> </div>
<!---->
<button <!---->
class="btn btn-default btn-md disabled gl-button"
disabled="disabled"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Reset Key
</span>
</button>
<!----> <!---->
</div> </div>
<!----> </fieldset>
<!---->
<!----> <button
</div> class="btn btn-danger btn-md disabled gl-button"
</div> disabled="disabled"
type="button"
<div
class="form-group gl-form-group"
id="test-integration"
role="group"
>
<label
class="d-block col-form-label"
for="test-integration"
id="test-integration__BV_label_"
>
4. Sample alert payload (optional)
</label>
<div
class="bv-no-focus-ring"
> >
<span> <!---->
Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).
</span>
<textarea
class="gl-form-input gl-form-textarea gl-my-3 form-control is-valid"
disabled="disabled"
id="test-payload"
placeholder="{ \\"events\\": [{ \\"application\\": \\"Name of application\\" }] }"
style="resize: none; overflow-y: scroll;"
wrap="soft"
/>
<!----> <!---->
<span
class="gl-button-text"
>
Reset Key
</span>
</button>
<button
class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
type="reset"
>
<!----> <!---->
<!----> <!---->
</div>
<span
class="gl-button-text"
>
Cancel and close
</span>
</button>
<!---->
</div> </div>
</transition-stub>
<!---->
<!---->
</div>
<div <transition-stub
class="gl-display-flex gl-justify-content-start gl-py-3" css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
leaveactiveclass=""
leaveclass="show"
leavetoclass=""
mode="out-in"
name=""
> >
<button <div
class="btn js-no-auto-disable btn-success btn-md gl-button" aria-hidden="true"
data-testid="integration-form-submit" aria-labelledby="__BVID__41___BV_tab_button__"
type="submit" class="tab-pane disabled"
id="__BVID__41"
role="tabpanel"
style="display: none;"
> >
<!----> <fieldset
class="form-group gl-form-group"
<!----> id="test-integration"
<span
class="gl-button-text"
> >
Save integration <!---->
<div
</span> class="bv-no-focus-ring"
</button> role="group"
tabindex="-1"
<button >
class="btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary" <span>
data-testid="integration-test-and-submit" Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.
disabled="disabled" </span>
type="button"
> <textarea
<!----> class="gl-form-input gl-form-textarea gl-my-3 form-control is-valid"
id="test-payload"
placeholder="{ \\"events\\": [{ \\"application\\": \\"Name of application\\" }] }"
style="resize: none; overflow-y: scroll;"
wrap="soft"
/>
<!---->
<!---->
<!---->
</div>
</fieldset>
<!----> <button
class="btn js-no-auto-disable btn-confirm btn-md gl-button"
<span data-testid="send-test-alert"
class="gl-button-text" type="button"
> >
Save and test payload <!---->
</span>
</button> <!---->
<button <span
class="btn js-no-auto-disable btn-default btn-md gl-button" class="gl-button-text"
type="reset" >
>
<!----> Send
</span>
</button>
<!----> <button
class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
<span type="reset"
class="gl-button-text"
> >
Cancel <!---->
</span>
</button> <!---->
</div>
<span
class="gl-button-text"
>
Cancel and close
</span>
</button>
</div>
</transition-stub>
<!---->
</div> </div>
</transition-stub> </div>
</form> </form>
`; `;
import { import { GlForm, GlFormSelect, GlFormInput, GlToggle, GlFormTextarea, GlTab } from '@gitlab/ui';
GlForm,
GlFormSelect,
GlCollapse,
GlFormInput,
GlToggle,
GlFormTextarea,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue'; import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
...@@ -52,18 +45,18 @@ describe('AlertsSettingsForm', () => { ...@@ -52,18 +45,18 @@ describe('AlertsSettingsForm', () => {
const findForm = () => wrapper.find(GlForm); const findForm = () => wrapper.find(GlForm);
const findSelect = () => wrapper.find(GlFormSelect); const findSelect = () => wrapper.find(GlFormSelect);
const findFormSteps = () => wrapper.find(GlCollapse);
const findFormFields = () => wrapper.findAll(GlFormInput); const findFormFields = () => wrapper.findAll(GlFormInput);
const findFormToggle = () => wrapper.find(GlToggle); const findFormToggle = () => wrapper.find(GlToggle);
const findTestPayloadSection = () => wrapper.find(`[id = "test-integration"]`); const findSamplePayloadSection = () => wrapper.find('[data-testid="sample-payload-section"]');
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`); const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
const findMappingBuilder = () => wrapper.findComponent(MappingBuilder); const findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`); const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
const findMultiSupportText = () => const findMultiSupportText = () =>
wrapper.find(`[data-testid="multi-integrations-not-supported"]`); wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
const findJsonTestSubmit = () => wrapper.find(`[data-testid="integration-test-and-submit"]`); const findJsonTestSubmit = () => wrapper.find(`[data-testid="send-test-alert"]`);
const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`); const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`);
const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`); const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`);
const findTabs = () => wrapper.findAll(GlTab);
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
...@@ -95,7 +88,7 @@ describe('AlertsSettingsForm', () => { ...@@ -95,7 +88,7 @@ describe('AlertsSettingsForm', () => {
expect(findForm().exists()).toBe(true); expect(findForm().exists()).toBe(true);
expect(findSelect().exists()).toBe(true); expect(findSelect().exists()).toBe(true);
expect(findMultiSupportText().exists()).toBe(false); expect(findMultiSupportText().exists()).toBe(false);
expect(findFormSteps().attributes('visible')).toBeUndefined(); expect(findFormFields()).toHaveLength(0);
}); });
it('shows the rest of the form when the dropdown is used', async () => { it('shows the rest of the form when the dropdown is used', async () => {
...@@ -110,11 +103,40 @@ describe('AlertsSettingsForm', () => { ...@@ -110,11 +103,40 @@ describe('AlertsSettingsForm', () => {
expect(findMultiSupportText().exists()).toBe(true); expect(findMultiSupportText().exists()).toBe(true);
}); });
it('disabled the name input when the selected value is prometheus', async () => { it('hides the name input when the selected value is prometheus', async () => {
createComponent(); createComponent();
await selectOptionAtIndex(2); await selectOptionAtIndex(2);
expect(findFormFields().at(0).attributes('id')).not.toBe('name-integration');
});
describe('form tabs', () => {
it('renders 3 tabs', () => {
expect(findTabs()).toHaveLength(3);
});
expect(findFormFields().at(0).attributes('disabled')).toBe('disabled'); it('only first tab is enabled on integration create', () => {
createComponent({
data: {
currentIntegration: null,
},
});
const tabs = findTabs();
expect(tabs.at(0).find('[role="tabpanel"]').classes('disabled')).toBe(false);
expect(tabs.at(1).find('[role="tabpanel"]').classes('disabled')).toBe(true);
expect(tabs.at(2).find('[role="tabpanel"]').classes('disabled')).toBe(true);
});
it('all tabs are enabled on integration edit', () => {
createComponent({
data: {
currentIntegration: { id: 1 },
},
});
const tabs = findTabs();
expect(tabs.at(0).find('[role="tabpanel"]').classes('disabled')).toBe(false);
expect(tabs.at(1).find('[role="tabpanel"]').classes('disabled')).toBe(false);
expect(tabs.at(2).find('[role="tabpanel"]').classes('disabled')).toBe(false);
});
}); });
}); });
...@@ -195,14 +217,9 @@ describe('AlertsSettingsForm', () => { ...@@ -195,14 +217,9 @@ describe('AlertsSettingsForm', () => {
describe('PROMETHEUS', () => { describe('PROMETHEUS', () => {
it('create', async () => { it('create', async () => {
createComponent(); createComponent();
await selectOptionAtIndex(2); await selectOptionAtIndex(2);
const apiUrl = 'https://test.com'; const apiUrl = 'https://test.com';
enableIntegration(1, apiUrl); enableIntegration(0, apiUrl);
findFormToggle().trigger('click');
const submitBtn = findSubmitButton(); const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true); expect(submitBtn.exists()).toBe(true);
expect(submitBtn.text()).toBe('Save integration'); expect(submitBtn.text()).toBe('Save integration');
...@@ -226,7 +243,7 @@ describe('AlertsSettingsForm', () => { ...@@ -226,7 +243,7 @@ describe('AlertsSettingsForm', () => {
}); });
const apiUrl = 'https://test-post.com'; const apiUrl = 'https://test-post.com';
enableIntegration(1, apiUrl); enableIntegration(0, apiUrl);
const submitBtn = findSubmitButton(); const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true); expect(submitBtn.exists()).toBe(true);
...@@ -264,7 +281,7 @@ describe('AlertsSettingsForm', () => { ...@@ -264,7 +281,7 @@ describe('AlertsSettingsForm', () => {
const jsonTestSubmit = findJsonTestSubmit(); const jsonTestSubmit = findJsonTestSubmit();
expect(jsonTestSubmit.exists()).toBe(true); expect(jsonTestSubmit.exists()).toBe(true);
expect(jsonTestSubmit.text()).toBe('Save and test payload'); expect(jsonTestSubmit.text()).toBe('Send');
expect(jsonTestSubmit.props('disabled')).toBe(true); expect(jsonTestSubmit.props('disabled')).toBe(true);
}); });
...@@ -313,16 +330,14 @@ describe('AlertsSettingsForm', () => { ...@@ -313,16 +330,14 @@ describe('AlertsSettingsForm', () => {
it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => { it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => {
wrapper.setData({ wrapper.setData({
currentIntegration: { selectedIntegration: typeSet.http,
type: typeSet.http,
payloadExample: validSamplePayload,
payloadAttributeMappings: [],
},
active, active,
resetPayloadAndMappingConfirmed, resetPayloadAndMappingConfirmed,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled); expect(findSamplePayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(
disabled,
);
}); });
}); });
...@@ -330,9 +345,9 @@ describe('AlertsSettingsForm', () => { ...@@ -330,9 +345,9 @@ describe('AlertsSettingsForm', () => {
describe.each` describe.each`
resetPayloadAndMappingConfirmed | payloadExample | caption resetPayloadAndMappingConfirmed | payloadExample | caption
${false} | ${validSamplePayload} | ${'Edit payload'} ${false} | ${validSamplePayload} | ${'Edit payload'}
${true} | ${emptySamplePayload} | ${'Submit payload'} ${true} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
${true} | ${validSamplePayload} | ${'Submit payload'} ${true} | ${validSamplePayload} | ${'Parse payload for custom mapping'}
${false} | ${emptySamplePayload} | ${'Submit payload'} ${false} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
`('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => { `('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided'; const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
const payloadResetMsg = resetPayloadAndMappingConfirmed const payloadResetMsg = resetPayloadAndMappingConfirmed
...@@ -386,7 +401,7 @@ describe('AlertsSettingsForm', () => { ...@@ -386,7 +401,7 @@ describe('AlertsSettingsForm', () => {
await waitForPromises(); await waitForPromises();
expect(findTestPayloadSection().find('.invalid-feedback').text()).toBe(errorMessage); expect(findSamplePayloadSection().find('.invalid-feedback').text()).toBe(errorMessage);
}); });
}); });
}); });
......
...@@ -9,7 +9,9 @@ import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; ...@@ -9,7 +9,9 @@ import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue'; import AlertsSettingsWrapper, {
i18n,
} from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import { typeSet } from '~/alerts_settings/constants'; import { typeSet } from '~/alerts_settings/constants';
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql'; import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql'; import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
...@@ -19,6 +21,7 @@ import updateCurrentHttpIntegrationMutation from '~/alerts_settings/graphql/muta ...@@ -19,6 +21,7 @@ import updateCurrentHttpIntegrationMutation from '~/alerts_settings/graphql/muta
import updateCurrentPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql'; import updateCurrentPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql'; import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
import alertsUpdateService from '~/alerts_settings/services';
import { import {
ADD_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR,
RESET_INTEGRATION_TOKEN_ERROR, RESET_INTEGRATION_TOKEN_ERROR,
...@@ -26,7 +29,7 @@ import { ...@@ -26,7 +29,7 @@ import {
INTEGRATION_PAYLOAD_TEST_ERROR, INTEGRATION_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR, DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages'; } from '~/alerts_settings/utils/error_messages';
import createFlash from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
createHttpVariables, createHttpVariables,
...@@ -78,6 +81,8 @@ describe('AlertsSettingsWrapper', () => { ...@@ -78,6 +81,8 @@ describe('AlertsSettingsWrapper', () => {
const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon); const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon);
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList); const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr'); const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
const findAddIntegrationBtn = () => wrapper.find('[data-testid="add-integration-btn"]');
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
async function destroyHttpIntegration(localWrapper) { async function destroyHttpIntegration(localWrapper) {
await jest.runOnlyPendingTimers(); await jest.runOnlyPendingTimers();
...@@ -144,14 +149,37 @@ describe('AlertsSettingsWrapper', () => { ...@@ -144,14 +149,37 @@ describe('AlertsSettingsWrapper', () => {
wrapper = null; wrapper = null;
}); });
describe('rendered via default permissions', () => { describe('template', () => {
it('renders the GraphQL alerts integrations list and new form', () => { beforeEach(() => {
createComponent(); createComponent({
expect(wrapper.find(IntegrationsList).exists()).toBe(true); data: {
expect(wrapper.find(AlertsSettingsForm).exists()).toBe(true); integrations: { list: mockIntegrations },
httpIntegrations: { list: [] },
currentIntegration: mockIntegrations[0],
},
loading: false,
});
}); });
it('uses a loading state inside the IntegrationsList table', () => { it('renders alerts integrations list and add new integration button by default', () => {
expect(findLoader().exists()).toBe(false);
expect(findIntegrations()).toHaveLength(mockIntegrations.length);
expect(findAddIntegrationBtn().exists()).toBe(true);
});
it('does NOT render settings form by default', () => {
expect(findAlertsSettingsForm().exists()).toBe(false);
});
it('hides `add new integration` button and displays setting form on btn click', async () => {
const addNewIntegrationBtn = findAddIntegrationBtn();
expect(addNewIntegrationBtn.exists()).toBe(true);
await addNewIntegrationBtn.trigger('click');
expect(findAlertsSettingsForm().exists()).toBe(true);
expect(addNewIntegrationBtn.exists()).toBe(false);
});
it('shows loading indicator inside the IntegrationsList table', () => {
createComponent({ createComponent({
data: { integrations: {} }, data: { integrations: {} },
loading: true, loading: true,
...@@ -159,26 +187,24 @@ describe('AlertsSettingsWrapper', () => { ...@@ -159,26 +187,24 @@ describe('AlertsSettingsWrapper', () => {
expect(wrapper.find(IntegrationsList).exists()).toBe(true); expect(wrapper.find(IntegrationsList).exists()).toBe(true);
expect(findLoader().exists()).toBe(true); expect(findLoader().exists()).toBe(true);
}); });
});
it('renders the IntegrationsList table using the API data', () => { describe('Integration updates', () => {
beforeEach(() => {
createComponent({ createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, data: {
integrations: { list: mockIntegrations },
currentIntegration: mockIntegrations[0],
formVisible: true,
},
loading: false, loading: false,
}); });
expect(findLoader().exists()).toBe(false);
expect(findIntegrations()).toHaveLength(mockIntegrations.length);
}); });
it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => { it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createHttpIntegrationMutation: { integration: { id: '1' } } }, data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
}); });
wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', { findAlertsSettingsForm().vm.$emit('create-new-integration', {
type: typeSet.http, type: typeSet.http,
variables: createHttpVariables, variables: createHttpVariables,
}); });
...@@ -192,15 +218,10 @@ describe('AlertsSettingsWrapper', () => { ...@@ -192,15 +218,10 @@ describe('AlertsSettingsWrapper', () => {
}); });
it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => { it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updateHttpIntegrationMutation: { integration: { id: '1' } } }, data: { updateHttpIntegrationMutation: { integration: { id: '1' } } },
}); });
wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', { findAlertsSettingsForm().vm.$emit('update-integration', {
type: typeSet.http, type: typeSet.http,
variables: updateHttpVariables, variables: updateHttpVariables,
}); });
...@@ -212,15 +233,10 @@ describe('AlertsSettingsWrapper', () => { ...@@ -212,15 +233,10 @@ describe('AlertsSettingsWrapper', () => {
}); });
it('calls `$apollo.mutate` with `resetHttpTokenMutation`', () => { it('calls `$apollo.mutate` with `resetHttpTokenMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { resetHttpTokenMutation: { integration: { id: '1' } } }, data: { resetHttpTokenMutation: { integration: { id: '1' } } },
}); });
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { findAlertsSettingsForm().vm.$emit('reset-token', {
type: typeSet.http, type: typeSet.http,
variables: { id: HTTP_ID }, variables: { id: HTTP_ID },
}); });
...@@ -234,15 +250,10 @@ describe('AlertsSettingsWrapper', () => { ...@@ -234,15 +250,10 @@ describe('AlertsSettingsWrapper', () => {
}); });
it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => { it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } }, data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } },
}); });
wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', { findAlertsSettingsForm().vm.$emit('create-new-integration', {
type: typeSet.prometheus, type: typeSet.prometheus,
variables: createPrometheusVariables, variables: createPrometheusVariables,
}); });
...@@ -257,14 +268,18 @@ describe('AlertsSettingsWrapper', () => { ...@@ -257,14 +268,18 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => { it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => {
createComponent({ createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[3] }, data: {
integrations: { list: mockIntegrations },
currentIntegration: mockIntegrations[3],
formVisible: true,
},
loading: false, loading: false,
}); });
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } }, data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } },
}); });
wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', { findAlertsSettingsForm().vm.$emit('update-integration', {
type: typeSet.prometheus, type: typeSet.prometheus,
variables: updatePrometheusVariables, variables: updatePrometheusVariables,
}); });
...@@ -276,15 +291,10 @@ describe('AlertsSettingsWrapper', () => { ...@@ -276,15 +291,10 @@ describe('AlertsSettingsWrapper', () => {
}); });
it('calls `$apollo.mutate` with `resetPrometheusTokenMutation`', () => { it('calls `$apollo.mutate` with `resetPrometheusTokenMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { resetPrometheusTokenMutation: { integration: { id: '1' } } }, data: { resetPrometheusTokenMutation: { integration: { id: '1' } } },
}); });
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { findAlertsSettingsForm().vm.$emit('reset-token', {
type: typeSet.prometheus, type: typeSet.prometheus,
variables: { id: PROMETHEUS_ID }, variables: { id: PROMETHEUS_ID },
}); });
...@@ -298,13 +308,8 @@ describe('AlertsSettingsWrapper', () => { ...@@ -298,13 +308,8 @@ describe('AlertsSettingsWrapper', () => {
}); });
it('shows an error alert when integration creation fails ', async () => { it('shows an error alert when integration creation fails ', async () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR);
wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {}); findAlertsSettingsForm().vm.$emit('create-new-integration', {});
await waitForPromises(); await waitForPromises();
...@@ -312,28 +317,18 @@ describe('AlertsSettingsWrapper', () => { ...@@ -312,28 +317,18 @@ describe('AlertsSettingsWrapper', () => {
}); });
it('shows an error alert when integration token reset fails ', async () => { it('shows an error alert when integration token reset fails ', async () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR);
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {}); findAlertsSettingsForm().vm.$emit('reset-token', {});
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR }); expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
}); });
it('shows an error alert when integration update fails ', async () => { it('shows an error alert when integration update fails ', async () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {}); findAlertsSettingsForm().vm.$emit('update-integration', {});
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR }); expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
...@@ -342,12 +337,7 @@ describe('AlertsSettingsWrapper', () => { ...@@ -342,12 +337,7 @@ describe('AlertsSettingsWrapper', () => {
it('shows an error alert when integration test payload fails ', async () => { it('shows an error alert when integration test payload fails ', async () => {
const mock = new AxiosMockAdapter(axios); const mock = new AxiosMockAdapter(axios);
mock.onPost(/(.*)/).replyOnce(403); mock.onPost(/(.*)/).replyOnce(403);
createComponent({ return wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
return wrapper.vm.validateAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createFlash).toHaveBeenCalledTimes(1); expect(createFlash).toHaveBeenCalledTimes(1);
mock.restore(); mock.restore();
...@@ -389,6 +379,34 @@ describe('AlertsSettingsWrapper', () => { ...@@ -389,6 +379,34 @@ describe('AlertsSettingsWrapper', () => {
variables: mockIntegrations[3], variables: mockIntegrations[3],
}); });
}); });
describe('Test alert', () => {
it('makes `updateTestAlert` service call', async () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockResolvedValueOnce();
const testPayload = '{"title":"test"}';
findAlertsSettingsForm().vm.$emit('test-alert-payload', testPayload);
expect(alertsUpdateService.updateTestAlert).toHaveBeenCalledWith(testPayload);
});
it('shows success message on successful test', async () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockResolvedValueOnce({});
findAlertsSettingsForm().vm.$emit('test-alert-payload', '');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: i18n.alertSent,
type: FLASH_TYPES.SUCCESS,
});
});
it('shows error message when test alert fails', async () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockRejectedValueOnce({});
findAlertsSettingsForm().vm.$emit('test-alert-payload', '');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: INTEGRATION_PAYLOAD_TEST_ERROR,
});
});
});
}); });
describe('with mocked Apollo client', () => { describe('with mocked Apollo client', () => {
......
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