Commit cc4e8fd5 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Ezekiel Kigbo

Integrate mapping creation with BE

parent c200cda7
......@@ -13,6 +13,12 @@ import { s__, __ } from '~/locale';
// 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 gitlabFieldsMock from './mocks/gitlabFields.json';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import {
getMappingData,
getPayloadFields,
transformForSave,
} from '../utils/mapping_transformations';
export const i18n = {
columns: {
......@@ -46,12 +52,12 @@ export default {
},
},
props: {
payloadFields: {
parsedPayload: {
type: Array,
required: false,
default: () => [],
},
mapping: {
savedMapping: {
type: Array,
required: false,
default: () => [],
......@@ -63,27 +69,11 @@ export default {
};
},
computed: {
payloadFields() {
return getPayloadFields(this.parsedPayload);
},
mappingData() {
return this.gitlabFields.map((gitlabField) => {
const mappingFields = this.payloadFields.filter(({ type }) =>
type.some((t) => gitlabField.compatibleTypes.includes(t)),
);
const foundMapping = this.mapping.find(
({ alertFieldName }) => alertFieldName === gitlabField.name,
);
const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
return {
mapping: payloadAlertPaths,
fallback: fallbackAlertPaths,
searchTerm: '',
fallbackSearchTerm: '',
mappingFields,
...gitlabField,
};
});
return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
},
},
methods: {
......@@ -91,6 +81,7 @@ export default {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
this.$emit('onMappingUpdate', transformForSave(this.mappingData));
},
setSearchTerm(search = '', searchFieldKey, gitlabKey) {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
......@@ -99,7 +90,6 @@ export default {
},
filterFields(searchTerm = '', fields) {
const search = searchTerm.toLowerCase();
return fields.filter((field) => field.label.toLowerCase().includes(search));
},
isSelected(fieldValue, mapping) {
......@@ -112,7 +102,9 @@ export default {
);
},
getFieldValue({ label, type }) {
return `${label} (${type.join(__(' or '))})`;
const types = type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or '));
return `${label} (${types})`;
},
noResults(searchTerm, fields) {
return !this.filterFields(searchTerm, fields).length;
......
......@@ -152,6 +152,7 @@ export default {
},
resetSamplePayloadConfirmed: false,
customMapping: null,
mapping: [],
parsingPayload: false,
currentIntegration: null,
};
......@@ -199,10 +200,10 @@ export default {
this.selectedIntegration === typeSet.http
);
},
mappingBuilderFields() {
parsedSamplePayload() {
return this.customMapping?.samplePayload?.payloadAlerFields?.nodes;
},
mappingBuilderMapping() {
savedMapping() {
return this.customMapping?.storedMapping?.nodes;
},
hasSamplePayload() {
......@@ -255,9 +256,20 @@ export default {
},
submit() {
const { name, apiUrl } = this.integrationForm;
const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
? {
payloadAttributeMappings: this.mapping,
payloadExample: this.integrationTestPayload.json,
}
: {};
const variables =
this.selectedIntegration === typeSet.http
? { name, active: this.active }
? {
name,
active: this.active,
...customMappingVariables,
}
: { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables };
......@@ -336,6 +348,9 @@ export default {
this.integrationTestPayload.json = res?.samplePayload.body;
});
},
updateMapping(mapping) {
this.mapping = mapping;
},
},
};
</script>
......@@ -541,8 +556,9 @@ export default {
>
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<mapping-builder
:payload-fields="mappingBuilderFields"
:mapping="mappingBuilderMapping"
:parsed-payload="parsedSamplePayload"
:saved-mapping="savedMapping"
@onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
......
[
{
"name": "title",
"name": "TITLE",
"label": "Title",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
],
"numberOfFallbacks": 1
},
{
"name": "description",
"name": "DESCRIPTION",
"label": "Description",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
]
},
{
"name": "startTime",
"name": "START_TIME",
"label": "Start time",
"type": [
"DateTime"
"DATETIME"
],
"compatibleTypes": [
"Number",
"DateTime"
"NUMBER",
"DATETIME"
]
},
{
"name": "service",
"name": "END_TIME",
"label": "End time",
"type": [
"DATETIME"
],
"compatibleTypes": [
"NUMBER",
"DATETIME"
]
},
{
"name": "SERVICE",
"label": "Service",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
]
},
{
"name": "monitoringTool",
"name": "MONITORING_TOOL",
"label": "Monitoring tool",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
]
},
{
"name": "hosts",
"name": "HOSTS",
"label": "Hosts",
"type": [
"String",
"Array"
"STRING",
"ARRAY"
],
"compatibleTypes": [
"String",
"Array",
"Number",
"DateTime"
"STRING",
"ARRAY",
"NUMBER",
"DATETIME"
]
},
{
"name": "severity",
"name": "SEVERITY",
"label": "Severity",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
]
},
{
"name": "fingerprint",
"name": "FINGERPRINT",
"label": "Fingerprint",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
]
},
{
"name": "environment",
"name": "GITLAB_ENVIRONMENT_NAME",
"label": "Environment",
"type": [
"String"
"STRING"
],
"compatibleTypes": [
"String",
"Number",
"DateTime"
"STRING",
"NUMBER",
"DATETIME"
]
}
]
......@@ -4,95 +4,69 @@
"payloadAlerFields": {
"nodes": [
{
"name": "dashboardId",
"path": ["dashboardId"],
"label": "Dashboard Id",
"type": [
"Number"
]
"type": "STRING"
},
{
"name": "evalMatches",
"path": ["evalMatches"],
"label": "Eval Matches",
"type": [
"Array"
]
"type": "ARRAY"
},
{
"name": "createdAt",
"path": ["createdAt"],
"label": "Created At",
"type": [
"DateTime"
]
"type": "DATETIME"
},
{
"name": "imageUrl",
"path": ["imageUrl"],
"label": "Image Url",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "message",
"path": ["message"],
"label": "Message",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "orgId",
"path": ["orgId"],
"label": "Org Id",
"type": [
"Number"
]
"type": "STRING"
},
{
"name": "panelId",
"path": ["panelId"],
"label": "Panel Id",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "ruleId",
"path": ["ruleId"],
"label": "Rule Id",
"type": [
"Number"
]
"type": "STRING"
},
{
"name": "ruleName",
"path": ["ruleName"],
"label": "Rule Name",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "ruleUrl",
"path": ["ruleUrl"],
"label": "Rule Url",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "state",
"path": ["state"],
"label": "State",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "title",
"path": ["title"],
"label": "Title",
"type": [
"String"
]
"type": "STRING"
},
{
"name": "tags",
"path": ["tags", "tag"],
"label": "Tags",
"type": [
"Object"
]
"type": "STRING"
}
]
}
......
#import "../fragments/integration_item.fragment.graphql"
mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
mutation createHttpIntegration(
$projectPath: ID!
$name: String!
$active: Boolean!
$payloadExample: JsonString
$payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
) {
httpIntegrationCreate(
input: {
projectPath: $projectPath
name: $name
active: $active
payloadExample: $payloadExample
payloadAttributeMappings: $payloadAttributeMappings
}
) {
errors
integration {
...IntegrationItem
......
/**
* 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
* @param {Object} gitlabFields - structure describing GitLab alert fields
* @param {Object} payloadFields - parsed from sample JSON sample alert fields
* @param {Object} savedMapping - GitLab fields to parsed fields mapping
*
* @return {Object} mapping data for UI mapping builder
*/
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.compatibleTypes.includes(type),
);
// find the mapping that was previously stored
const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
return {
mapping: payloadAlertPaths,
fallback: fallbackAlertPaths,
searchTerm: '',
fallbackSearchTerm: '',
mappingFields,
...gitlabField,
};
});
};
/**
* Based on mapping data configured by the user creates an object in a format suitable for save on BE
* @param {Object} mappingData - structure describing mapping between GitLab fields and parsed payload fields
*
* @return {Object} mapping data to send to BE
*/
export const transformForSave = (mappingData) => {
return mappingData.reduce((acc, field) => {
const mapped = field.mappingFields.find(({ name }) => name === field.mapping);
if (mapped) {
const { path, type, label } = mapped;
acc.push({
fieldName: field.name,
path,
type,
label,
});
}
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('_') }));
};
......@@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
describe('AlertMappingBuilder', () => {
let wrapper;
......@@ -10,8 +12,8 @@ describe('AlertMappingBuilder', () => {
function mountComponent() {
wrapper = shallowMount(AlertMappingBuilder, {
propsData: {
payloadFields: parsedMapping.samplePayload.payloadAlerFields.nodes,
mapping: parsedMapping.storedMapping.nodes,
parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
savedMapping: parsedMapping.storedMapping.nodes,
},
});
}
......@@ -44,7 +46,8 @@ describe('AlertMappingBuilder', () => {
it('renders disabled form input for each mapped field', () => {
gitlabFields.forEach((field, index) => {
const input = findColumnInRow(index + 1, 0).find(GlFormInput);
expect(input.attributes('value')).toBe(`${field.label} (${field.type.join(' or ')})`);
const types = field.type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
expect(input.attributes('value')).toBe(`${field.label} (${types})`);
expect(input.attributes('disabled')).toBe('');
});
});
......@@ -59,16 +62,14 @@ describe('AlertMappingBuilder', () => {
it('renders mapping dropdown for each field', () => {
gitlabFields.forEach(({ compatibleTypes }, index) => {
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
const searchBox = dropdown.find(GlSearchBoxByType);
const dropdownItems = dropdown.findAll(GlDropdownItem);
const searchBox = dropdown.findComponent(GlSearchBoxByType);
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
const numberOfMappingOptions = nodes.filter(({ type }) =>
type.some((t) => compatibleTypes.includes(t)),
);
const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type));
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
expect(dropdownItems).toHaveLength(mappingOptions.length);
});
});
......@@ -78,16 +79,23 @@ describe('AlertMappingBuilder', () => {
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
if (numberOfFallbacks) {
const searchBox = dropdown.find(GlSearchBoxByType);
const dropdownItems = dropdown.findAll(GlDropdownItem);
const searchBox = dropdown.findComponent(GlSearchBoxByType);
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
const numberOfMappingOptions = nodes.filter(({ type }) =>
type.some((t) => compatibleTypes.includes(t)),
);
const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type));
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
expect(dropdownItems).toHaveLength(mappingOptions.length);
}
});
});
it('emits event with selected mapping', () => {
const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' };
jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave);
const dropdown = findColumnInRow(1, 2).find(GlDropdown);
const option = dropdown.find(GlDropdownItem);
option.vm.$emit('click');
expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]);
});
});
import {
getMappingData,
getPayloadFields,
transformForSave,
} from '~/alerts_settings/utils/mapping_transformations';
import gitlabFieldsMock from '~/alerts_settings/components/mocks/gitlabFields.json';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
describe('Mapping Transformation Utilities', () => {
const nameField = {
label: 'Name',
path: ['alert', 'name'],
type: 'STRING',
};
const dashboardField = {
label: 'Dashboard Id',
path: ['alert', 'dashboardId'],
type: 'STRING',
};
describe('getMappingData', () => {
it('should return mapping data', () => {
const alertFields = gitlabFieldsMock.slice(0, 3);
const result = getMappingData(
alertFields,
getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
parsedMapping.storedMapping.nodes.slice(0, 3),
);
result.forEach((data, index) => {
expect(data).toEqual(
expect.objectContaining({
...alertFields[index],
searchTerm: '',
fallbackSearchTerm: '',
}),
);
});
});
});
describe('transformForSave', () => {
it('should transform mapped data for save', () => {
const fieldName = 'title';
const mockMappingData = [
{
name: fieldName,
mapping: 'alert_name',
mappingFields: getPayloadFields([dashboardField, nameField]),
},
];
const result = transformForSave(mockMappingData);
const { path, type, label } = nameField;
expect(result).toEqual([{ fieldName, path, type, label }]);
});
it('should return empty array if no mapping provided', () => {
const fieldName = 'title';
const mockMappingData = [
{
name: fieldName,
mapping: null,
mappingFields: getPayloadFields([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