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>
import {
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormSelect,
......@@ -11,10 +10,11 @@ import {
GlModal,
GlModalDirective,
GlToggle,
GlTabs,
GlTab,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { isEmpty, omit } from 'lodash';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
......@@ -22,74 +22,13 @@ import {
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
typeSet,
i18n,
} from '../constants';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql';
import MappingBuilder from './alert_mapping_builder.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 {
placeholders: {
prometheus: targetPrometheusUrlPlaceholder,
......@@ -100,7 +39,6 @@ export default {
components: {
ClipboardButton,
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormInput,
......@@ -109,6 +47,8 @@ export default {
GlFormSelect,
GlModal,
GlToggle,
GlTabs,
GlTab,
AlertSettingsFormHelpBlock,
MappingBuilder,
},
......@@ -155,8 +95,11 @@ export default {
integrationTypesOptions: Object.values(integrationTypes),
selectedIntegration: integrationTypes.none.value,
active: false,
formVisible: false,
integrationTestPayload: {
samplePayload: {
json: null,
error: null,
},
testPayload: {
json: null,
error: null,
},
......@@ -171,8 +114,17 @@ export default {
isPrometheus() {
return this.selectedIntegration === this.$options.typeSet.prometheus;
},
jsonIsValid() {
return this.integrationTestPayload.error === null;
isHttp() {
return this.selectedIntegration === this.$options.typeSet.http;
},
isCreating() {
return !this.currentIntegration;
},
isSampePayloadValid() {
return this.samplePayload.error === null;
},
isTestPayloadValid() {
return this.testPayload.error === null;
},
selectedIntegrationType() {
switch (this.selectedIntegration) {
......@@ -199,7 +151,7 @@ export default {
},
testAlertPayload() {
return {
data: this.integrationTestPayload.json,
data: this.testPayload.json,
endpoint: this.integrationForm.url,
token: this.integrationForm.token,
};
......@@ -208,7 +160,7 @@ export default {
return (
this.multiIntegrations &&
this.glFeatures.multipleHttpIntegrationsCustomMapping &&
this.selectedIntegration === typeSet.http &&
this.isHttp &&
this.alertFields?.length
);
},
......@@ -227,18 +179,11 @@ export default {
: !this.active;
},
isSubmitTestPayloadDisabled() {
return (
!this.active ||
Boolean(this.integrationTestPayload.error) ||
this.integrationTestPayload.json === ''
);
return !this.active || Boolean(this.samplePayload.error) || this.samplePayload.json === '';
},
isSelectDisabled() {
return this.currentIntegration !== null || !this.canAddIntegration;
},
savedMapping() {
return this.mapping;
},
},
watch: {
currentIntegration(val) {
......@@ -250,21 +195,14 @@ export default {
this.selectedIntegration = type;
this.active = active;
if (type === typeSet.prometheus) {
this.integrationTestPayload.json = null;
}
if (type === typeSet.http && this.showMappingBuilder) {
this.parsedPayload = payloadAlertFields;
this.integrationTestPayload.json = this.isValidNonEmptyJSON(payloadExample)
? payloadExample
: null;
this.samplePayload.json = this.isValidNonEmptyJSON(payloadExample) ? payloadExample : null;
const mapping = payloadAttributeMappings.map((mappingItem) =>
omit(mappingItem, '__typename'),
);
this.updateMapping(mapping);
}
this.toggleFormVisibility();
},
},
methods: {
......@@ -280,19 +218,15 @@ export default {
}
return false;
},
toggleFormVisibility() {
this.formVisible = this.selectedIntegration !== integrationTypes.none.value;
},
submitWithTestPayload() {
this.$emit('set-test-alert-payload', this.testAlertPayload);
this.submit();
sendTestAlert() {
this.$emit('test-alert-payload', this.testAlertPayload);
},
submit() {
const { name, apiUrl } = this.integrationForm;
const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
? {
payloadAttributeMappings: this.mapping,
payloadExample: this.integrationTestPayload.json || '{}',
payloadExample: this.samplePayload.json || '{}',
}
: {};
......@@ -300,7 +234,9 @@ export default {
this.selectedIntegration === typeSet.http
? { name, active: this.active, ...customMappingVariables }
: { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables };
if (this.currentIntegration) {
return this.$emit('update-integration', integrationPayload);
}
......@@ -309,20 +245,15 @@ export default {
return this.$emit('create-new-integration', integrationPayload);
},
reset() {
this.selectedIntegration = integrationTypes.none.value;
this.toggleFormVisibility();
this.resetFormValues();
this.resetPayloadAndMapping();
if (this.currentIntegration) {
return this.$emit('clear-current-integration', { type: this.currentIntegration.type });
}
return this.resetFormValues();
this.$emit('clear-current-integration', { type: this.currentIntegration?.type });
},
resetFormValues() {
this.selectedIntegration = integrationTypes.none.value;
this.integrationForm.name = '';
this.integrationForm.apiUrl = '';
this.integrationTestPayload = {
this.samplePayload = {
json: null,
error: null,
};
......@@ -338,16 +269,18 @@ export default {
variables: { id: this.currentIntegration.id },
});
},
validateJson() {
this.integrationTestPayload.error = null;
if (this.integrationTestPayload.json === '') {
validateJson(isSamplePayload = true) {
const payload = isSamplePayload ? this.samplePayload : this.testPayload;
payload.error = null;
if (payload.json === '') {
return;
}
try {
JSON.parse(this.integrationTestPayload.json);
JSON.parse(payload.json);
} catch (e) {
this.integrationTestPayload.error = JSON.stringify(e.message);
payload.error = JSON.stringify(e.message);
}
},
parseMapping() {
......@@ -356,7 +289,7 @@ export default {
return this.$apollo
.query({
query: parseSamplePayloadQuery,
variables: { projectPath: this.projectPath, payload: this.integrationTestPayload.json },
variables: { projectPath: this.projectPath, payload: this.samplePayload.json },
})
.then(
({
......@@ -367,11 +300,13 @@ export default {
this.parsedPayload = alertManagementPayloadFields;
this.resetPayloadAndMappingConfirmed = false;
this.$toast.show(this.$options.i18n.integrationFormSteps.step4.payloadParsedSucessMsg);
this.$toast.show(
this.$options.i18n.integrationFormSteps.setSamplePayload.payloadParsedSucessMsg,
);
},
)
.catch(({ message }) => {
this.integrationTestPayload.error = message;
this.samplePayload.error = message;
})
.finally(() => {
this.parsingPayload = false;
......@@ -391,55 +326,49 @@ export default {
<template>
<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-form-group
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-tabs>
<gl-tab :title="$options.i18n.integrationTabs.configureDetails">
<gl-form-group
id="name-integration"
:label="$options.i18n.integrationFormSteps.step2.label"
label-for="name-integration"
v-if="isCreating"
id="integration-type"
:label="$options.i18n.integrationFormSteps.selectType.label"
label-for="integration-type"
>
<gl-form-input
v-model="integrationForm.name"
:disabled="isPrometheus"
type="text"
:placeholder="
isPrometheus
? $options.i18n.integrationFormSteps.step2.prometheus
: $options.i18n.integrationFormSteps.step2.placeholder
"
<gl-form-select
v-model="selectedIntegration"
:disabled="isSelectDisabled"
class="gl-max-w-full"
:options="integrationTypesOptions"
/>
<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
id="integration-webhook"
:label="$options.i18n.integrationFormSteps.step3.label"
label-for="integration-webhook"
>
<div class="gl-mt-3">
<gl-form-group
v-if="isHttp"
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
:message="
isPrometheus
? $options.i18n.integrationFormSteps.step3.prometheusHelp
: $options.i18n.integrationFormSteps.step3.help
? $options.i18n.integrationFormSteps.setupCredentials.prometheusHelp
: $options.i18n.integrationFormSteps.setupCredentials.help
"
link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
/>
......@@ -447,7 +376,7 @@ export default {
<gl-toggle
v-model="active"
:is-loading="loading"
:label="__('Active')"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
class="gl-my-4 gl-font-weight-normal"
/>
......@@ -468,16 +397,110 @@ export default {
</span>
</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">
<span class="gl-font-weight-bold">
{{ s__('AlertSettings|Webhook URL') }}
{{ $options.i18n.integrationFormSteps.setupCredentials.webhookUrl }}
</span>
<gl-form-input-group id="url" readonly :value="integrationForm.url">
<template #append>
<clipboard-button
:text="integrationForm.url || ''"
:title="__('Copy')"
:title="$options.i18n.copy"
class="gl-m-0!"
/>
</template>
......@@ -486,7 +509,7 @@ export default {
<div class="gl-my-4">
<span class="gl-font-weight-bold">
{{ $options.i18n.integrationFormSteps.step3.info }}
{{ $options.i18n.integrationFormSteps.setupCredentials.authorizationKey }}
</span>
<gl-form-input-group
......@@ -498,124 +521,67 @@ export default {
<template #append>
<clipboard-button
:text="integrationForm.token || ''"
:title="__('Copy')"
:title="$options.i18n.copy"
class="gl-m-0!"
/>
</template>
</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>
</gl-form-group>
<gl-form-group
id="test-integration"
:label="$options.i18n.integrationFormSteps.step4.label"
label-for="test-integration"
:class="{ 'gl-mb-0!': showMappingBuilder }"
:invalid-feedback="integrationTestPayload.error"
<gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled" variant="danger">
{{ $options.i18n.integrationFormSteps.setupCredentials.reset }}
</gl-button>
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
$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
:message="
isPrometheus || !showMappingBuilder
? $options.i18n.integrationFormSteps.step4.prometheusHelp
: $options.i18n.integrationFormSteps.step4.help
"
:message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelp"
:link="generic.alertsUsageUrl"
/>
<gl-form-textarea
id="test-payload"
v-model.trim="integrationTestPayload.json"
:disabled="isPayloadEditDisabled"
:state="jsonIsValid"
:placeholder="$options.i18n.integrationFormSteps.step4.placeholder"
v-model.trim="testPayload.json"
:state="isTestPayloadValid"
:placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
@input="validateJson"
@input="validateJson(false)"
/>
</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
type="submit"
variant="success"
:disabled="!isTestPayloadValid"
data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable"
data-testid="integration-form-submit"
>{{ 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
@click="sendTestAlert"
>
<gl-button type="reset" class="js-no-auto-disable">{{ __('Cancel') }}</gl-button>
</div>
</gl-collapse>
{{ $options.i18n.send }}
</gl-button>
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
$options.i18n.cancelAndClose
}}</gl-button>
</gl-tab>
</gl-tabs>
</gl-form>
</template>
<script>
import { GlButton } from '@gitlab/ui';
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 createFlash, { FLASH_TYPES } from '~/flash';
......@@ -31,20 +32,24 @@ import {
import IntegrationsList from './alerts_integrations_list.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 {
typeSet,
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.',
),
},
i18n,
components: {
IntegrationsList,
AlertSettingsForm,
GlButton,
},
inject: {
generic: {
......@@ -116,10 +121,10 @@ export default {
data() {
return {
isUpdating: false,
testAlertPayload: null,
integrations: {},
httpIntegrations: {},
currentIntegration: null,
formVisible: false,
};
},
computed: {
......@@ -161,18 +166,6 @@ export default {
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({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
......@@ -203,10 +196,6 @@ export default {
return createFlash({ message: error });
}
if (this.testAlertPayload) {
return this.validateAlertPayload();
}
this.clearCurrentIntegration({ type });
return createFlash({
......@@ -219,7 +208,6 @@ export default {
})
.finally(() => {
this.isUpdating = false;
this.testAlertPayload = null;
});
},
resetToken({ type, variables }) {
......@@ -276,6 +264,7 @@ export default {
: updateCurrentPrometheusIntegrationMutation,
variables: currentIntegration,
});
this.setFormVisibility(true);
},
deleteIntegration({ id, type }) {
const { projectPath } = this;
......@@ -308,19 +297,19 @@ export default {
});
},
clearCurrentIntegration({ type }) {
this.$apollo.mutate({
mutation: this.isHttp(type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: {},
});
},
setTestAlertPayload(payload) {
this.testAlertPayload = payload;
if (type) {
this.$apollo.mutate({
mutation: this.isHttp(type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: {},
});
}
this.setFormVisibility(false);
},
validateAlertPayload(payload) {
testAlertPayload(payload) {
return service
.updateTestAlert(payload ?? this.testAlertPayload)
.updateTestAlert(payload)
.then(() => {
return createFlash({
message: this.$options.i18n.alertSent,
......@@ -331,6 +320,9 @@ export default {
createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
});
},
setFormVisibility(visible) {
this.formVisible = visible;
},
},
};
</script>
......@@ -343,7 +335,18 @@ export default {
@edit-integration="editIntegration"
@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
v-if="formVisible"
:loading="isUpdating"
:can-add-integration="canAddIntegration"
:alert-fields="alertFields"
......@@ -351,7 +354,7 @@ export default {
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
@set-test-alert-payload="setTestAlertPayload"
@test-alert-payload="testAlertPayload"
/>
</div>
</template>
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
// TODO: Remove this as part of the form old removal
export const i18n = {
......@@ -38,6 +38,74 @@ export const i18n = {
'AlertSettings|Authorization key has been successfully reset. Please save your changes now.',
),
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 = {
......
......@@ -17,5 +17,5 @@ export const RESET_INTEGRATION_TOKEN_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 ""
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 ""
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"
msgstr ""
......@@ -2844,7 +2829,7 @@ msgstr ""
msgid "AlertSettings|Add URL and auth key to your Prometheus config file"
msgstr ""
msgid "AlertSettings|Add new integrations"
msgid "AlertSettings|Add new integration"
msgstr ""
msgid "AlertSettings|Alert test payload"
......@@ -2856,6 +2841,9 @@ msgstr ""
msgid "AlertSettings|Authorization key has been successfully reset. Please save your changes now."
msgstr ""
msgid "AlertSettings|Configure details"
msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
......@@ -2892,19 +2880,25 @@ msgstr ""
msgid "AlertSettings|Learn more about our our upcoming %{linkStart}integrations%{linkEnd}"
msgstr ""
msgid "AlertSettings|Proceed with editing"
msgid "AlertSettings|Map fields (optional)"
msgstr ""
msgid "AlertSettings|Prometheus"
msgid "AlertSettings|Name integration"
msgstr ""
msgid "AlertSettings|Parse payload for custom mapping"
msgstr ""
msgid "AlertSettings|Proceed with editing"
msgstr ""
msgid "AlertSettings|Prometheus API base URL"
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 ""
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 ""
msgid "AlertSettings|Reset Key"
......@@ -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."
msgstr ""
msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
msgid "AlertSettings|Sample alert payload (optional)"
msgstr ""
msgid "AlertSettings|Save and test payload"
msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
msgstr ""
msgid "AlertSettings|Save integration"
......@@ -2934,7 +2928,10 @@ msgstr ""
msgid "AlertSettings|Select integration type"
msgstr ""
msgid "AlertSettings|Submit payload"
msgid "AlertSettings|Send"
msgstr ""
msgid "AlertSettings|Send test alert"
msgstr ""
msgid "AlertSettings|Test alert payload"
......@@ -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."
msgstr ""
msgid "AlertSettings|View credentials"
msgstr ""
msgid "AlertSettings|Webhook URL"
msgstr ""
......@@ -2991,7 +2991,7 @@ msgstr ""
msgid "AlertsIntegrations|Integration Name"
msgstr ""
msgid "AlertsIntegrations|Integration payload is invalid. You can still save your changes."
msgid "AlertsIntegrations|Integration payload is invalid."
msgstr ""
msgid "AlertsIntegrations|No integrations have been added yet"
......@@ -5418,6 +5418,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Cancel and close"
msgstr ""
msgid "Cancel index deletion"
msgstr ""
......
......@@ -30,8 +30,8 @@ RSpec.describe 'Alert integrations settings form', :js do
end
end
it 'shows the new alerts setting form' do
expect(page).to have_content('1. Select integration type')
it 'shows the integrations list title' do
expect(page).to have_content('Current integrations')
end
end
end
......@@ -44,7 +44,7 @@ RSpec.describe 'Alert integrations settings form', :js do
wait_for_requests
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('#js-alert-management-settings')
end
......
......@@ -4,113 +4,152 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
<form
class="gl-mt-6"
>
<h5
class="gl-font-lg gl-my-5"
>
Add new integrations
</h5>
<div
class="form-group gl-form-group"
id="integration-type"
role="group"
class="tabs gl-tabs"
id="__BVID__6"
>
<label
class="d-block col-form-label"
for="integration-type"
id="integration-type__BV_label_"
>
1. Select integration type
</label>
<!---->
<div
class="bv-no-focus-ring"
class=""
>
<select
class="gl-form-select mw-100 custom-select"
id="__BVID__8"
<ul
class="nav gl-tabs-nav"
id="__BVID__6__BV_tab_controls_"
role="tablist"
>
<option
value=""
<!---->
<li
class="nav-item"
role="presentation"
>
Select integration type
</option>
<option
value="HTTP"
<a
aria-controls="__BVID__8"
aria-posinset="1"
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
</option>
<option
value="PROMETHEUS"
<a
aria-controls="__BVID__22"
aria-disabled="true"
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
</option>
</select>
<!---->
<!---->
<!---->
<!---->
<a
aria-controls="__BVID__41"
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>
<transition-stub
class="gl-mt-3"
css="true"
enteractiveclass="collapsing"
enterclass=""
entertoclass="collapse show"
leaveactiveclass="collapsing"
leaveclass="collapse show"
leavetoclass="collapse"
>
<div
class="collapse"
id="__BVID__10"
style="display: none;"
class="tab-content gl-tab-content"
id="__BVID__6__BV_tab_container_"
>
<div>
<transition-stub
css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
leaveactiveclass=""
leaveclass="show"
leavetoclass=""
mode="out-in"
name=""
>
<div
class="form-group gl-form-group"
id="name-integration"
role="group"
aria-hidden="false"
aria-labelledby="__BVID__8___BV_tab_button__"
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
class="bv-no-focus-ring"
class="form-group gl-form-group"
id="integration-type"
role="group"
>
<input
class="gl-form-input form-control"
id="__BVID__15"
placeholder="Enter integration name"
type="text"
/>
<!---->
<!---->
<!---->
<label
class="d-block col-form-label"
for="integration-type"
id="integration-type__BV_label_"
>
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
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
class="bv-no-focus-ring"
class="gl-mt-3"
>
<!---->
<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
<a
......@@ -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
class="gl-font-weight-bold"
class="gl-button-text"
>
Webhook URL
Save integration
</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
id="url"
readonly="readonly"
class="gl-my-4"
>
<span
class="gl-font-weight-bold"
>
Webhook URL
</span>
<div
class="input-group"
role="group"
id="url"
readonly="readonly"
>
<!---->
<!---->
<input
class="gl-form-input form-control"
id="url"
readonly="readonly"
type="text"
/>
<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=""
title="Copy"
type="button"
<!---->
<!---->
<input
class="gl-form-input form-control"
id="url"
readonly="readonly"
type="text"
/>
<div
class="input-group-append"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
<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=""
title="Copy"
type="button"
>
<use
href="#copy-to-clipboard"
/>
</svg>
<!---->
</button>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
>
<use
href="#copy-to-clipboard"
/>
</svg>
<!---->
</button>
</div>
<!---->
</div>
<!---->
</div>
</div>
</div>
<div
class="gl-my-4"
>
<span
class="gl-font-weight-bold"
>
Authorization key
</span>
<div
class="gl-mb-3"
id="authorization-key"
readonly="readonly"
class="gl-my-4"
>
<span
class="gl-font-weight-bold"
>
Authorization key
</span>
<div
class="input-group"
role="group"
class="gl-mb-3"
id="authorization-key"
readonly="readonly"
>
<!---->
<!---->
<input
class="gl-form-input form-control"
id="authorization-key"
readonly="readonly"
type="text"
/>
<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=""
title="Copy"
type="button"
<!---->
<!---->
<input
class="gl-form-input form-control"
id="authorization-key"
readonly="readonly"
type="text"
/>
<div
class="input-group-append"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
<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=""
title="Copy"
type="button"
>
<use
href="#copy-to-clipboard"
/>
</svg>
<!---->
</button>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
>
<use
href="#copy-to-clipboard"
/>
</svg>
<!---->
</button>
</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>
</div>
<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"
</fieldset>
<button
class="btn btn-danger btn-md disabled gl-button"
disabled="disabled"
type="button"
>
<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
class="gl-display-flex gl-justify-content-start gl-py-3"
<transition-stub
css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
leaveactiveclass=""
leaveclass="show"
leavetoclass=""
mode="out-in"
name=""
>
<button
class="btn js-no-auto-disable btn-success btn-md gl-button"
data-testid="integration-form-submit"
type="submit"
<div
aria-hidden="true"
aria-labelledby="__BVID__41___BV_tab_button__"
class="tab-pane disabled"
id="__BVID__41"
role="tabpanel"
style="display: none;"
>
<!---->
<!---->
<span
class="gl-button-text"
<fieldset
class="form-group gl-form-group"
id="test-integration"
>
Save integration
</span>
</button>
<button
class="btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary"
data-testid="integration-test-and-submit"
disabled="disabled"
type="button"
>
<!---->
<!---->
<div
class="bv-no-focus-ring"
role="group"
tabindex="-1"
>
<span>
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.
</span>
<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>
<!---->
<span
class="gl-button-text"
<button
class="btn js-no-auto-disable btn-confirm btn-md gl-button"
data-testid="send-test-alert"
type="button"
>
Save and test payload
</span>
</button>
<button
class="btn js-no-auto-disable btn-default btn-md gl-button"
type="reset"
>
<!---->
<!---->
<!---->
<span
class="gl-button-text"
>
Send
</span>
</button>
<!---->
<span
class="gl-button-text"
<button
class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
type="reset"
>
Cancel
</span>
</button>
</div>
<!---->
<!---->
<span
class="gl-button-text"
>
Cancel and close
</span>
</button>
</div>
</transition-stub>
<!---->
</div>
</transition-stub>
</div>
</form>
`;
import {
GlForm,
GlFormSelect,
GlCollapse,
GlFormInput,
GlToggle,
GlFormTextarea,
} from '@gitlab/ui';
import { GlForm, GlFormSelect, GlFormInput, GlToggle, GlFormTextarea, GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
......@@ -52,18 +45,18 @@ describe('AlertsSettingsForm', () => {
const findForm = () => wrapper.find(GlForm);
const findSelect = () => wrapper.find(GlFormSelect);
const findFormSteps = () => wrapper.find(GlCollapse);
const findFormFields = () => wrapper.findAll(GlFormInput);
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 findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
const findMultiSupportText = () =>
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 findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`);
const findTabs = () => wrapper.findAll(GlTab);
afterEach(() => {
if (wrapper) {
......@@ -95,7 +88,7 @@ describe('AlertsSettingsForm', () => {
expect(findForm().exists()).toBe(true);
expect(findSelect().exists()).toBe(true);
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 () => {
......@@ -110,11 +103,40 @@ describe('AlertsSettingsForm', () => {
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();
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', () => {
describe('PROMETHEUS', () => {
it('create', async () => {
createComponent();
await selectOptionAtIndex(2);
const apiUrl = 'https://test.com';
enableIntegration(1, apiUrl);
findFormToggle().trigger('click');
enableIntegration(0, apiUrl);
const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true);
expect(submitBtn.text()).toBe('Save integration');
......@@ -226,7 +243,7 @@ describe('AlertsSettingsForm', () => {
});
const apiUrl = 'https://test-post.com';
enableIntegration(1, apiUrl);
enableIntegration(0, apiUrl);
const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true);
......@@ -264,7 +281,7 @@ describe('AlertsSettingsForm', () => {
const jsonTestSubmit = findJsonTestSubmit();
expect(jsonTestSubmit.exists()).toBe(true);
expect(jsonTestSubmit.text()).toBe('Save and test payload');
expect(jsonTestSubmit.text()).toBe('Send');
expect(jsonTestSubmit.props('disabled')).toBe(true);
});
......@@ -313,16 +330,14 @@ describe('AlertsSettingsForm', () => {
it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => {
wrapper.setData({
currentIntegration: {
type: typeSet.http,
payloadExample: validSamplePayload,
payloadAttributeMappings: [],
},
selectedIntegration: typeSet.http,
active,
resetPayloadAndMappingConfirmed,
});
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', () => {
describe.each`
resetPayloadAndMappingConfirmed | payloadExample | caption
${false} | ${validSamplePayload} | ${'Edit payload'}
${true} | ${emptySamplePayload} | ${'Submit payload'}
${true} | ${validSamplePayload} | ${'Submit payload'}
${false} | ${emptySamplePayload} | ${'Submit payload'}
${true} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
${true} | ${validSamplePayload} | ${'Parse payload for custom mapping'}
${false} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
`('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
const payloadResetMsg = resetPayloadAndMappingConfirmed
......@@ -386,7 +401,7 @@ describe('AlertsSettingsForm', () => {
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';
import waitForPromises from 'helpers/wait_for_promises';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.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 createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_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
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 getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
import alertsUpdateService from '~/alerts_settings/services';
import {
ADD_INTEGRATION_ERROR,
RESET_INTEGRATION_TOKEN_ERROR,
......@@ -26,7 +29,7 @@ import {
INTEGRATION_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
import createFlash from '~/flash';
import createFlash, { FLASH_TYPES } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
createHttpVariables,
......@@ -78,6 +81,8 @@ describe('AlertsSettingsWrapper', () => {
const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon);
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
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) {
await jest.runOnlyPendingTimers();
......@@ -144,14 +149,37 @@ describe('AlertsSettingsWrapper', () => {
wrapper = null;
});
describe('rendered via default permissions', () => {
it('renders the GraphQL alerts integrations list and new form', () => {
createComponent();
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
expect(wrapper.find(AlertsSettingsForm).exists()).toBe(true);
describe('template', () => {
beforeEach(() => {
createComponent({
data: {
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({
data: { integrations: {} },
loading: true,
......@@ -159,26 +187,24 @@ describe('AlertsSettingsWrapper', () => {
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
expect(findLoader().exists()).toBe(true);
});
});
it('renders the IntegrationsList table using the API data', () => {
describe('Integration updates', () => {
beforeEach(() => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
data: {
integrations: { list: mockIntegrations },
currentIntegration: mockIntegrations[0],
formVisible: true,
},
loading: false,
});
expect(findLoader().exists()).toBe(false);
expect(findIntegrations()).toHaveLength(mockIntegrations.length);
});
it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
});
wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {
findAlertsSettingsForm().vm.$emit('create-new-integration', {
type: typeSet.http,
variables: createHttpVariables,
});
......@@ -192,15 +218,10 @@ describe('AlertsSettingsWrapper', () => {
});
it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updateHttpIntegrationMutation: { integration: { id: '1' } } },
});
wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {
findAlertsSettingsForm().vm.$emit('update-integration', {
type: typeSet.http,
variables: updateHttpVariables,
});
......@@ -212,15 +233,10 @@ describe('AlertsSettingsWrapper', () => {
});
it('calls `$apollo.mutate` with `resetHttpTokenMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { resetHttpTokenMutation: { integration: { id: '1' } } },
});
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
findAlertsSettingsForm().vm.$emit('reset-token', {
type: typeSet.http,
variables: { id: HTTP_ID },
});
......@@ -234,15 +250,10 @@ describe('AlertsSettingsWrapper', () => {
});
it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } },
});
wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {
findAlertsSettingsForm().vm.$emit('create-new-integration', {
type: typeSet.prometheus,
variables: createPrometheusVariables,
});
......@@ -257,14 +268,18 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[3] },
data: {
integrations: { list: mockIntegrations },
currentIntegration: mockIntegrations[3],
formVisible: true,
},
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } },
});
wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {
findAlertsSettingsForm().vm.$emit('update-integration', {
type: typeSet.prometheus,
variables: updatePrometheusVariables,
});
......@@ -276,15 +291,10 @@ describe('AlertsSettingsWrapper', () => {
});
it('calls `$apollo.mutate` with `resetPrometheusTokenMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { resetPrometheusTokenMutation: { integration: { id: '1' } } },
});
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
findAlertsSettingsForm().vm.$emit('reset-token', {
type: typeSet.prometheus,
variables: { id: PROMETHEUS_ID },
});
......@@ -298,13 +308,8 @@ describe('AlertsSettingsWrapper', () => {
});
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);
wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {});
findAlertsSettingsForm().vm.$emit('create-new-integration', {});
await waitForPromises();
......@@ -312,28 +317,18 @@ describe('AlertsSettingsWrapper', () => {
});
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);
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {});
findAlertsSettingsForm().vm.$emit('reset-token', {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
});
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);
wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {});
findAlertsSettingsForm().vm.$emit('update-integration', {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
......@@ -342,12 +337,7 @@ describe('AlertsSettingsWrapper', () => {
it('shows an error alert when integration test payload fails ', async () => {
const mock = new AxiosMockAdapter(axios);
mock.onPost(/(.*)/).replyOnce(403);
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
loading: false,
});
return wrapper.vm.validateAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
return wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createFlash).toHaveBeenCalledTimes(1);
mock.restore();
......@@ -389,6 +379,34 @@ describe('AlertsSettingsWrapper', () => {
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', () => {
......
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