588 lines
18 KiB
Vue
588 lines
18 KiB
Vue
<script>
|
|
import {
|
|
GlButton,
|
|
GlForm,
|
|
GlFormGroup,
|
|
GlFormSelect,
|
|
GlFormInput,
|
|
GlFormInputGroup,
|
|
GlFormTextarea,
|
|
GlModal,
|
|
GlModalDirective,
|
|
GlToggle,
|
|
GlTabs,
|
|
GlTab,
|
|
} from '@gitlab/ui';
|
|
import * as Sentry from '@sentry/browser';
|
|
import { isEmpty, omit } from 'lodash';
|
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
import {
|
|
integrationTypes,
|
|
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 default {
|
|
placeholders: {
|
|
prometheus: targetPrometheusUrlPlaceholder,
|
|
},
|
|
JSON_VALIDATE_DELAY,
|
|
typeSet,
|
|
i18n,
|
|
components: {
|
|
ClipboardButton,
|
|
GlButton,
|
|
GlForm,
|
|
GlFormGroup,
|
|
GlFormInput,
|
|
GlFormInputGroup,
|
|
GlFormTextarea,
|
|
GlFormSelect,
|
|
GlModal,
|
|
GlToggle,
|
|
GlTabs,
|
|
GlTab,
|
|
AlertSettingsFormHelpBlock,
|
|
MappingBuilder,
|
|
},
|
|
directives: {
|
|
GlModal: GlModalDirective,
|
|
},
|
|
mixins: [glFeatureFlagsMixin()],
|
|
inject: {
|
|
generic: {
|
|
default: {},
|
|
},
|
|
prometheus: {
|
|
default: {},
|
|
},
|
|
multiIntegrations: {
|
|
default: false,
|
|
},
|
|
projectPath: {
|
|
default: '',
|
|
},
|
|
},
|
|
props: {
|
|
loading: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
canAddIntegration: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
alertFields: {
|
|
type: Array,
|
|
required: false,
|
|
default: null,
|
|
},
|
|
},
|
|
apollo: {
|
|
currentIntegration: {
|
|
query: getCurrentIntegrationQuery,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
integrationTypesOptions: Object.values(integrationTypes),
|
|
selectedIntegration: integrationTypes.none.value,
|
|
active: false,
|
|
samplePayload: {
|
|
json: null,
|
|
error: null,
|
|
},
|
|
testPayload: {
|
|
json: null,
|
|
error: null,
|
|
},
|
|
resetPayloadAndMappingConfirmed: false,
|
|
mapping: [],
|
|
parsingPayload: false,
|
|
currentIntegration: null,
|
|
parsedPayload: [],
|
|
};
|
|
},
|
|
computed: {
|
|
isPrometheus() {
|
|
return this.selectedIntegration === this.$options.typeSet.prometheus;
|
|
},
|
|
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) {
|
|
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,
|
|
endpoint: this.integrationForm.url,
|
|
token: this.integrationForm.token,
|
|
};
|
|
},
|
|
showMappingBuilder() {
|
|
return (
|
|
this.multiIntegrations &&
|
|
this.glFeatures.multipleHttpIntegrationsCustomMapping &&
|
|
this.isHttp &&
|
|
this.alertFields?.length
|
|
);
|
|
},
|
|
hasSamplePayload() {
|
|
return this.isValidNonEmptyJSON(this.currentIntegration?.payloadExample);
|
|
},
|
|
canEditPayload() {
|
|
return this.hasSamplePayload && !this.resetPayloadAndMappingConfirmed;
|
|
},
|
|
isResetAuthKeyDisabled() {
|
|
return !this.active && !this.integrationForm.token !== '';
|
|
},
|
|
isPayloadEditDisabled() {
|
|
return this.glFeatures.multipleHttpIntegrationsCustomMapping
|
|
? !this.active || this.canEditPayload
|
|
: !this.active;
|
|
},
|
|
isSubmitTestPayloadDisabled() {
|
|
return !this.active || Boolean(this.samplePayload.error) || this.samplePayload.json === '';
|
|
},
|
|
isSelectDisabled() {
|
|
return this.currentIntegration !== null || !this.canAddIntegration;
|
|
},
|
|
},
|
|
watch: {
|
|
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.parsedPayload = payloadAlertFields;
|
|
this.samplePayload.json = this.isValidNonEmptyJSON(payloadExample) ? payloadExample : null;
|
|
const mapping = payloadAttributeMappings.map((mappingItem) =>
|
|
omit(mappingItem, '__typename'),
|
|
);
|
|
this.updateMapping(mapping);
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
isValidNonEmptyJSON(JSONString) {
|
|
if (JSONString) {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(JSONString);
|
|
} catch (error) {
|
|
Sentry.captureException(error);
|
|
}
|
|
if (parsed) return !isEmpty(parsed);
|
|
}
|
|
return false;
|
|
},
|
|
sendTestAlert() {
|
|
this.$emit('test-alert-payload', this.testAlertPayload);
|
|
},
|
|
submit() {
|
|
const { name, apiUrl } = this.integrationForm;
|
|
const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
|
|
? {
|
|
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);
|
|
},
|
|
reset() {
|
|
this.resetFormValues();
|
|
this.resetPayloadAndMapping();
|
|
this.$emit('clear-current-integration', { type: this.currentIntegration?.type });
|
|
},
|
|
resetFormValues() {
|
|
this.selectedIntegration = integrationTypes.none.value;
|
|
this.integrationForm.name = '';
|
|
this.integrationForm.apiUrl = '';
|
|
this.samplePayload = {
|
|
json: null,
|
|
error: null,
|
|
};
|
|
this.active = false;
|
|
},
|
|
resetAuthKey() {
|
|
if (!this.currentIntegration) {
|
|
return;
|
|
}
|
|
|
|
this.$emit('reset-token', {
|
|
type: this.selectedIntegration,
|
|
variables: { id: this.currentIntegration.id },
|
|
});
|
|
},
|
|
validateJson(isSamplePayload = true) {
|
|
const payload = isSamplePayload ? this.samplePayload : this.testPayload;
|
|
|
|
payload.error = null;
|
|
if (payload.json === '') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
JSON.parse(payload.json);
|
|
} catch (e) {
|
|
payload.error = JSON.stringify(e.message);
|
|
}
|
|
},
|
|
parseMapping() {
|
|
this.parsingPayload = true;
|
|
|
|
return this.$apollo
|
|
.query({
|
|
query: parseSamplePayloadQuery,
|
|
variables: { projectPath: this.projectPath, payload: this.samplePayload.json },
|
|
})
|
|
.then(
|
|
({
|
|
data: {
|
|
project: { alertManagementPayloadFields },
|
|
},
|
|
}) => {
|
|
this.parsedPayload = alertManagementPayloadFields;
|
|
this.resetPayloadAndMappingConfirmed = false;
|
|
|
|
this.$toast.show(
|
|
this.$options.i18n.integrationFormSteps.setSamplePayload.payloadParsedSucessMsg,
|
|
);
|
|
},
|
|
)
|
|
.catch(({ message }) => {
|
|
this.samplePayload.error = message;
|
|
})
|
|
.finally(() => {
|
|
this.parsingPayload = false;
|
|
});
|
|
},
|
|
updateMapping(mapping) {
|
|
this.mapping = mapping;
|
|
},
|
|
resetPayloadAndMapping() {
|
|
this.resetPayloadAndMappingConfirmed = true;
|
|
this.parsedPayload = [];
|
|
this.updateMapping([]);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
|
|
<gl-tabs>
|
|
<gl-tab :title="$options.i18n.integrationTabs.configureDetails">
|
|
<gl-form-group
|
|
v-if="isCreating"
|
|
id="integration-type"
|
|
:label="$options.i18n.integrationFormSteps.selectType.label"
|
|
label-for="integration-type"
|
|
>
|
|
<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>
|
|
<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.setupCredentials.prometheusHelp
|
|
: $options.i18n.integrationFormSteps.setupCredentials.help
|
|
"
|
|
link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
|
|
/>
|
|
|
|
<gl-toggle
|
|
v-model="active"
|
|
:is-loading="loading"
|
|
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
|
|
class="gl-my-4 gl-font-weight-normal"
|
|
/>
|
|
|
|
<div v-if="isPrometheus" class="gl-my-4">
|
|
<span class="gl-font-weight-bold">
|
|
{{ $options.i18n.integrationFormSteps.prometheusFormUrl.label }}
|
|
</span>
|
|
|
|
<gl-form-input
|
|
id="integration-apiUrl"
|
|
v-model="integrationForm.apiUrl"
|
|
type="text"
|
|
:placeholder="$options.placeholders.prometheus"
|
|
/>
|
|
|
|
<span class="gl-text-gray-400">
|
|
{{ $options.i18n.integrationFormSteps.prometheusFormUrl.help }}
|
|
</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">
|
|
{{ $options.i18n.integrationFormSteps.setupCredentials.webhookUrl }}
|
|
</span>
|
|
|
|
<gl-form-input-group id="url" readonly :value="integrationForm.url">
|
|
<template #append>
|
|
<clipboard-button
|
|
:text="integrationForm.url || ''"
|
|
:title="$options.i18n.copy"
|
|
class="gl-m-0!"
|
|
/>
|
|
</template>
|
|
</gl-form-input-group>
|
|
</div>
|
|
|
|
<div class="gl-my-4">
|
|
<span class="gl-font-weight-bold">
|
|
{{ $options.i18n.integrationFormSteps.setupCredentials.authorizationKey }}
|
|
</span>
|
|
|
|
<gl-form-input-group
|
|
id="authorization-key"
|
|
class="gl-mb-3"
|
|
readonly
|
|
:value="integrationForm.token"
|
|
>
|
|
<template #append>
|
|
<clipboard-button
|
|
:text="integrationForm.token || ''"
|
|
:title="$options.i18n.copy"
|
|
class="gl-m-0!"
|
|
/>
|
|
</template>
|
|
</gl-form-input-group>
|
|
</div>
|
|
</gl-form-group>
|
|
|
|
<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="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelp"
|
|
:link="generic.alertsUsageUrl"
|
|
/>
|
|
|
|
<gl-form-textarea
|
|
id="test-payload"
|
|
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(false)"
|
|
/>
|
|
</gl-form-group>
|
|
|
|
<gl-button
|
|
:disabled="!isTestPayloadValid"
|
|
data-testid="send-test-alert"
|
|
variant="confirm"
|
|
class="js-no-auto-disable"
|
|
@click="sendTestAlert"
|
|
>
|
|
{{ $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>
|