Commit 50f9fc4e authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by David O'Regan

Alert integration UX cleanup - 3rd part

parent bb2bcbd3
......@@ -118,17 +118,17 @@ export default {
<template>
<div class="gl-display-table gl-w-full gl-mt-5">
<div class="gl-display-table-row">
<h5 id="gitlabFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
<h5 id="gitlabFieldsHeader" class="gl-display-table-cell gl-pb-3 gl-pr-3">
{{ $options.i18n.columns.gitlabKeyTitle }}
</h5>
<h5 class="gl-display-table-cell gl-py-3 gl-pr-3">&nbsp;</h5>
<h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
<h5 class="gl-display-table-cell gl-pb-3 gl-pr-3">&nbsp;</h5>
<h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-pb-3 gl-pr-3">
{{ $options.i18n.columns.payloadKeyTitle }}
</h5>
<h5
v-if="hasFallbackColumn"
id="fallbackFieldsHeader"
class="gl-display-table-cell gl-py-3 gl-pr-3"
class="gl-display-table-cell gl-pb-3 gl-pr-3"
>
{{ $options.i18n.columns.fallbackKeyTitle }}
<gl-icon
......@@ -140,11 +140,7 @@ export default {
</h5>
</div>
<div
v-for="(gitlabField, index) in mappingData"
:key="gitlabField.name"
class="gl-display-table-row"
>
<div v-for="gitlabField in mappingData" :key="gitlabField.name" class="gl-display-table-row">
<div class="gl-display-table-cell gl-py-3 gl-pr-3 gl-w-30p gl-vertical-align-middle">
<gl-form-input
aria-labelledby="gitlabFieldsHeader"
......@@ -153,8 +149,8 @@ export default {
/>
</div>
<div class="gl-display-table-cell gl-py-3 gl-pr-3">
<div class="right-arrow" :class="{ 'gl-vertical-align-middle': index === 0 }">
<div class="gl-display-table-cell gl-pr-3 gl-vertical-align-middle">
<div class="right-arrow">
<i class="right-arrow-head"></i>
</div>
</div>
......
......@@ -139,7 +139,7 @@ export default {
<template>
<div class="incident-management-list">
<h5 class="gl-font-lg">{{ $options.i18n.title }}</h5>
<h5 class="gl-font-lg gl-mt-5">{{ $options.i18n.title }}</h5>
<gl-table
class="integration-list"
:items="integrations"
......
......@@ -14,7 +14,7 @@ import {
GlTab,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { isEmpty, omit } from 'lodash';
import { isEqual, isEmpty, omit } from 'lodash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
integrationTypes,
......@@ -24,8 +24,9 @@ import {
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
typeSet,
viewCredentialsTabIndex,
i18n,
tabIndices,
testAlertModalId,
} from '../constants';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql';
......@@ -40,6 +41,10 @@ export default {
typeSet,
integrationSteps,
i18n,
primaryProps: { text: i18n.integrationFormSteps.testPayload.savedAndTest },
secondaryProps: { text: i18n.integrationFormSteps.testPayload.proceedWithoutSave },
cancelProps: { text: i18n.integrationFormSteps.testPayload.cancel },
testAlertModalId,
components: {
ClipboardButton,
GlButton,
......@@ -60,11 +65,8 @@ export default {
GlModal: GlModalDirective,
},
inject: {
generic: {
default: {},
},
prometheus: {
default: {},
alertsUsageUrl: {
default: '#',
},
multiIntegrations: {
default: false,
......@@ -87,6 +89,11 @@ export default {
required: false,
default: null,
},
tabIndex: {
type: Number,
required: false,
default: tabIndices.configureDetails,
},
},
apollo: {
currentIntegration: {
......@@ -96,11 +103,10 @@ export default {
data() {
return {
integrationTypesOptions: Object.values(integrationTypes),
selectedIntegration: integrationTypes.none.value,
active: false,
samplePayload: {
json: null,
error: null,
loading: false,
},
testPayload: {
json: null,
......@@ -108,18 +114,32 @@ export default {
},
resetPayloadAndMappingConfirmed: false,
mapping: [],
parsingPayload: false,
integrationForm: {
active: false,
type: integrationTypes.none.value,
name: '',
token: '',
url: '',
apiUrl: '',
},
activeTabIndex: this.tabIndex,
currentIntegration: null,
parsedPayload: [],
activeTabIndex: 0,
validationState: {
name: true,
apiUrl: true,
},
};
},
computed: {
isPrometheus() {
return this.selectedIntegration === this.$options.typeSet.prometheus;
return this.integrationForm.type === typeSet.prometheus;
},
isHttp() {
return this.selectedIntegration === this.$options.typeSet.http;
return this.integrationForm.type === typeSet.http;
},
isNone() {
return !this.isHttp && !this.isPrometheus;
},
isCreating() {
return !this.currentIntegration;
......@@ -130,29 +150,6 @@ export default {
isTestPayloadValid() {
return this.testPayload.error === null;
},
selectedIntegrationType() {
switch (this.selectedIntegration) {
case typeSet.http:
return this.generic;
case typeSet.prometheus:
return this.prometheus;
default:
return {};
}
},
integrationForm() {
return {
name: this.currentIntegration?.name || '',
active: this.currentIntegration?.active || false,
token:
this.currentIntegration?.token ||
(this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.token : ''),
url:
this.currentIntegration?.url ||
(this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.url : ''),
apiUrl: this.currentIntegration?.apiUrl || '',
};
},
testAlertPayload() {
return {
data: this.testPayload.json,
......@@ -170,13 +167,7 @@ export default {
return this.hasSamplePayload && !this.resetPayloadAndMappingConfirmed;
},
canParseSamplePayload() {
return !this.active || !this.isSampePayloadValid || !this.samplePayload.json;
},
isResetAuthKeyDisabled() {
return !this.active && !this.integrationForm.token !== '';
},
isPayloadEditDisabled() {
return !this.active || this.canEditPayload;
return this.isSampePayloadValid && this.samplePayload.json;
},
isSelectDisabled() {
return this.currentIntegration !== null || !this.canAddIntegration;
......@@ -186,30 +177,101 @@ export default {
? i18n.integrationFormSteps.setupCredentials.prometheusHelp
: i18n.integrationFormSteps.setupCredentials.help;
},
isFormValid() {
return (
Object.values(this.validationState).every(Boolean) &&
!this.isNone &&
this.isSampePayloadValid
);
},
isFormDirty() {
const { type, active, name, apiUrl, payloadAlertFields = [], payloadAttributeMappings = [] } =
this.currentIntegration || {};
const {
name: formName,
apiUrl: formApiUrl,
active: formActive,
type: formType,
} = this.integrationForm;
const isDirty =
type !== formType ||
active !== formActive ||
name !== formName ||
apiUrl !== formApiUrl ||
!isEqual(this.parsedPayload, payloadAlertFields) ||
!isEqual(this.mapping, this.getCleanMapping(payloadAttributeMappings));
return isDirty;
},
canSubmitForm() {
return this.isFormValid && this.isFormDirty;
},
dataForSave() {
const { name, apiUrl, active } = this.integrationForm;
const customMappingVariables = {
payloadAttributeMappings: this.mapping,
payloadExample: this.samplePayload.json || '{}',
};
const variables = this.isHttp
? { name, active, ...customMappingVariables }
: { apiUrl, active };
return { type: this.integrationForm.type, variables };
},
testAlertModal() {
return this.isFormDirty ? testAlertModalId : null;
},
},
watch: {
tabIndex(val) {
this.activeTabIndex = val;
},
currentIntegration(val) {
if (val === null) {
this.reset();
return;
}
const { type, active, payloadExample, payloadAlertFields, payloadAttributeMappings } = val;
this.selectedIntegration = type;
this.active = active;
if (type === typeSet.http && this.showMappingBuilder) {
this.resetPayloadAndMapping();
const {
name,
type,
active,
url,
apiUrl,
token,
payloadExample,
payloadAlertFields,
payloadAttributeMappings,
} = val;
this.integrationForm = { type, name, active, url, apiUrl, token };
if (this.showMappingBuilder) {
this.resetPayloadAndMappingConfirmed = false;
this.parsedPayload = payloadAlertFields;
this.samplePayload.json = this.isValidNonEmptyJSON(payloadExample) ? payloadExample : null;
const mapping = payloadAttributeMappings.map((mappingItem) =>
omit(mappingItem, '__typename'),
);
this.updateMapping(mapping);
this.updateMapping(this.getCleanMapping(payloadAttributeMappings));
}
this.activeTabIndex = viewCredentialsTabIndex;
this.$el.scrollIntoView({ block: 'center' });
},
},
methods: {
getCleanMapping(mapping) {
return mapping.map((mappingItem) => omit(mappingItem, '__typename'));
},
validateName() {
this.validationState.name = Boolean(this.integrationForm.name?.length);
},
validateApiUrl() {
try {
const parsedUrl = new URL(this.integrationForm.apiUrl);
this.validationState.apiUrl = ['http:', 'https:'].includes(parsedUrl.protocol);
} catch (e) {
this.validationState.apiUrl = false;
}
},
isValidNonEmptyJSON(JSONString) {
if (JSONString) {
let parsed;
......@@ -222,29 +284,24 @@ export default {
}
return false;
},
triggerValidation() {
if (this.isHttp) {
this.validationState.apiUrl = true;
this.validateName();
} else if (this.isPrometheus) {
this.validationState.name = true;
this.validateApiUrl();
}
},
sendTestAlert() {
this.$emit('test-alert-payload', this.testAlertPayload);
},
submit() {
const { name, apiUrl } = this.integrationForm;
const customMappingVariables = {
payloadAttributeMappings: this.mapping,
payloadExample: this.samplePayload.json || '{}',
};
const variables =
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);
}
this.reset();
return this.$emit('create-new-integration', integrationPayload);
saveAndSendTestAlert() {
this.$emit('save-and-test-alert-payload', this.dataForSave, this.testAlertPayload);
},
submit(testAfterSubmit = false) {
const event = this.currentIntegration ? 'update-integration' : 'create-new-integration';
this.$emit(event, this.dataForSave, testAfterSubmit);
},
reset() {
this.resetFormValues();
......@@ -252,14 +309,14 @@ export default {
this.$emit('clear-current-integration', { type: this.currentIntegration?.type });
},
resetFormValues() {
this.selectedIntegration = integrationTypes.none.value;
this.integrationForm.type = integrationTypes.none.value;
this.integrationForm.name = '';
this.integrationForm.active = false;
this.integrationForm.apiUrl = '';
this.samplePayload = {
json: null,
error: null,
};
this.active = false;
},
resetAuthKey() {
if (!this.currentIntegration) {
......@@ -267,7 +324,7 @@ export default {
}
this.$emit('reset-token', {
type: this.selectedIntegration,
type: this.integrationForm.type,
variables: { id: this.currentIntegration.id },
});
},
......@@ -285,8 +342,8 @@ export default {
payload.error = JSON.stringify(e.message);
}
},
parseMapping() {
this.parsingPayload = true;
parseSamplePayload() {
this.samplePayload.loading = true;
return this.$apollo
.query({
......@@ -303,7 +360,7 @@ export default {
this.resetPayloadAndMappingConfirmed = false;
this.$toast.show(
this.$options.i18n.integrationFormSteps.setSamplePayload.payloadParsedSucessMsg,
this.$options.i18n.integrationFormSteps.mapFields.payloadParsedSucessMsg,
);
},
)
......@@ -311,7 +368,7 @@ export default {
this.samplePayload.error = message;
})
.finally(() => {
this.parsingPayload = false;
this.samplePayload.loading = false;
});
},
updateMapping(mapping) {
......@@ -338,7 +395,7 @@ export default {
<template>
<gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
<gl-tabs v-model="activeTabIndex">
<gl-tab :title="$options.i18n.integrationTabs.configureDetails">
<gl-tab :title="$options.i18n.integrationTabs.configureDetails" class="gl-mt-3">
<gl-form-group
v-if="isCreating"
id="integration-type"
......@@ -351,10 +408,11 @@ export default {
label-for="integration-type"
>
<gl-form-select
v-model="selectedIntegration"
v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
:options="integrationTypesOptions"
@change="triggerValidation"
/>
<alert-settings-form-help-block
......@@ -369,7 +427,6 @@ export default {
<div class="gl-mt-3">
<gl-form-group
v-if="isHttp"
id="name-integration"
:label="
getLabelWithStepNumber(
$options.integrationSteps.nameIntegration,
......@@ -377,67 +434,81 @@ export default {
)
"
label-for="name-integration"
:invalid-feedback="$options.i18n.integrationFormSteps.nameIntegration.error"
:state="validationState.name"
>
<gl-form-input
id="name-integration"
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
@input="validateName"
/>
</gl-form-group>
<gl-form-group
v-if="!isNone"
:label="
getLabelWithStepNumber(
isHttp
? $options.integrationSteps.enableHttpIntegration
: $options.integrationSteps.enablePrometheusIntegration,
$options.i18n.integrationFormSteps.enableIntegration.label,
)
"
>
<span>{{ $options.i18n.integrationFormSteps.enableIntegration.help }}</span>
<gl-toggle
v-model="active"
id="enable-integration"
v-model="integrationForm.active"
:is-loading="loading"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
class="gl-my-4 gl-font-weight-normal"
class="gl-mt-4 gl-font-weight-normal"
/>
</gl-form-group>
<div v-if="isPrometheus" class="gl-my-4">
<span class="gl-font-weight-bold">
{{
getLabelWithStepNumber(
$options.integrationSteps.setPrometheusApiUrl,
$options.i18n.integrationFormSteps.prometheusFormUrl.label,
)
}}
</span>
<gl-form-group
v-if="isPrometheus"
class="gl-my-4"
:label="$options.i18n.integrationFormSteps.prometheusFormUrl.label"
label-for="api-url"
:invalid-feedback="$options.i18n.integrationFormSteps.prometheusFormUrl.error"
:state="validationState.apiUrl"
>
<gl-form-input
id="integration-apiUrl"
id="api-url"
v-model="integrationForm.apiUrl"
type="text"
:placeholder="$options.placeholders.prometheus"
@input="validateApiUrl"
/>
<span class="gl-text-gray-400">
{{ $options.i18n.integrationFormSteps.prometheusFormUrl.help }}
</span>
</div>
</gl-form-group>
<template v-if="showMappingBuilder">
<gl-form-group
data-testid="sample-payload-section"
:label="
getLabelWithStepNumber(
$options.integrationSteps.setSamplePayload,
$options.i18n.integrationFormSteps.setSamplePayload.label,
$options.integrationSteps.customizeMapping,
$options.i18n.integrationFormSteps.mapFields.label,
)
"
label-for="sample-payload"
class="gl-mb-0!"
:invalid-feedback="samplePayload.error"
>
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelpHttp"
:link="generic.alertsUsageUrl"
/>
<span>{{ $options.i18n.integrationFormSteps.mapFields.help }}</span>
<gl-form-textarea
id="sample-payload"
v-model.trim="samplePayload.json"
:disabled="isPayloadEditDisabled"
:disabled="canEditPayload"
:state="isSampePayloadValid"
:placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
:placeholder="$options.i18n.integrationFormSteps.mapFields.placeholder"
class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
......@@ -450,71 +521,76 @@ export default {
v-if="canEditPayload"
v-gl-modal.resetPayloadModal
data-testid="payload-action-btn"
:disabled="!active"
:disabled="!integrationForm.active"
class="gl-mt-3"
>
{{ $options.i18n.integrationFormSteps.setSamplePayload.editPayload }}
{{ $options.i18n.integrationFormSteps.mapFields.editPayload }}
</gl-button>
<gl-button
v-else
data-testid="payload-action-btn"
:class="{ 'gl-mt-3': samplePayload.error }"
:disabled="canParseSamplePayload"
:loading="parsingPayload"
@click="parseMapping"
:disabled="!canParseSamplePayload"
:loading="samplePayload.loading"
@click="parseSamplePayload"
>
{{ $options.i18n.integrationFormSteps.setSamplePayload.parsePayload }}
{{ $options.i18n.integrationFormSteps.mapFields.parsePayload }}
</gl-button>
<gl-modal
modal-id="resetPayloadModal"
:title="$options.i18n.integrationFormSteps.setSamplePayload.resetHeader"
:ok-title="$options.i18n.integrationFormSteps.setSamplePayload.resetOk"
:title="$options.i18n.integrationFormSteps.mapFields.resetHeader"
:ok-title="$options.i18n.integrationFormSteps.mapFields.resetOk"
ok-variant="danger"
@ok="resetPayloadAndMapping"
@ok="resetPayloadAndMappingConfirmed = true"
>
{{ $options.i18n.integrationFormSteps.setSamplePayload.resetBody }}
{{ $options.i18n.integrationFormSteps.mapFields.resetBody }}
</gl-modal>
<gl-form-group
id="mapping-builder"
class="gl-mt-5"
:label="
getLabelWithStepNumber(
$options.integrationSteps.customizeMapping,
$options.i18n.integrationFormSteps.mapFields.label,
)
"
label-for="mapping-builder"
>
<span>{{ $options.i18n.integrationFormSteps.mapFields.intro }}</span>
<div class="gl-mt-5">
<span>{{ $options.i18n.integrationFormSteps.mapFields.mapIntro }}</span>
<mapping-builder
:parsed-payload="parsedPayload"
:saved-mapping="mapping"
:alert-fields="alertFields"
@onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
</template>
</div>
<div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button
type="submit"
:disabled="!canSubmitForm"
variant="confirm"
class="js-no-auto-disable"
data-testid="integration-form-submit"
@click="submit(false)"
>
{{ $options.i18n.saveIntegration }}
</gl-button>
<gl-button
:disabled="!canSubmitForm"
variant="confirm"
category="secondary"
class="gl-ml-3 js-no-auto-disable"
data-testid="integration-form-test-and-submit"
@click="submit(true)"
>
{{ $options.i18n.saveAndTestIntegration }}
</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-tab
:title="$options.i18n.integrationTabs.viewCredentials"
:disabled="isCreating"
class="gl-mt-3"
>
<alert-settings-form-help-block
:message="viewCredentialsHelpMsg"
link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
......@@ -559,13 +635,15 @@ export default {
</div>
</gl-form-group>
<gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled" variant="danger">
<div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button v-gl-modal.authKeyModal 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-button type="reset" class="gl-ml-3 js-no-auto-disable">
{{ $options.i18n.cancelAndClose }}
</gl-button>
</div>
<gl-modal
modal-id="authKeyModal"
......@@ -578,18 +656,22 @@ export default {
</gl-modal>
</gl-tab>
<gl-tab :title="$options.i18n.integrationTabs.sendTestAlert" :disabled="isCreating">
<gl-tab
:title="$options.i18n.integrationTabs.sendTestAlert"
:disabled="isCreating"
class="gl-mt-3"
>
<gl-form-group id="test-integration" :invalid-feedback="testPayload.error">
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelp"
:link="generic.alertsUsageUrl"
:message="$options.i18n.integrationFormSteps.testPayload.help"
:link="alertsUsageUrl"
/>
<gl-form-textarea
id="test-payload"
v-model.trim="testPayload.json"
:state="isTestPayloadValid"
:placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
:placeholder="$options.i18n.integrationFormSteps.testPayload.placeholder"
class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
......@@ -597,20 +679,35 @@ export default {
@input="validateJson(false)"
/>
</gl-form-group>
<div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button
v-gl-modal="testAlertModal"
:disabled="!isTestPayloadValid"
:loading="loading"
data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable"
@click="sendTestAlert"
@click="isFormDirty ? null : sendTestAlert()"
>
{{ $options.i18n.send }}
</gl-button>
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
$options.i18n.cancelAndClose
}}</gl-button>
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
{{ $options.i18n.cancelAndClose }}
</gl-button>
</div>
<gl-modal
:modal-id="$options.testAlertModalId"
:title="$options.i18n.integrationFormSteps.testPayload.modalTitle"
:action-primary="$options.primaryProps"
:action-secondary="$options.secondaryProps"
:action-cancel="$options.cancelProps"
@primary="saveAndSendTestAlert"
@secondary="sendTestAlert"
>
{{ $options.i18n.integrationFormSteps.testPayload.modalBody }}
</gl-modal>
</gl-tab>
</gl-tabs>
</gl-form>
......
<script>
import { GlButton } from '@gitlab/ui';
import { GlButton, GlAlert } 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';
import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale';
import { typeSet } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
import { typeSet, i18n, tabIndices } from '../constants';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
......@@ -28,21 +28,12 @@ import {
RESET_INTEGRATION_TOKEN_ERROR,
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
DEFAULT_ERROR,
} from '../utils/error_messages';
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,
......@@ -50,14 +41,9 @@ export default {
IntegrationsList,
AlertSettingsForm,
GlButton,
GlAlert,
},
inject: {
generic: {
default: {},
},
prometheus: {
default: {},
},
projectPath: {
default: '',
},
......@@ -124,7 +110,10 @@ export default {
integrations: {},
httpIntegrations: {},
currentIntegration: null,
newIntegration: null,
formVisible: false,
showSuccessfulCreateAlert: false,
tabIndex: tabIndices.configureDetails,
};
},
computed: {
......@@ -139,10 +128,10 @@ export default {
isHttp(type) {
return type === typeSet.http;
},
createNewIntegration({ type, variables }) {
createNewIntegration({ type, variables }, testAfterSubmit) {
const { projectPath } = this;
const isHttp = this.isHttp(type);
this.isUpdating = true;
this.$apollo
.mutate({
......@@ -163,16 +152,19 @@ export default {
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
return createFlash({ message: error });
createFlash({ message: error });
return;
}
const { integration } = httpIntegrationCreate || prometheusIntegrationCreate;
this.editIntegration(integration);
const { integration } = httpIntegrationCreate || prometheusIntegrationCreate;
this.newIntegration = integration;
this.showSuccessfulCreateAlert = true;
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
});
if (testAfterSubmit) {
this.viewIntegration(this.newIntegration, tabIndices.sendTestAlert);
} else {
this.setFormVisibility(false);
}
})
.catch(() => {
createFlash({ message: ADD_INTEGRATION_ERROR });
......@@ -181,9 +173,9 @@ export default {
this.isUpdating = false;
});
},
updateIntegration({ type, variables }) {
updateIntegration({ type, variables }, testAfterSubmit) {
this.isUpdating = true;
this.$apollo
return this.$apollo
.mutate({
mutation: this.isHttp(type)
? updateHttpIntegrationMutation
......@@ -196,12 +188,20 @@ export default {
.then(({ data: { httpIntegrationUpdate, prometheusIntegrationUpdate } = {} } = {}) => {
const error = httpIntegrationUpdate?.errors[0] || prometheusIntegrationUpdate?.errors[0];
if (error) {
return createFlash({ message: error });
createFlash({ message: error });
return;
}
this.clearCurrentIntegration({ type });
const integration =
httpIntegrationUpdate?.integration || prometheusIntegrationUpdate?.integration;
return createFlash({
if (testAfterSubmit) {
this.viewIntegration(integration, tabIndices.sendTestAlert);
} else {
this.clearCurrentIntegration(type);
}
createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
});
......@@ -261,13 +261,23 @@ export default {
currentIntegration = { ...currentIntegration, ...httpIntegrationMappingData };
}
this.$apollo.mutate({
mutation: this.isHttp(type)
this.viewIntegration(currentIntegration, tabIndices.viewCredentials);
},
viewIntegration(integration, tabIndex) {
this.$apollo
.mutate({
mutation: this.isHttp(integration.type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: currentIntegration,
});
variables: integration,
})
.then(() => {
this.setFormVisibility(true);
this.tabIndex = tabIndex;
})
.catch(() => {
createFlash({ message: DEFAULT_ERROR });
});
},
deleteIntegration({ id, type }) {
const { projectPath } = this;
......@@ -319,19 +329,44 @@ export default {
type: FLASH_TYPES.SUCCESS,
});
})
.catch(() => {
createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
.catch((error) => {
let message = INTEGRATION_PAYLOAD_TEST_ERROR;
if (error.response?.status === httpStatusCodes.FORBIDDEN) {
message = INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR;
}
createFlash({ message });
});
},
saveAndTestAlertPayload(integration, payload) {
return this.updateIntegration(integration, false).then(() => {
this.testAlertPayload(payload);
});
},
setFormVisibility(visible) {
this.formVisible = visible;
},
viewCreatedIntegration() {
this.viewIntegration(this.newIntegration, tabIndices.viewCredentials);
this.showSuccessfulCreateAlert = false;
this.newIntegration = null;
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showSuccessfulCreateAlert"
class="gl-mt-n2"
:primary-button-text="$options.i18n.integrationCreated.btnCaption"
:title="$options.i18n.integrationCreated.title"
@primaryAction="viewCreatedIntegration"
@dismiss="showSuccessfulCreateAlert = false"
>
{{ $options.i18n.integrationCreated.successMsg }}
</gl-alert>
<integrations-list
:integrations="integrations.list"
:loading="loading"
......@@ -353,11 +388,13 @@ export default {
:loading="isUpdating"
:can-add-integration="canAddIntegration"
:alert-fields="alertFields"
:tab-index="tabIndex"
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
@test-alert-payload="testAlertPayload"
@save-and-test-alert-payload="saveAndTestAlertPayload"
/>
</div>
</template>
......@@ -17,6 +17,13 @@ export const i18n = {
label: s__('AlertSettings|Name integration'),
placeholder: s__('AlertSettings|Enter integration name'),
activeToggle: __('Active'),
error: __("Name can't be blank"),
},
enableIntegration: {
label: s__('AlertSettings|Enable integration'),
help: s__(
'AlertSettings|A webhook URL and authorization key will be generated for the integration. Both will be visible after saving the integration in the “View credentials” tab.',
),
},
setupCredentials: {
help: s__(
......@@ -29,35 +36,41 @@ export const i18n = {
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.',
mapFields: {
label: s__('AlertSettings|Customize alert payload mapping (optional)'),
help: s__(
'AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click the "parse payload fields" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration.',
),
placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
editPayload: s__('AlertSettings|Edit payload'),
parsePayload: s__('AlertSettings|Parse payload fields'),
payloadParsedSucessMsg: s__(
'AlertSettings|Sample payload has been parsed. You can now map the fields.',
),
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.',
mapIntro: s__(
"AlertSettings|The default GitLab alert fields are listed below. If you choose to map your payload keys to GitLab's, please make a selection in the dropdowns below. You may also opt to leave the fields unmapped and move straight to saving your integration.",
),
},
mapFields: {
label: s__('AlertSettings|Customize alert payload mapping (optional)'),
intro: s__(
'AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click "parse payload fields" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration.',
testPayload: {
help: 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" }] }'),
modalTitle: s__('AlertSettings|The form has unsaved changes'),
modalBody: s__('AlertSettings|The form has unsaved changes. How would you like to proceed?'),
savedAndTest: s__('AlertSettings|Save integration & send'),
proceedWithoutSave: s__('AlertSettings|Send without saving'),
cancel: __('Cancel'),
},
prometheusFormUrl: {
label: s__('AlertSettings|Prometheus API base URL'),
help: s__('AlertSettings|URL cannot be blank and must start with http or https'),
error: s__('AlertSettings|URL is invalid.'),
},
restKeyInfo: {
label: s__(
......@@ -66,40 +79,52 @@ export const i18n = {
},
},
saveIntegration: s__('AlertSettings|Save integration'),
changesSaved: s__('AlertSettings|Your integration was successfully updated.'),
saveAndTestIntegration: s__('AlertSettings|Save & create test alert'),
cancelAndClose: __('Cancel and close'),
send: s__('AlertSettings|Send'),
send: __('Send'),
copy: __('Copy'),
integrationCreated: {
title: s__('AlertSettings|Integration successfully saved'),
successMsg: s__(
'AlertSettings|A URL and authorization key have been created for your integration. You will need them to setup a webhook and authorize your endpoint to send alerts to GitLab.',
),
btnCaption: s__('AlertSettings|View URL and authorization key'),
},
changesSaved: s__('AlertsIntegrations|The integration has been successfully saved.'),
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 const integrationSteps = {
selectType: 'SELECT_TYPE',
nameIntegration: 'NAME_INTEGRATION',
setPrometheusApiUrl: 'SET_PROMETHEUS_API_URL',
setSamplePayload: 'SET_SAMPLE_PAYLOAD',
enableHttpIntegration: 'ENABLE_HTTP_INTEGRATION',
enablePrometheusIntegration: 'ENABLE_PROMETHEUS_INTEGRATION',
customizeMapping: 'CUSTOMIZE_MAPPING',
};
export const createStepNumbers = {
[integrationSteps.selectType]: 1,
[integrationSteps.nameIntegration]: 2,
[integrationSteps.setPrometheusApiUrl]: 2,
[integrationSteps.setSamplePayload]: 3,
[integrationSteps.enableHttpIntegration]: 3,
[integrationSteps.enablePrometheusIntegration]: 2,
[integrationSteps.customizeMapping]: 4,
};
export const editStepNumbers = {
[integrationSteps.selectType]: 1,
[integrationSteps.nameIntegration]: 1,
[integrationSteps.setPrometheusApiUrl]: null,
[integrationSteps.setSamplePayload]: 2,
[integrationSteps.enableHttpIntegration]: 2,
[integrationSteps.enablePrometheusIntegration]: null,
[integrationSteps.customizeMapping]: 3,
};
export const integrationTypes = {
none: { value: '', text: s__('AlertSettings|Select integration type') },
http: { value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
prometheus: { value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
prometheus: { value: 'PROMETHEUS', text: s__('AlertSettings|Prometheus') },
};
export const typeSet = {
......@@ -127,4 +152,10 @@ export const mappingFields = {
fallback: 'fallback',
};
export const viewCredentialsTabIndex = 1;
export const tabIndices = {
configureDetails: 0,
viewCredentials: 1,
sendTestAlert: 2,
};
export const testAlertModalId = 'confirmSendTestAlert';
......@@ -19,23 +19,7 @@ export default (el) => {
return null;
}
const {
prometheusActivated,
prometheusUrl,
prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
activated: activatedStr,
alertsSetupUrl,
alertsUsageUrl,
formPath,
authorizationKey,
url,
projectPath,
multiIntegrations,
alertFields,
} = el.dataset;
const { alertsUsageUrl, projectPath, multiIntegrations, alertFields } = el.dataset;
return new Vue({
el,
......@@ -43,22 +27,7 @@ export default (el) => {
AlertSettingsWrapper,
},
provide: {
prometheus: {
active: parseBoolean(prometheusActivated),
url: prometheusUrl,
token: prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
},
generic: {
alertsSetupUrl,
alertsUsageUrl,
active: parseBoolean(activatedStr),
formPath,
token: authorizationKey,
url,
},
projectPath,
multiIntegrations: parseBoolean(multiIntegrations),
},
......
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
export const DELETE_INTEGRATION_ERROR = s__(
'AlertsIntegrations|The integration could not be deleted. Please try again.',
......@@ -19,3 +19,9 @@ export const RESET_INTEGRATION_TOKEN_ERROR = s__(
export const INTEGRATION_PAYLOAD_TEST_ERROR = s__(
'AlertsIntegrations|Integration payload is invalid.',
);
export const INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR = s__(
'AlertsIntegrations|The integration is currently inactive. Enable the integration to send the test alert.',
);
export const DEFAULT_ERROR = __('Something went wrong on our end.');
......@@ -6,7 +6,6 @@ $stroke-size: 1px;
@include gl-relative;
@include gl-w-full;
height: $stroke-size;
@include gl-display-inline-block;
background-color: var(--gray-400, $gray-400);
min-width: $gl-spacing-scale-5;
......
......@@ -6,11 +6,11 @@
%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Alerts')
= _('Alert integrations')
%button.btn.js-settings-toggle{ type: 'button' }
= _('Expand')
%p
= _('Display alerts from all your monitoring tools directly within GitLab.')
= link_to _('More information'), help_page_path('operations/incident_management/index.md'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('More information'), help_page_path('operations/incident_management/integrations.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-alerts-settings{ data: alerts_settings_data }
---
title: Alerts integration form UX cleanup
merge_request: 55892
author:
type: changed
......@@ -2731,6 +2731,9 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
msgid "Alert integrations"
msgstr ""
msgid "AlertManagement|Acknowledged"
msgstr ""
......@@ -2893,6 +2896,12 @@ 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|A URL and authorization key have been created for your integration. You will need them to setup a webhook and authorize your endpoint to send alerts to GitLab."
msgstr ""
msgid "AlertSettings|A webhook URL and authorization key will be generated for the integration. Both will be visible after saving the integration in the “View credentials” tab."
msgstr ""
msgid "AlertSettings|Add new integration"
msgstr ""
......@@ -2911,10 +2920,10 @@ msgstr ""
msgid "AlertSettings|Edit payload"
msgstr ""
msgid "AlertSettings|Enter integration name"
msgid "AlertSettings|Enable integration"
msgstr ""
msgid "AlertSettings|External Prometheus"
msgid "AlertSettings|Enter integration name"
msgstr ""
msgid "AlertSettings|HTTP Endpoint"
......@@ -2923,25 +2932,28 @@ msgstr ""
msgid "AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields."
msgstr ""
msgid "AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click \"parse payload fields\" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration."
msgid "AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click the \"parse payload fields\" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration."
msgstr ""
msgid "AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations."
msgstr ""
msgid "AlertSettings|Integration successfully saved"
msgstr ""
msgid "AlertSettings|Name integration"
msgstr ""
msgid "AlertSettings|Parse payload for custom mapping"
msgid "AlertSettings|Parse payload fields"
msgstr ""
msgid "AlertSettings|Proceed with editing"
msgstr ""
msgid "AlertSettings|Prometheus API base URL"
msgid "AlertSettings|Prometheus"
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)."
msgid "AlertSettings|Prometheus API base URL"
msgstr ""
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."
......@@ -2956,33 +2968,51 @@ msgstr ""
msgid "AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in."
msgstr ""
msgid "AlertSettings|Sample alert payload (optional)"
msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
msgstr ""
msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
msgid "AlertSettings|Save & create test alert"
msgstr ""
msgid "AlertSettings|Save integration"
msgstr ""
msgid "AlertSettings|Select integration type"
msgid "AlertSettings|Save integration & send"
msgstr ""
msgid "AlertSettings|Send"
msgid "AlertSettings|Select integration type"
msgstr ""
msgid "AlertSettings|Send test alert"
msgstr ""
msgid "AlertSettings|Send without saving"
msgstr ""
msgid "AlertSettings|The default GitLab alert fields are listed below. If you choose to map your payload keys to GitLab's, please make a selection in the dropdowns below. You may also opt to leave the fields unmapped and move straight to saving your integration."
msgstr ""
msgid "AlertSettings|The form has unsaved changes"
msgstr ""
msgid "AlertSettings|The form has unsaved changes. How would you like to proceed?"
msgstr ""
msgid "AlertSettings|URL cannot be blank and must start with http or https"
msgstr ""
msgid "AlertSettings|URL is invalid."
msgstr ""
msgid "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."
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 URL and authorization key"
msgstr ""
msgid "AlertSettings|View credentials"
msgstr ""
......@@ -2992,9 +3022,6 @@ msgstr ""
msgid "AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated."
msgstr ""
msgid "AlertSettings|Your integration was successfully updated."
msgstr ""
msgid "AlertSettings|{ \"events\": [{ \"application\": \"Name of application\" }] }"
msgstr ""
......@@ -3031,7 +3058,10 @@ msgstr ""
msgid "AlertsIntegrations|The integration has been successfully removed."
msgstr ""
msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
msgid "AlertsIntegrations|The integration has been successfully saved."
msgstr ""
msgid "AlertsIntegrations|The integration is currently inactive. Enable the integration to send the test alert."
msgstr ""
msgid "AlertsIntegrations|The integration token could not be reset. Please try again."
......@@ -20127,6 +20157,9 @@ msgstr ""
msgid "Name"
msgstr ""
msgid "Name can't be blank"
msgstr ""
msgid "Name has already been taken"
msgstr ""
......@@ -27529,6 +27562,9 @@ msgstr ""
msgid "SelfMonitoring|Self monitoring project has been successfully deleted."
msgstr ""
msgid "Send"
msgstr ""
msgid "Send a single email notification to Owners and Maintainers for new alerts."
msgstr ""
......
......@@ -25,7 +25,7 @@ RSpec.describe 'Alert integrations settings form', :js do
it 'shows the alerts setting form title' do
page.within('#js-alert-management-settings') do
expect(find('h4')).to have_content('Alerts')
expect(find('h4')).to have_content('Alert integrations')
end
end
......
......@@ -63,7 +63,7 @@ RSpec.describe 'User searches project settings', :js do
visit project_settings_operations_path(project)
end
it_behaves_like 'can search settings', 'Alerts', 'Error tracking'
it_behaves_like 'can search settings', 'Alert integrations', 'Error tracking'
end
context 'in Pages page' do
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
<form
class="gl-mt-6"
>
<div
class="tabs gl-tabs"
id="__BVID__6"
>
<!---->
<div
class=""
>
<ul
class="nav gl-tabs-nav"
id="__BVID__6__BV_tab_controls_"
role="tablist"
>
<!---->
<li
class="nav-item"
role="presentation"
>
<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"
>
<a
aria-controls="__BVID__19"
aria-disabled="true"
aria-posinset="2"
aria-selected="false"
aria-setsize="3"
class="nav-link disabled disabled gl-tab-nav-item"
href="#"
id="__BVID__19___BV_tab_button__"
role="tab"
tabindex="-1"
target="_self"
>
View credentials
</a>
</li>
<li
class="nav-item"
role="presentation"
>
<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
class="tab-content gl-tab-content"
id="__BVID__6__BV_tab_container_"
>
<transition-stub
css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
leaveactiveclass=""
leaveclass="show"
leavetoclass=""
mode="out-in"
name=""
>
<div
aria-hidden="false"
aria-labelledby="__BVID__8___BV_tab_button__"
class="tab-pane active"
id="__BVID__8"
role="tabpanel"
style=""
>
<div
class="form-group gl-form-group"
id="integration-type"
role="group"
>
<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"
>
<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
class="gl-mt-3"
>
<!---->
<label
class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal"
>
<span
class="gl-toggle-wrapper"
>
<span
class="gl-toggle-label"
data-testid="toggle-label"
>
Active
</span>
<!---->
<button
aria-label="Active"
class="gl-toggle"
role="switch"
type="button"
>
<span
class="toggle-icon"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="close-icon"
>
<use
href="#close"
/>
</svg>
</span>
</button>
</span>
<!---->
</label>
<!---->
<!---->
</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-button-text"
>
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__19___BV_tab_button__"
class="tab-pane disabled"
id="__BVID__19"
role="tabpanel"
style="display: none;"
>
<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
class="gl-link gl-display-inline-block"
href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
rel="noopener noreferrer"
target="_blank"
>
GitLab documentation
</a>
to learn more about configuring your endpoint.
</span>
<fieldset
class="form-group gl-form-group"
id="integration-webhook"
>
<!---->
<div
class="bv-no-focus-ring"
role="group"
tabindex="-1"
>
<div
class="gl-my-4"
>
<span
class="gl-font-weight-bold"
>
Webhook URL
</span>
<div
id="url"
readonly="readonly"
>
<div
class="input-group"
role="group"
>
<!---->
<!---->
<input
class="gl-form-input form-control"
id="url"
readonly="readonly"
type="text"
/>
<div
class="input-group-append"
>
<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"
>
<!---->
<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
class="gl-my-4"
>
<span
class="gl-font-weight-bold"
>
Authorization key
</span>
<div
class="gl-mb-3"
id="authorization-key"
readonly="readonly"
>
<div
class="input-group"
role="group"
>
<!---->
<!---->
<input
class="gl-form-input form-control"
id="authorization-key"
readonly="readonly"
type="text"
/>
<div
class="input-group-append"
>
<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"
>
<!---->
<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>
</fieldset>
<button
class="btn btn-danger btn-md disabled gl-button"
disabled="disabled"
type="button"
>
<!---->
<!---->
<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"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Cancel and close
</span>
</button>
<!---->
</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__41___BV_tab_button__"
class="tab-pane disabled"
id="__BVID__41"
role="tabpanel"
style="display: none;"
>
<fieldset
class="form-group gl-form-group"
id="test-integration"
>
<!---->
<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>
<button
class="btn js-no-auto-disable btn-confirm btn-md gl-button"
data-testid="send-test-alert"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Send
</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>
</transition-stub>
<!---->
</div>
</div>
</form>
`;
import { GlForm, GlFormSelect, GlFormInput, GlToggle, GlFormTextarea, GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
......@@ -13,7 +15,8 @@ describe('AlertsSettingsForm', () => {
const mockToastShow = jest.fn();
const createComponent = ({ data = {}, props = {}, multiIntegrations = true } = {}) => {
wrapper = mount(AlertsSettingsForm, {
wrapper = extendedWrapper(
mount(AlertsSettingsForm, {
data() {
return { ...data };
},
......@@ -34,22 +37,22 @@ describe('AlertsSettingsForm', () => {
show: mockToastShow,
},
},
});
}),
);
};
const findForm = () => wrapper.findComponent(GlForm);
const findSelect = () => wrapper.findComponent(GlFormSelect);
const findFormFields = () => wrapper.findAllComponents(GlFormInput);
const findFormToggle = () => wrapper.findComponent(GlToggle);
const findSamplePayloadSection = () => wrapper.find('[data-testid="sample-payload-section"]');
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
const findSamplePayloadSection = () => wrapper.findByTestId('sample-payload-section');
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="send-test-alert"]`);
const findSubmitButton = () => wrapper.findByTestId('integration-form-submit');
const findMultiSupportText = () => wrapper.findByTestId('multi-integrations-not-supported');
const findJsonTestSubmit = () => wrapper.findByTestId('send-test-alert');
const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`);
const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`);
const findActionBtn = () => wrapper.findByTestId('payload-action-btn');
const findTabs = () => wrapper.findAllComponents(GlTab);
afterEach(() => {
......@@ -74,10 +77,6 @@ describe('AlertsSettingsForm', () => {
createComponent();
});
it('renders the initial template', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('render the initial form with only an integration type dropdown', () => {
expect(findForm().exists()).toBe(true);
expect(findSelect().exists()).toBe(true);
......@@ -151,8 +150,7 @@ describe('AlertsSettingsForm', () => {
findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
findForm().trigger('submit');
expect(wrapper.emitted('create-new-integration')[0]).toEqual([
{
expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({
type: typeSet.http,
variables: {
name: integrationName,
......@@ -160,20 +158,20 @@ describe('AlertsSettingsForm', () => {
payloadAttributeMappings: sampleMapping,
payloadExample: '{}',
},
},
]);
});
});
it('update', () => {
createComponent({
data: {
selectedIntegration: typeSet.http,
currentIntegration: { id: '1', name: 'Test integration pre' },
integrationForm: { id: '1', name: 'Test integration pre', type: typeSet.http },
currentIntegration: { id: '1' },
},
props: {
loading: false,
},
});
const updatedIntegrationName = 'Test integration post';
enableIntegration(0, updatedIntegrationName);
......@@ -181,11 +179,8 @@ describe('AlertsSettingsForm', () => {
expect(submitBtn.exists()).toBe(true);
expect(submitBtn.text()).toBe('Save integration');
findForm().trigger('submit');
expect(wrapper.emitted('update-integration')[0]).toEqual(
expect.arrayContaining([
{
submitBtn.trigger('click');
expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({
type: typeSet.http,
variables: {
name: updatedIntegrationName,
......@@ -193,9 +188,7 @@ describe('AlertsSettingsForm', () => {
payloadAttributeMappings: [],
payloadExample: '{}',
},
},
]),
);
});
});
});
......@@ -211,16 +204,17 @@ describe('AlertsSettingsForm', () => {
findForm().trigger('submit');
expect(wrapper.emitted('create-new-integration')[0]).toEqual([
{ type: typeSet.prometheus, variables: { apiUrl, active: true } },
]);
expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({
type: typeSet.prometheus,
variables: { apiUrl, active: true },
});
});
it('update', () => {
createComponent({
data: {
selectedIntegration: typeSet.prometheus,
currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
integrationForm: { id: '1', apiUrl: 'https://test-pre.com', type: typeSet.prometheus },
currentIntegration: { id: '1' },
},
props: {
loading: false,
......@@ -236,9 +230,10 @@ describe('AlertsSettingsForm', () => {
findForm().trigger('submit');
expect(wrapper.emitted('update-integration')[0]).toEqual([
{ type: typeSet.prometheus, variables: { apiUrl, active: true } },
]);
expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({
type: typeSet.prometheus,
variables: { apiUrl, active: true },
});
});
});
});
......@@ -247,7 +242,6 @@ describe('AlertsSettingsForm', () => {
beforeEach(() => {
createComponent({
data: {
selectedIntegration: typeSet.http,
currentIntegration: { id: '1', name: 'Test' },
active: true,
},
......@@ -262,7 +256,7 @@ describe('AlertsSettingsForm', () => {
await findJsonTextArea().setValue('Invalid JSON');
jest.runAllTimers();
await wrapper.vm.$nextTick();
await nextTick();
const jsonTestSubmit = findJsonTestSubmit();
expect(jsonTestSubmit.exists()).toBe(true);
......@@ -275,7 +269,7 @@ describe('AlertsSettingsForm', () => {
await findJsonTextArea().setValue('{ "value": "value" }');
jest.runAllTimers();
await wrapper.vm.$nextTick();
await nextTick();
expect(findJsonTestSubmit().props('disabled')).toBe(false);
});
});
......@@ -283,14 +277,13 @@ describe('AlertsSettingsForm', () => {
describe('Test payload section for HTTP integration', () => {
const validSamplePayload = JSON.stringify(alertFields);
const emptySamplePayload = '{}';
beforeEach(() => {
createComponent({
multiIntegrations: true,
data: {
integrationForm: { type: typeSet.http },
currentIntegration: {
type: typeSet.http,
payloadExample: validSamplePayload,
payloadAttributeMappings: [],
payloadExample: emptySamplePayload,
},
active: false,
resetPayloadAndMappingConfirmed: false,
......@@ -300,25 +293,25 @@ describe('AlertsSettingsForm', () => {
});
describe.each`
active | resetPayloadAndMappingConfirmed | disabled
${true} | ${true} | ${undefined}
${false} | ${true} | ${'disabled'}
${true} | ${false} | ${'disabled'}
${false} | ${false} | ${'disabled'}
`('', ({ active, resetPayloadAndMappingConfirmed, disabled }) => {
payload | resetPayloadAndMappingConfirmed | disabled
${validSamplePayload} | ${true} | ${undefined}
${emptySamplePayload} | ${true} | ${undefined}
${validSamplePayload} | ${false} | ${'disabled'}
${emptySamplePayload} | ${false} | ${undefined}
`('', ({ payload, resetPayloadAndMappingConfirmed, disabled }) => {
const payloadResetMsg = resetPayloadAndMappingConfirmed
? 'was confirmed'
: 'was not confirmed';
const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled';
const activeState = active ? 'active' : 'not active';
const validPayloadMsg = payload === emptySamplePayload ? 'not valid' : 'valid';
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 payload is ${validPayloadMsg}`, async () => {
wrapper.setData({
selectedIntegration: typeSet.http,
active,
currentIntegration: { payloadExample: payload },
resetPayloadAndMappingConfirmed,
});
await wrapper.vm.$nextTick();
await nextTick();
expect(findSamplePayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(
disabled,
);
......@@ -329,9 +322,9 @@ describe('AlertsSettingsForm', () => {
describe.each`
resetPayloadAndMappingConfirmed | payloadExample | caption
${false} | ${validSamplePayload} | ${'Edit payload'}
${true} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
${true} | ${validSamplePayload} | ${'Parse payload for custom mapping'}
${false} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
${true} | ${emptySamplePayload} | ${'Parse payload fields'}
${true} | ${validSamplePayload} | ${'Parse payload fields'}
${false} | ${emptySamplePayload} | ${'Parse payload fields'}
`('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
const payloadResetMsg = resetPayloadAndMappingConfirmed
......@@ -340,16 +333,12 @@ describe('AlertsSettingsForm', () => {
it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
wrapper.setData({
selectedIntegration: typeSet.http,
currentIntegration: {
payloadExample,
type: typeSet.http,
active: true,
payloadAttributeMappings: [],
},
resetPayloadAndMappingConfirmed,
});
await wrapper.vm.$nextTick();
await nextTick();
expect(findActionBtn().text()).toBe(caption);
});
});
......@@ -358,7 +347,6 @@ describe('AlertsSettingsForm', () => {
describe('Parsing payload', () => {
beforeEach(() => {
wrapper.setData({
selectedIntegration: typeSet.http,
resetPayloadAndMappingConfirmed: true,
});
});
......@@ -398,11 +386,12 @@ describe('AlertsSettingsForm', () => {
${true} | ${false} | ${1} | ${false}
${false} | ${true} | ${1} | ${false}
`('', ({ alertFieldsProvided, multiIntegrations, integrationOption, visible }) => {
const visibleMsg = visible ? 'is rendered' : 'is not rendered';
const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided';
const visibleMsg = visible ? 'rendered' : 'not rendered';
const alertFieldsMsg = alertFieldsProvided ? 'provided' : 'not provided';
const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
const multiIntegrationsEnabled = multiIntegrations ? 'enabled' : 'not enabled';
it(`${visibleMsg} when integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => {
it(`is ${visibleMsg} when multiIntegrations are ${multiIntegrationsEnabled}, integration type is ${integrationType} and alert fields are ${alertFieldsMsg}`, async () => {
createComponent({
multiIntegrations,
props: {
......@@ -411,8 +400,63 @@ describe('AlertsSettingsForm', () => {
});
await selectOptionAtIndex(integrationOption);
expect(findMappingBuilderSection().exists()).toBe(visible);
expect(findMappingBuilder().exists()).toBe(visible);
});
});
});
describe('Form validation', () => {
beforeEach(() => {
createComponent();
});
it('should not be able to submit when no integration type is selected', () => {
selectOptionAtIndex(0);
expect(findSubmitButton().attributes('disabled')).toBe('disabled');
});
it('should not be able to submit when HTTP integration form is invalid', () => {
selectOptionAtIndex(1);
expect(findSubmitButton().attributes('disabled')).toBe('disabled');
});
it('should be able to submit when HTTP integration form is valid', async () => {
await selectOptionAtIndex(1);
await findFormFields().at(0).setValue('Name');
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
it('should not be able to submit when Prometheus integration form is invalid', () => {
selectOptionAtIndex(2);
expect(findSubmitButton().attributes('disabled')).toBe('disabled');
});
it('should be able to submit when Prometheus integration form is valid', async () => {
await selectOptionAtIndex(2);
await findFormFields().at(0).setValue('http://valid.url');
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
it('should be able to submit when form is dirty', async () => {
wrapper.setData({
currentIntegration: { type: typeSet.http, name: 'Existing integration' },
});
await nextTick();
await findFormFields().at(0).setValue('Updated name');
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
it('should not be able to submit when form is pristine', async () => {
wrapper.setData({
currentIntegration: { type: typeSet.http, name: 'Existing integration' },
});
await nextTick();
expect(findSubmitButton().attributes('disabled')).toBe('disabled');
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
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 createMockApollo from 'helpers/mock_apollo_helper';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
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, {
i18n,
} from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import { typeSet } from '~/alerts_settings/constants';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import { typeSet, i18n } 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';
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
......@@ -27,10 +27,12 @@ import {
RESET_INTEGRATION_TOKEN_ERROR,
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
import createFlash, { FLASH_TYPES } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import {
createHttpVariables,
updateHttpVariables,
......@@ -81,8 +83,9 @@ 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 findAddIntegrationBtn = () => wrapper.findByTestId('add-integration-btn');
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
const findAlert = () => wrapper.findComponent(GlAlert);
async function destroyHttpIntegration(localWrapper) {
await jest.runOnlyPendingTimers();
......@@ -94,13 +97,14 @@ describe('AlertsSettingsWrapper', () => {
}
async function awaitApolloDomMock() {
await wrapper.vm.$nextTick(); // kick off the DOM update
await nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
await nextTick(); // kick off the DOM update for flash
}
const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
wrapper = mount(AlertsSettingsWrapper, {
wrapper = extendedWrapper(
mount(AlertsSettingsWrapper, {
data() {
return { ...data };
},
......@@ -119,7 +123,8 @@ describe('AlertsSettingsWrapper', () => {
},
},
},
});
}),
);
};
function createComponentWithApollo({
......@@ -200,15 +205,19 @@ describe('AlertsSettingsWrapper', () => {
loading: false,
});
});
it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
describe('Create', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
data: { httpIntegrationCreate: { integration: { id: '1' }, errors: [] } },
});
findAlertsSettingsForm().vm.$emit('create-new-integration', {
type: typeSet.http,
variables: createHttpVariables,
});
});
it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createHttpIntegrationMutation,
......@@ -217,6 +226,11 @@ describe('AlertsSettingsWrapper', () => {
});
});
it('shows success alert', () => {
expect(findAlert().exists()).toBe(true);
});
});
it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updateHttpIntegrationMutation: { integration: { id: '1' } } },
......@@ -334,13 +348,29 @@ describe('AlertsSettingsWrapper', () => {
expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
});
it('shows an error alert when integration test payload fails ', async () => {
const mock = new AxiosMockAdapter(axios);
mock.onPost(/(.*)/).replyOnce(403);
return wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
describe('Test alert failure', () => {
let mock;
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('shows an error alert when integration test payload is invalid ', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createFlash).toHaveBeenCalledTimes(1);
mock.restore();
});
it('shows an error alert when integration is not activated ', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.FORBIDDEN);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createFlash).toHaveBeenCalledWith({
message: INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
});
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
......@@ -354,7 +384,7 @@ describe('AlertsSettingsWrapper', () => {
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValueOnce({});
findIntegrationsList().vm.$emit('edit-integration', updateHttpVariables);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateCurrentHttpIntegrationMutation,
......@@ -372,7 +402,7 @@ describe('AlertsSettingsWrapper', () => {
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
findIntegrationsList().vm.$emit('edit-integration', updatePrometheusVariables);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateCurrentPrometheusIntegrationMutation,
......@@ -414,7 +444,7 @@ describe('AlertsSettingsWrapper', () => {
createComponentWithApollo();
await jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
await nextTick();
expect(findIntegrations()).toHaveLength(4);
});
......@@ -426,7 +456,7 @@ describe('AlertsSettingsWrapper', () => {
expect(destroyIntegrationHandler).toHaveBeenCalled();
await wrapper.vm.$nextTick();
await nextTick();
expect(findIntegrations()).toHaveLength(3);
});
......
......@@ -36,7 +36,7 @@ RSpec.describe 'projects/settings/operations/show' do
it 'renders the Operations Settings page' do
render
expect(rendered).to have_content _('Alerts')
expect(rendered).to have_content _('Alert integrations')
expect(rendered).to have_content _('Display alerts from all your monitoring tools directly within GitLab.')
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment