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

Integrate payload attribute mappings to cache

Handle data for integration load, updte, change, save
parent 70a9bd19
......@@ -7,15 +7,12 @@ import {
GlSearchBoxByType,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { cloneDeep, isEqual } from 'lodash';
import Vue from 'vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
import {
getMappingData,
getPayloadFields,
transformForSave,
} from '../utils/mapping_transformations';
import { mappingFields } from '../constants';
import { getMappingData, transformForSave } from '../utils/mapping_transformations';
export const i18n = {
columns: {
......@@ -33,6 +30,7 @@ export const i18n = {
export default {
i18n,
mappingFields,
components: {
GlIcon,
GlFormInput,
......@@ -73,18 +71,15 @@ export default {
};
},
computed: {
payloadFields() {
return getPayloadFields(this.parsedPayload);
},
mappingData() {
return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
return getMappingData(this.gitlabFields, this.parsedPayload, this.savedMapping);
},
hasFallbackColumn() {
return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks));
},
},
methods: {
setMapping(gitlabKey, mappingKey, valueKey) {
setMapping(gitlabKey, mappingKey, valueKey = mappingFields.mapping) {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
......@@ -100,11 +95,11 @@ export default {
return fields.filter((field) => field.label.toLowerCase().includes(search));
},
isSelected(fieldValue, mapping) {
return fieldValue === mapping;
return isEqual(fieldValue, mapping);
},
selectedValue(name) {
selectedValue(mapping) {
return (
this.payloadFields.find((item) => item.name === name)?.label ||
this.parsedPayload.find((item) => isEqual(item.path, mapping))?.label ||
this.$options.i18n.makeSelection
);
},
......@@ -150,7 +145,7 @@ export default {
:key="gitlabField.name"
class="gl-display-table-row"
>
<div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p gl-vertical-align-middle">
<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"
disabled
......@@ -164,7 +159,7 @@ export default {
</div>
</div>
<div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p gl-vertical-align-middle">
<div class="gl-display-table-cell gl-py-3 gl-pr-3 gl-w-30p gl-vertical-align-middle">
<gl-dropdown
:disabled="!gitlabField.mappingFields.length"
aria-labelledby="parsedFieldsHeader"
......@@ -175,10 +170,10 @@ export default {
<gl-search-box-by-type @input="setSearchTerm($event, 'searchTerm', gitlabField.name)" />
<gl-dropdown-item
v-for="mappingField in filterFields(gitlabField.searchTerm, gitlabField.mappingFields)"
:key="`${mappingField.name}__mapping`"
:is-checked="isSelected(gitlabField.mapping, mappingField.name)"
:key="`${mappingField.path}__mapping`"
:is-checked="isSelected(gitlabField.mapping, mappingField.path)"
is-check-item
@click="setMapping(gitlabField.name, mappingField.name, 'mapping')"
@click="setMapping(gitlabField.name, mappingField.path)"
>
{{ mappingField.label }}
</gl-dropdown-item>
......@@ -188,7 +183,7 @@ export default {
</gl-dropdown>
</div>
<div class="gl-display-table-cell gl-py-3 w-30p">
<div class="gl-display-table-cell gl-py-3 gl-w-30p">
<gl-dropdown
v-if="Boolean(gitlabField.numberOfFallbacks)"
:disabled="!gitlabField.mappingFields.length"
......@@ -205,10 +200,12 @@ export default {
gitlabField.fallbackSearchTerm,
gitlabField.mappingFields,
)"
:key="`${mappingField.name}__fallback`"
:is-checked="isSelected(gitlabField.fallback, mappingField.name)"
:key="`${mappingField.path}__fallback`"
:is-checked="isSelected(gitlabField.fallback, mappingField.path)"
is-check-item
@click="setMapping(gitlabField.name, mappingField.name, 'fallback')"
@click="
setMapping(gitlabField.name, mappingField.path, $options.mappingFields.fallback)
"
>
{{ mappingField.label }}
</gl-dropdown-item>
......
......@@ -120,14 +120,17 @@ export default {
const { category, action } = trackAlertIntegrationsViewsOptions;
Tracking.event(category, action);
},
setIntegrationToDelete({ name, id }) {
this.integrationToDelete.id = id;
this.integrationToDelete.name = name;
setIntegrationToDelete(integration) {
this.integrationToDelete = integration;
},
deleteIntegration() {
this.$emit('delete-integration', { id: this.integrationToDelete.id });
const { id, type } = this.integrationToDelete;
this.$emit('delete-integration', { id, type });
this.integrationToDelete = { ...integrationToDeleteDefault };
},
editIntegration({ id, type }) {
this.$emit('edit-integration', { id, type });
},
},
};
</script>
......@@ -169,7 +172,7 @@ export default {
<template #cell(actions)="{ item }">
<gl-button-group class="gl-ml-3">
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
<gl-button icon="pencil" @click="editIntegration(item)" />
<gl-button
v-gl-modal.deleteIntegration
:disabled="item.type === $options.typeSet.prometheus"
......
......@@ -12,6 +12,8 @@ import {
GlModalDirective,
GlToggle,
} 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';
......@@ -22,12 +24,9 @@ import {
typeSet,
} 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';
// Mocks will be removed when integrating with BE is ready
// data format is defined and will be the same as mocked (maybe with some minor changes)
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
import mockedCustomMapping from './mocks/parsedMapping.json';
export const i18n = {
integrationFormSteps: {
......@@ -92,7 +91,6 @@ export const i18n = {
};
export default {
integrationTypes,
placeholders: {
prometheus: targetPrometheusUrlPlaceholder,
},
......@@ -128,6 +126,9 @@ export default {
multiIntegrations: {
default: false,
},
projectPath: {
default: '',
},
},
props: {
loading: {
......@@ -151,18 +152,19 @@ export default {
},
data() {
return {
selectedIntegration: integrationTypes[0].value,
integrationTypesOptions: Object.values(integrationTypes),
selectedIntegration: integrationTypes.none.value,
active: false,
formVisible: false,
integrationTestPayload: {
json: null,
error: null,
},
resetSamplePayloadConfirmed: false,
customMapping: null,
resetPayloadAndMappingConfirmed: false,
mapping: [],
parsingPayload: false,
currentIntegration: null,
parsedPayload: [],
};
},
computed: {
......@@ -210,17 +212,11 @@ export default {
this.alertFields?.length
);
},
parsedSamplePayload() {
return this.customMapping?.samplePayload?.payloadAlerFields?.nodes;
},
savedMapping() {
return this.customMapping?.storedMapping?.nodes;
},
hasSamplePayload() {
return Boolean(this.customMapping?.samplePayload);
return this.isValidNonEmptyJSON(this.currentIntegration?.payloadExample);
},
canEditPayload() {
return this.hasSamplePayload && !this.resetSamplePayloadConfirmed;
return this.hasSamplePayload && !this.resetPayloadAndMappingConfirmed;
},
isResetAuthKeyDisabled() {
return !this.active && !this.integrationForm.token !== '';
......@@ -240,25 +236,52 @@ export default {
isSelectDisabled() {
return this.currentIntegration !== null || !this.canAddIntegration;
},
savedMapping() {
return this.mapping;
},
},
watch: {
currentIntegration(val) {
if (val === null) {
return this.reset();
this.reset();
return;
}
const { type, active, payloadExample, payloadAlertFields, payloadAttributeMappings } = val;
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;
const mapping = payloadAttributeMappings.map((mappingItem) =>
omit(mappingItem, '__typename'),
);
this.updateMapping(mapping);
}
this.selectedIntegration = val.type;
this.active = val.active;
if (val.type === typeSet.http && this.showMappingBuilder) this.getIntegrationMapping(val.id);
return this.integrationTypeSelect();
this.toggleFormVisibility();
},
},
methods: {
integrationTypeSelect() {
if (this.selectedIntegration === integrationTypes[0].value) {
this.formVisible = false;
} else {
this.formVisible = true;
isValidNonEmptyJSON(JSONString) {
if (JSONString) {
let parsed;
try {
parsed = JSON.parse(JSONString);
} catch (error) {
Sentry.captureException(error);
}
if (parsed) return !isEmpty(parsed);
}
return false;
},
toggleFormVisibility() {
this.formVisible = this.selectedIntegration !== integrationTypes.none.value;
},
submitWithTestPayload() {
this.$emit('set-test-alert-payload', this.testAlertPayload);
......@@ -269,20 +292,15 @@ export default {
const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
? {
payloadAttributeMappings: this.mapping,
payloadExample: this.integrationTestPayload.json,
payloadExample: this.integrationTestPayload.json || '{}',
}
: {};
const variables =
this.selectedIntegration === typeSet.http
? {
name,
active: this.active,
...customMappingVariables,
}
? { name, active: this.active, ...customMappingVariables }
: { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables };
if (this.currentIntegration) {
return this.$emit('update-integration', integrationPayload);
}
......@@ -291,11 +309,12 @@ export default {
return this.$emit('create-new-integration', integrationPayload);
},
reset() {
this.selectedIntegration = integrationTypes[0].value;
this.integrationTypeSelect();
this.selectedIntegration = integrationTypes.none.value;
this.toggleFormVisibility();
this.resetPayloadAndMapping();
if (this.currentIntegration) {
return this.$emit('clear-current-integration');
return this.$emit('clear-current-integration', { type: this.currentIntegration.type });
}
return this.resetFormValues();
......@@ -332,35 +351,40 @@ export default {
}
},
parseMapping() {
// TODO: replace with real BE mutation when ready;
this.parsingPayload = true;
return new Promise((resolve) => {
setTimeout(() => resolve(mockedCustomMapping), 1000);
})
.then((res) => {
const mapping = { ...res };
delete mapping.storedMapping;
this.customMapping = res;
this.integrationTestPayload.json = res?.samplePayload.body;
this.resetSamplePayloadConfirmed = false;
return this.$apollo
.query({
query: parseSamplePayloadQuery,
variables: { projectPath: this.projectPath, payload: this.integrationTestPayload.json },
})
.then(
({
data: {
project: { alertManagementPayloadFields },
},
}) => {
this.parsedPayload = alertManagementPayloadFields;
this.resetPayloadAndMappingConfirmed = false;
this.$toast.show(this.$options.i18n.integrationFormSteps.step4.payloadParsedSucessMsg);
this.$toast.show(this.$options.i18n.integrationFormSteps.step4.payloadParsedSucessMsg);
},
)
.catch(({ message }) => {
this.integrationTestPayload.error = message;
})
.finally(() => {
this.parsingPayload = false;
});
},
getIntegrationMapping() {
// TODO: replace with real BE mutation when ready;
return Promise.resolve(mockedCustomMapping).then((res) => {
this.customMapping = res;
this.integrationTestPayload.json = res?.samplePayload.body;
});
},
updateMapping(mapping) {
this.mapping = mapping;
},
resetPayloadAndMapping() {
this.resetPayloadAndMappingConfirmed = true;
this.parsedPayload = [];
this.updateMapping([]);
},
},
};
</script>
......@@ -377,8 +401,8 @@ export default {
v-model="selectedIntegration"
:disabled="isSelectDisabled"
class="mw-100"
:options="$options.integrationTypes"
@change="integrationTypeSelect"
:options="integrationTypesOptions"
@change="toggleFormVisibility"
/>
<div v-if="!canAddIntegration" class="gl-my-4" data-testid="multi-integrations-not-supported">
......@@ -551,7 +575,7 @@ export default {
:title="$options.i18n.integrationFormSteps.step4.resetHeader"
:ok-title="$options.i18n.integrationFormSteps.step4.resetOk"
ok-variant="danger"
@ok="resetSamplePayloadConfirmed = true"
@ok="resetPayloadAndMapping"
>
{{ $options.i18n.integrationFormSteps.step4.resetBody }}
</gl-modal>
......@@ -566,7 +590,7 @@ export default {
>
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<mapping-builder
:parsed-payload="parsedSamplePayload"
:parsed-payload="parsedPayload"
:saved-mapping="savedMapping"
:alert-fields="alertFields"
@onMappingUpdate="updateMapping"
......
......@@ -8,15 +8,18 @@ import createPrometheusIntegrationMutation from '../graphql/mutations/create_pro
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql';
import updateCurrentHttpIntegrationMutation from '../graphql/mutations/update_current_http_integration.mutation.graphql';
import updateCurrentPrometheusIntegrationMutation from '../graphql/mutations/update_current_prometheus_integration.mutation.graphql';
import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
import getHttpIntegrationsQuery from '../graphql/queries/get_http_integrations.query.graphql';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
import service from '../services';
import {
updateStoreAfterIntegrationDelete,
updateStoreAfterIntegrationAdd,
updateStoreAfterHttpIntegrationAdd,
} from '../utils/cache_updates';
import {
DELETE_INTEGRATION_ERROR,
......@@ -84,6 +87,28 @@ export default {
createFlash({ message: err });
},
},
// TODO: we'll need to update the logic to request specific http integration by its id on edit
// when BE adds support for it https://gitlab.com/gitlab-org/gitlab/-/issues/321674
// currently the request for ALL http integrations is made and on specific integration edit we search it in the list
httpIntegrations: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getHttpIntegrationsQuery,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
const { alertManagementHttpIntegrations: { nodes: list = [] } = {} } = data.project || {};
return {
list,
};
},
error(err) {
createFlash({ message: err });
},
},
currentIntegration: {
query: getCurrentIntegrationQuery,
},
......@@ -93,6 +118,7 @@ export default {
isUpdating: false,
testAlertPayload: null,
integrations: {},
httpIntegrations: {},
currentIntegration: null,
};
},
......@@ -105,22 +131,28 @@ export default {
},
},
methods: {
isHttp(type) {
return type === typeSet.http;
},
createNewIntegration({ type, variables }) {
const { projectPath } = this;
const isHttp = this.isHttp(type);
this.isUpdating = true;
this.$apollo
.mutate({
mutation:
type === this.$options.typeSet.http
? createHttpIntegrationMutation
: createPrometheusIntegrationMutation,
mutation: isHttp ? createHttpIntegrationMutation : createPrometheusIntegrationMutation,
variables: {
...variables,
projectPath,
},
update(store, { data }) {
updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath });
if (isHttp) {
updateStoreAfterHttpIntegrationAdd(store, getHttpIntegrationsQuery, data, {
projectPath,
});
}
},
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
......@@ -157,10 +189,9 @@ export default {
this.isUpdating = true;
this.$apollo
.mutate({
mutation:
type === this.$options.typeSet.http
? updateHttpIntegrationMutation
: updatePrometheusIntegrationMutation,
mutation: this.isHttp(type)
? updateHttpIntegrationMutation
: updatePrometheusIntegrationMutation,
variables: {
...variables,
id: this.currentIntegration.id,
......@@ -176,7 +207,7 @@ export default {
return this.validateAlertPayload();
}
this.clearCurrentIntegration();
this.clearCurrentIntegration({ type });
return createFlash({
message: this.$options.i18n.changesSaved,
......@@ -195,16 +226,13 @@ export default {
this.isUpdating = true;
this.$apollo
.mutate({
mutation:
type === this.$options.typeSet.http
? resetHttpTokenMutation
: resetPrometheusTokenMutation,
mutation: this.isHttp(type) ? resetHttpTokenMutation : resetPrometheusTokenMutation,
variables,
})
.then(
({ data: { httpIntegrationResetToken, prometheusIntegrationResetToken } = {} } = {}) => {
const error =
httpIntegrationResetToken?.errors[0] || prometheusIntegrationResetToken?.errors[0];
const [error] =
httpIntegrationResetToken?.errors || prometheusIntegrationResetToken?.errors;
if (error) {
return createFlash({ message: error });
}
......@@ -214,10 +242,10 @@ export default {
prometheusIntegrationResetToken?.integration;
this.$apollo.mutate({
mutation: updateCurrentIntergrationMutation,
variables: {
...integration,
},
mutation: this.isHttp(type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: integration,
});
return createFlash({
......@@ -233,33 +261,30 @@ export default {
this.isUpdating = false;
});
},
editIntegration({ id }) {
const currentIntegration = this.integrations.list.find(
(integration) => integration.id === id,
);
editIntegration({ id, type }) {
let currentIntegration = this.integrations.list.find((integration) => integration.id === id);
if (this.isHttp(type)) {
const httpIntegrationMappingData = this.httpIntegrations.list.find(
(integration) => integration.id === id,
);
currentIntegration = { ...currentIntegration, ...httpIntegrationMappingData };
}
this.$apollo.mutate({
mutation: updateCurrentIntergrationMutation,
variables: {
id: currentIntegration.id,
name: currentIntegration.name,
active: currentIntegration.active,
token: currentIntegration.token,
type: currentIntegration.type,
url: currentIntegration.url,
apiUrl: currentIntegration.apiUrl,
},
mutation: this.isHttp(type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: currentIntegration,
});
},
deleteIntegration({ id }) {
deleteIntegration({ id, type }) {
const { projectPath } = this;
this.isUpdating = true;
this.$apollo
.mutate({
mutation: destroyHttpIntegrationMutation,
variables: {
id,
},
variables: { id },
update(store, { data }) {
updateStoreAfterIntegrationDelete(store, getIntegrationsQuery, data, { projectPath });
},
......@@ -269,7 +294,7 @@ export default {
if (error) {
return createFlash({ message: error });
}
this.clearCurrentIntegration();
this.clearCurrentIntegration({ type });
return createFlash({
message: this.$options.i18n.integrationRemoved,
type: FLASH_TYPES.SUCCESS,
......@@ -282,9 +307,11 @@ export default {
this.isUpdating = false;
});
},
clearCurrentIntegration() {
clearCurrentIntegration({ type }) {
this.$apollo.mutate({
mutation: updateCurrentIntergrationMutation,
mutation: this.isHttp(type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: {},
});
},
......
{
"samplePayload": {
"body": "{\n \"dashboardId\":1,\n \"evalMatches\":[\n {\n \"value\":1,\n \"metric\":\"Count\",\n \"tags\":{}\n }\n ],\n \"imageUrl\":\"https://grafana.com/static/assets/img/blog/mixed_styles.png\",\n \"message\":\"Notification Message\",\n \"orgId\":1,\n \"panelId\":2,\n \"ruleId\":1,\n \"ruleName\":\"Panel Title alert\",\n \"ruleUrl\":\"http://localhost:3000/d/hZ7BuVbWz/test-dashboard?fullscreen\\u0026edit\\u0026tab=alert\\u0026panelId=2\\u0026orgId=1\",\n \"state\":\"alerting\",\n \"tags\":{\n \"tag name\":\"tag value\"\n },\n \"title\":\"[Alerting] Panel Title alert\"\n}\n",
"payloadAlerFields": {
"nodes": [
{
"path": ["dashboardId"],
"label": "Dashboard Id",
"type": "string"
},
{
"path": ["evalMatches"],
"label": "Eval Matches",
"type": "array"
},
{
"path": ["createdAt"],
"label": "Created At",
"type": "datetime"
},
{
"path": ["imageUrl"],
"label": "Image Url",
"type": "string"
},
{
"path": ["message"],
"label": "Message",
"type": "string"
},
{
"path": ["orgId"],
"label": "Org Id",
"type": "string"
},
{
"path": ["panelId"],
"label": "Panel Id",
"type": "string"
},
{
"path": ["ruleId"],
"label": "Rule Id",
"type": "string"
},
{
"path": ["ruleName"],
"label": "Rule Name",
"type": "string"
},
{
"path": ["ruleUrl"],
"label": "Rule Url",
"type": "string"
},
{
"path": ["state"],
"label": "State",
"type": "string"
},
{
"path": ["title"],
"label": "Title",
"type": "string"
},
{
"path": ["tags", "tag"],
"label": "Tags",
"type": "string"
}
]
}
},
"storedMapping": {
"nodes": [
{
"alertFieldName": "title",
"payloadAlertPaths": "title",
"fallbackAlertPaths": "ruleUrl"
},
{
"alertFieldName": "description",
"payloadAlertPaths": "message"
},
{
"alertFieldName": "hosts",
"payloadAlertPaths": "evalMatches"
},
{
"alertFieldName": "startTime",
"payloadAlertPaths": "createdAt"
}
]
}
}
......@@ -40,11 +40,11 @@ export const i18n = {
integration: s__('AlertSettings|Integration'),
};
export const integrationTypes = [
{ value: '', text: s__('AlertSettings|Select integration type') },
{ value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
];
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') },
};
export const typeSet = {
http: 'HTTP',
......@@ -68,3 +68,8 @@ export const trackAlertIntegrationsViewsOptions = {
category: 'Alert Integrations',
action: 'view_alert_integrations_list',
};
export const mappingFields = {
mapping: 'mapping',
fallback: 'fallback',
};
......@@ -10,7 +10,18 @@ const resolvers = {
Mutation: {
updateCurrentIntegration: (
_,
{ id = null, name, active, token, type, url, apiUrl },
{
id = null,
name,
active,
token,
type,
url,
apiUrl,
payloadExample,
payloadAttributeMappings,
payloadAlertFields,
},
{ cache },
) => {
const sourceData = cache.readQuery({ query: getCurrentIntegrationQuery });
......@@ -28,6 +39,9 @@ const resolvers = {
type,
url,
apiUrl,
payloadExample,
payloadAttributeMappings,
payloadAlertFields,
};
}
});
......
#import "./integration_item.fragment.graphql"
#import "./http_integration_payload_data.fragment.graphql"
fragment HttpIntegrationItem on AlertManagementHttpIntegration {
...IntegrationItem
...HttpIntegrationPayloadData
}
fragment HttpIntegrationPayloadData on AlertManagementHttpIntegration {
payloadExample
payloadAttributeMappings {
fieldName
path
type
label
}
payloadAlertFields {
path
type
label
}
}
#import "../fragments/integration_item.fragment.graphql"
#import "../fragments/http_integration_item.fragment.graphql"
mutation createHttpIntegration(
$projectPath: ID!
......@@ -18,7 +18,7 @@ mutation createHttpIntegration(
) {
errors
integration {
...IntegrationItem
...HttpIntegrationItem
}
}
}
#import "../fragments/integration_item.fragment.graphql"
#import "../fragments/http_integration_item.fragment.graphql"
mutation destroyHttpIntegration($id: ID!) {
httpIntegrationDestroy(input: { id: $id }) {
errors
integration {
...IntegrationItem
...HttpIntegrationItem
}
}
}
#import "../fragments/integration_item.fragment.graphql"
#import "../fragments/http_integration_item.fragment.graphql"
mutation resetHttpIntegrationToken($id: ID!) {
httpIntegrationResetToken(input: { id: $id }) {
errors
integration {
...IntegrationItem
...HttpIntegrationItem
}
}
}
mutation updateCurrentHttpIntegration(
$id: String
$name: String
$active: Boolean
$token: String
$type: String
$url: String
$apiUrl: String
$payloadExample: JsonString
$payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
$payloadAlertFields: [AlertManagementPayloadAlertField!]
) {
updateCurrentIntegration(
id: $id
name: $name
active: $active
token: $token
type: $type
url: $url
apiUrl: $apiUrl
payloadExample: $payloadExample
payloadAttributeMappings: $payloadAttributeMappings
payloadAlertFields: $payloadAlertFields
) @client
}
mutation updateCurrentIntegration(
mutation updateCurrentPrometheusIntegration(
$id: String
$name: String
$active: Boolean
......@@ -6,6 +6,7 @@ mutation updateCurrentIntegration(
$type: String
$url: String
$apiUrl: String
$samplePayload: String
) {
updateCurrentIntegration(
id: $id
......@@ -15,5 +16,6 @@ mutation updateCurrentIntegration(
type: $type
url: $url
apiUrl: $apiUrl
samplePayload: $samplePayload
) @client
}
#import "../fragments/integration_item.fragment.graphql"
#import "../fragments/http_integration_item.fragment.graphql"
mutation updateHttpIntegration($id: ID!, $name: String!, $active: Boolean!) {
httpIntegrationUpdate(input: { id: $id, name: $name, active: $active }) {
mutation updateHttpIntegration(
$id: ID!
$name: String!
$active: Boolean!
$payloadExample: JsonString
$payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
) {
httpIntegrationUpdate(
input: {
id: $id
name: $name
active: $active
payloadExample: $payloadExample
payloadAttributeMappings: $payloadAttributeMappings
}
) {
errors
integration {
...IntegrationItem
...HttpIntegrationItem
}
}
}
#import "../fragments/http_integration_payload_data.fragment.graphql"
# TODO: this query need to accept http integration id to request a sepcific integration
query getHttpIntegrations($projectPath: ID!) {
project(fullPath: $projectPath) {
alertManagementHttpIntegrations {
nodes {
id
...HttpIntegrationPayloadData
}
}
}
}
query parsePayloadFields($projectPath: ID!, $payload: String!) {
project(fullPath: $projectPath) {
alertManagementPayloadFields(payloadExample: $payload) {
path
label
type
}
}
}
......@@ -60,6 +60,32 @@ const addIntegrationToStore = (
});
};
const addHttpIntegrationToStore = (store, query, { httpIntegrationCreate }, variables) => {
const integration = httpIntegrationCreate?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementHttpIntegrations.nodes = [
integration,
...draftData.project.alertManagementHttpIntegrations.nodes,
];
});
store.writeQuery({
query,
variables,
data,
});
};
const onError = (data, message) => {
createFlash({ message });
throw new Error(data.errors);
......@@ -82,3 +108,11 @@ export const updateStoreAfterIntegrationAdd = (store, query, data, variables) =>
addIntegrationToStore(store, query, data, variables);
}
};
export const updateStoreAfterHttpIntegrationAdd = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, ADD_INTEGRATION_ERROR);
} else {
addHttpIntegrationToStore(store, query, data, variables);
}
};
import { isEqual } from 'lodash';
/**
* Given data for GitLab alert fields, parsed payload fields data and previously stored mapping (if any)
* creates an object in a form convenient to build UI && interact with it
......@@ -10,16 +11,19 @@
export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
return gitlabFields.map((gitlabField) => {
// find fields from payload that match gitlab alert field by type
const mappingFields = payloadFields.filter(({ type }) => gitlabField.types.includes(type));
const mappingFields = payloadFields.filter(({ type }) =>
gitlabField.types.includes(type.toLowerCase()),
);
// find the mapping that was previously stored
const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
const foundMapping = savedMapping.find(
({ fieldName }) => fieldName.toLowerCase() === gitlabField.name,
);
const { path: mapping, fallbackPath: fallback } = foundMapping || {};
return {
mapping: payloadAlertPaths,
fallback: fallbackAlertPaths,
mapping,
fallback,
searchTerm: '',
fallbackSearchTerm: '',
mappingFields,
......@@ -36,7 +40,7 @@ export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
*/
export const transformForSave = (mappingData) => {
return mappingData.reduce((acc, field) => {
const mapped = field.mappingFields.find(({ name }) => name === field.mapping);
const mapped = field.mappingFields.find(({ path }) => isEqual(path, field.mapping));
if (mapped) {
const { path, type, label } = mapped;
acc.push({
......@@ -49,13 +53,3 @@ export const transformForSave = (mappingData) => {
return acc;
}, []);
};
/**
* Adds `name` prop to each provided by BE parsed payload field
* @param {Object} payload - parsed sample payload
*
* @return {Object} same as input with an extra `name` property which basically serves as a key to make a match
*/
export const getPayloadFields = (payload) => {
return payload.map((field) => ({ ...field, name: field.path.join('_') }));
};
import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import alertFields from '../mocks/alertFields.json';
import alertFields from '../mocks/alert_fields.json';
import parsedMapping from '../mocks/parsed_mapping.json';
describe('AlertMappingBuilder', () => {
let wrapper;
......@@ -12,8 +12,8 @@ describe('AlertMappingBuilder', () => {
function mountComponent() {
wrapper = shallowMount(AlertMappingBuilder, {
propsData: {
parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
savedMapping: parsedMapping.storedMapping.nodes,
parsedPayload: parsedMapping.payloadAlerFields,
savedMapping: parsedMapping.payloadAttributeMappings,
alertFields,
},
});
......@@ -33,6 +33,15 @@ describe('AlertMappingBuilder', () => {
const findColumnInRow = (row, column) =>
wrapper.findAll('.gl-display-table-row').at(row).findAll('.gl-display-table-cell ').at(column);
const getDropdownContent = (dropdown, types) => {
const searchBox = dropdown.findComponent(GlSearchBoxByType);
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const mappingOptions = parsedMapping.payloadAlerFields.filter(({ type }) =>
types.includes(type),
);
return { searchBox, dropdownItems, mappingOptions };
};
it('renders column captions', () => {
expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle);
expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle);
......@@ -63,10 +72,7 @@ describe('AlertMappingBuilder', () => {
it('renders mapping dropdown for each field', () => {
alertFields.forEach(({ types }, index) => {
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
const searchBox = dropdown.findComponent(GlSearchBoxByType);
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
const mappingOptions = nodes.filter(({ type }) => types.includes(type));
const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types);
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
......@@ -80,11 +86,7 @@ describe('AlertMappingBuilder', () => {
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
if (numberOfFallbacks) {
const searchBox = dropdown.findComponent(GlSearchBoxByType);
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
const mappingOptions = nodes.filter(({ type }) => types.includes(type));
const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types);
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
expect(dropdownItems).toHaveLength(mappingOptions.length);
}
......
......@@ -11,7 +11,8 @@ 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';
import { typeSet } from '~/alerts_settings/constants';
import alertFields from '../mocks/alertFields.json';
import alertFields from '../mocks/alert_fields.json';
import parsedMapping from '../mocks/parsed_mapping.json';
import { defaultAlertSettingsConfig } from './util';
describe('AlertsSettingsForm', () => {
......@@ -39,6 +40,9 @@ describe('AlertsSettingsForm', () => {
multiIntegrations,
},
mocks: {
$apollo: {
query: jest.fn(),
},
$toast: {
show: mockToastShow,
},
......@@ -146,7 +150,7 @@ describe('AlertsSettingsForm', () => {
enableIntegration(0, integrationName);
const sampleMapping = { field: 'test' };
const sampleMapping = parsedMapping.payloadAttributeMappings;
findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
findForm().trigger('submit');
......@@ -157,7 +161,7 @@ describe('AlertsSettingsForm', () => {
name: integrationName,
active: true,
payloadAttributeMappings: sampleMapping,
payloadExample: null,
payloadExample: '{}',
},
},
]);
......@@ -275,34 +279,47 @@ describe('AlertsSettingsForm', () => {
});
describe('Test payload section for HTTP integration', () => {
const validSamplePayload = JSON.stringify(alertFields);
const emptySamplePayload = '{}';
beforeEach(() => {
createComponent({
multipleHttpIntegrationsCustomMapping: true,
props: {
data: {
currentIntegration: {
type: typeSet.http,
payloadExample: validSamplePayload,
payloadAttributeMappings: [],
},
alertFields,
active: false,
resetPayloadAndMappingConfirmed: false,
},
props: { alertFields },
});
});
describe.each`
active | resetSamplePayloadConfirmed | disabled
${true} | ${true} | ${undefined}
${false} | ${true} | ${'disabled'}
${true} | ${false} | ${'disabled'}
${false} | ${false} | ${'disabled'}
`('', ({ active, resetSamplePayloadConfirmed, disabled }) => {
const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed';
active | resetPayloadAndMappingConfirmed | disabled
${true} | ${true} | ${undefined}
${false} | ${true} | ${'disabled'}
${true} | ${false} | ${'disabled'}
${false} | ${false} | ${'disabled'}
`('', ({ active, resetPayloadAndMappingConfirmed, disabled }) => {
const payloadResetMsg = resetPayloadAndMappingConfirmed
? 'was confirmed'
: 'was not confirmed';
const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled';
const activeState = active ? 'active' : 'not active';
it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => {
wrapper.setData({
customMapping: { samplePayload: true },
currentIntegration: {
type: typeSet.http,
payloadExample: validSamplePayload,
payloadAttributeMappings: [],
},
active,
resetSamplePayloadConfirmed,
resetPayloadAndMappingConfirmed,
});
await wrapper.vm.$nextTick();
expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled);
......@@ -311,20 +328,27 @@ describe('AlertsSettingsForm', () => {
describe('action buttons for sample payload', () => {
describe.each`
resetSamplePayloadConfirmed | samplePayload | caption
${false} | ${true} | ${'Edit payload'}
${true} | ${false} | ${'Submit payload'}
${true} | ${true} | ${'Submit payload'}
${false} | ${false} | ${'Submit payload'}
`('', ({ resetSamplePayloadConfirmed, samplePayload, caption }) => {
const samplePayloadMsg = samplePayload ? 'was provided' : 'was not provided';
const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed';
resetPayloadAndMappingConfirmed | payloadExample | caption
${false} | ${validSamplePayload} | ${'Edit payload'}
${true} | ${emptySamplePayload} | ${'Submit payload'}
${true} | ${validSamplePayload} | ${'Submit payload'}
${false} | ${emptySamplePayload} | ${'Submit payload'}
`('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
const payloadResetMsg = resetPayloadAndMappingConfirmed
? 'was confirmed'
: 'was not confirmed';
it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
wrapper.setData({
selectedIntegration: typeSet.http,
customMapping: { samplePayload },
resetSamplePayloadConfirmed,
currentIntegration: {
payloadExample,
type: typeSet.http,
active: true,
payloadAttributeMappings: [],
},
resetPayloadAndMappingConfirmed,
});
await wrapper.vm.$nextTick();
expect(findActionBtn().text()).toBe(caption);
......@@ -333,16 +357,20 @@ describe('AlertsSettingsForm', () => {
});
describe('Parsing payload', () => {
it('displays a toast message on successful parse', async () => {
jest.useFakeTimers();
beforeEach(() => {
wrapper.setData({
selectedIntegration: typeSet.http,
customMapping: { samplePayload: false },
resetPayloadAndMappingConfirmed: true,
});
await wrapper.vm.$nextTick();
});
it('displays a toast message on successful parse', async () => {
jest.spyOn(wrapper.vm.$apollo, 'query').mockResolvedValue({
data: {
project: { alertManagementPayloadFields: [] },
},
});
findActionBtn().vm.$emit('click');
jest.advanceTimersByTime(1000);
await waitForPromises();
......@@ -350,6 +378,16 @@ describe('AlertsSettingsForm', () => {
'Sample payload has been parsed. You can now map the fields.',
);
});
it('displays an error message under payload field on unsuccessful parse', async () => {
const errorMessage = 'Error parsing paylod';
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({ message: errorMessage });
findActionBtn().vm.$emit('click');
await waitForPromises();
expect(findTestPayloadSection().find('.invalid-feedback').text()).toBe(errorMessage);
});
});
});
......
......@@ -14,6 +14,8 @@ import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutat
import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
import updateCurrentHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql';
import updateCurrentPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql';
import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_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';
......@@ -31,7 +33,8 @@ import {
updateHttpVariables,
createPrometheusVariables,
updatePrometheusVariables,
ID,
HTTP_ID,
PROMETHEUS_ID,
errorMsg,
getIntegrationsQueryResponse,
destroyIntegrationResponse,
......@@ -50,8 +53,30 @@ describe('AlertsSettingsWrapper', () => {
let fakeApollo;
let destroyIntegrationHandler;
useMockIntersectionObserver();
const httpMappingData = {
payloadExample: '{"test: : "field"}',
payloadAttributeMappings: [],
payloadAlertFields: [],
};
const httpIntegrations = {
list: [
{
id: mockIntegrations[0].id,
...httpMappingData,
},
{
id: mockIntegrations[1].id,
...httpMappingData,
},
{
id: mockIntegrations[2].id,
httpMappingData,
},
],
};
const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon);
const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon);
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
async function destroyHttpIntegration(localWrapper) {
......@@ -197,13 +222,13 @@ describe('AlertsSettingsWrapper', () => {
});
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
type: typeSet.http,
variables: { id: ID },
variables: { id: HTTP_ID },
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: resetHttpTokenMutation,
variables: {
id: ID,
id: HTTP_ID,
},
});
});
......@@ -232,7 +257,7 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[3] },
loading: false,
});
......@@ -261,13 +286,13 @@ describe('AlertsSettingsWrapper', () => {
});
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
type: typeSet.prometheus,
variables: { id: ID },
variables: { id: PROMETHEUS_ID },
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: resetPrometheusTokenMutation,
variables: {
id: ID,
id: PROMETHEUS_ID,
},
});
});
......@@ -328,6 +353,42 @@ describe('AlertsSettingsWrapper', () => {
mock.restore();
});
});
it('calls `$apollo.mutate` with `updateCurrentHttpIntegrationMutation` on HTTP integration edit', () => {
createComponent({
data: {
integrations: { list: mockIntegrations },
currentIntegration: mockIntegrations[0],
httpIntegrations,
},
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
findIntegrationsList().vm.$emit('edit-integration', updateHttpVariables);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateCurrentHttpIntegrationMutation,
variables: { ...mockIntegrations[0], ...httpMappingData },
});
});
it('calls `$apollo.mutate` with `updateCurrentPrometheusIntegrationMutation` on PROMETHEUS integration edit', () => {
createComponent({
data: {
integrations: { list: mockIntegrations },
currentIntegration: mockIntegrations[3],
httpIntegrations,
},
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
findIntegrationsList().vm.$emit('edit-integration', updatePrometheusVariables);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateCurrentPrometheusIntegrationMutation,
variables: mockIntegrations[3],
});
});
});
describe('with mocked Apollo client', () => {
......
const projectPath = '';
export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
export const HTTP_ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
export const PROMETHEUS_ID = 'gid://gitlab/PrometheusService/12';
export const errorMsg = 'Something went wrong';
export const createHttpVariables = {
name: 'Test Pre',
active: true,
projectPath,
type: 'HTTP',
};
export const updateHttpVariables = {
name: 'Test Pre',
active: true,
id: ID,
id: HTTP_ID,
type: 'HTTP',
};
export const createPrometheusVariables = {
apiUrl: 'https://test-pre.com',
active: true,
projectPath,
type: 'PROMETHEUS',
};
export const updatePrometheusVariables = {
apiUrl: 'https://test-pre.com',
active: true,
id: ID,
id: PROMETHEUS_ID,
type: 'PROMETHEUS',
};
export const getIntegrationsQueryResponse = {
......@@ -99,6 +104,9 @@ export const destroyIntegrationResponse = {
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
token: '89eb01df471d990ff5162a1c640408cf',
apiUrl: null,
payloadExample: '{"field": "value"}',
payloadAttributeMappings: [],
payloadAlertFields: [],
},
},
},
......@@ -117,6 +125,9 @@ export const destroyIntegrationResponseWithErrors = {
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
token: '89eb01df471d990ff5162a1c640408cf',
apiUrl: null,
payloadExample: '{"field": "value"}',
payloadAttributeMappings: [],
payloadAlertFields: [],
},
},
},
......
{
"payloadAlerFields": [
{
"path": [
"dashboardId"
],
"label": "Dashboard Id",
"type": "string"
},
{
"path": [
"evalMatches"
],
"label": "Eval Matches",
"type": "array"
},
{
"path": [
"createdAt"
],
"label": "Created At",
"type": "datetime"
},
{
"path": [
"imageUrl"
],
"label": "Image Url",
"type": "string"
},
{
"path": [
"message"
],
"label": "Message",
"type": "string"
},
{
"path": [
"orgId"
],
"label": "Org Id",
"type": "string"
},
{
"path": [
"panelId"
],
"label": "Panel Id",
"type": "string"
},
{
"path": [
"ruleId"
],
"label": "Rule Id",
"type": "string"
},
{
"path": [
"ruleName"
],
"label": "Rule Name",
"type": "string"
},
{
"path": [
"ruleUrl"
],
"label": "Rule Url",
"type": "string"
},
{
"path": [
"state"
],
"label": "State",
"type": "string"
},
{
"path": [
"title"
],
"label": "Title",
"type": "string"
},
{
"path": [
"tags",
"tag"
],
"label": "Tags",
"type": "string"
}
],
"payloadAttributeMappings": [
{
"fieldName": "title",
"label": "Title",
"type": "STRING",
"path": ["title"]
},
{
"fieldName": "description",
"label": "description",
"type": "STRING",
"path": ["description"]
},
{
"fieldName": "hosts",
"label": "Host",
"type": "ARRAY",
"path": ["hosts", "host"]
},
{
"fieldName": "startTime",
"label": "Created Atd",
"type": "STRING",
"path": ["time", "createdAt"]
}
]
}
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
import {
getMappingData,
getPayloadFields,
transformForSave,
} from '~/alerts_settings/utils/mapping_transformations';
import alertFields from '../mocks/alertFields.json';
import { getMappingData, transformForSave } from '~/alerts_settings/utils/mapping_transformations';
import alertFields from '../mocks/alert_fields.json';
import parsedMapping from '../mocks/parsed_mapping.json';
describe('Mapping Transformation Utilities', () => {
const nameField = {
label: 'Name',
path: ['alert', 'name'],
type: 'string',
type: 'STRING',
};
const dashboardField = {
label: 'Dashboard Id',
path: ['alert', 'dashboardId'],
type: 'string',
type: 'STRING',
};
describe('getMappingData', () => {
it('should return mapping data', () => {
const result = getMappingData(
alertFields,
getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
parsedMapping.storedMapping.nodes.slice(0, 3),
parsedMapping.payloadAlerFields.slice(0, 3),
parsedMapping.payloadAttributeMappings.slice(0, 3),
);
result.forEach((data, index) => {
......@@ -44,8 +40,8 @@ describe('Mapping Transformation Utilities', () => {
const mockMappingData = [
{
name: fieldName,
mapping: 'alert_name',
mappingFields: getPayloadFields([dashboardField, nameField]),
mapping: ['alert', 'name'],
mappingFields: [dashboardField, nameField],
},
];
const result = transformForSave(mockMappingData);
......@@ -61,21 +57,11 @@ describe('Mapping Transformation Utilities', () => {
{
name: fieldName,
mapping: null,
mappingFields: getPayloadFields([nameField, dashboardField]),
mappingFields: [nameField, dashboardField],
},
];
const result = transformForSave(mockMappingData);
expect(result).toEqual([]);
});
});
describe('getPayloadFields', () => {
it('should add name field to each payload field', () => {
const result = getPayloadFields([nameField, dashboardField]);
expect(result).toEqual([
{ ...nameField, name: 'alert_name' },
{ ...dashboardField, name: 'alert_dashboardId' },
]);
});
});
});
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