Commit 9649d3b4 authored by Mark Florian's avatar Mark Florian

Merge branch '294500-get-alert-fields-from-BE' into 'master'

Get gitlab fields for mapping builder as data-attributes

See merge request gitlab-org/gitlab!52004
parents 60b1bcdc 706be7cd
......@@ -8,17 +8,14 @@ import {
GlSearchBoxByType,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { s__, __ } from '~/locale';
// 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 { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import {
getMappingData,
getPayloadFields,
transformForSave,
} from '../utils/mapping_transformations';
import gitlabFieldsMock from './mocks/gitlabFields.json';
export const i18n = {
columns: {
......@@ -46,12 +43,19 @@ export default {
directives: {
GlTooltip,
},
inject: {
gitlabAlertFields: {
default: gitlabFieldsMock,
props: {
alertFields: {
type: Array,
required: true,
validator: (fields) => {
return (
fields.length &&
fields.every(({ name, types, label }) => {
return typeof name === 'string' && Array.isArray(types) && typeof label === 'string';
})
);
},
},
props: {
parsedPayload: {
type: Array,
required: false,
......@@ -65,7 +69,7 @@ export default {
},
data() {
return {
gitlabFields: this.gitlabAlertFields,
gitlabFields: cloneDeep(this.alertFields),
};
},
computed: {
......@@ -75,6 +79,9 @@ export default {
mappingData() {
return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
},
hasFallbackColumn() {
return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks));
},
},
methods: {
setMapping(gitlabKey, mappingKey, valueKey) {
......@@ -101,10 +108,10 @@ export default {
this.$options.i18n.makeSelection
);
},
getFieldValue({ label, type }) {
const types = type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or '));
getFieldValue({ label, types }) {
const type = types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or '));
return `${label} (${types})`;
return `${label} (${type})`;
},
noResults(searchTerm, fields) {
return !this.filterFields(searchTerm, fields).length;
......@@ -123,7 +130,11 @@ export default {
<h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
{{ $options.i18n.columns.payloadKeyTitle }}
</h5>
<h5 id="fallbackFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
<h5
v-if="hasFallbackColumn"
id="fallbackFieldsHeader"
class="gl-display-table-cell gl-py-3 gl-pr-3"
>
{{ $options.i18n.columns.fallbackKeyTitle }}
<gl-icon
v-gl-tooltip
......
......@@ -125,6 +125,9 @@ export default {
prometheus: {
default: {},
},
multiIntegrations: {
default: false,
},
},
props: {
loading: {
......@@ -135,6 +138,11 @@ export default {
type: Boolean,
required: true,
},
alertFields: {
type: Array,
required: false,
default: null,
},
},
apollo: {
currentIntegration: {
......@@ -196,8 +204,10 @@ export default {
},
showMappingBuilder() {
return (
this.multiIntegrations &&
this.glFeatures.multipleHttpIntegrationsCustomMapping &&
this.selectedIntegration === typeSet.http
this.selectedIntegration === typeSet.http &&
this.alertFields?.length
);
},
parsedSamplePayload() {
......@@ -558,6 +568,7 @@ export default {
<mapping-builder
:parsed-payload="parsedSamplePayload"
:saved-mapping="savedMapping"
:alert-fields="alertFields"
@onMappingUpdate="updateMapping"
/>
</gl-form-group>
......
......@@ -57,6 +57,13 @@ export default {
default: false,
},
},
props: {
alertFields: {
type: Array,
required: false,
default: null,
},
},
apollo: {
integrations: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
......@@ -312,6 +319,7 @@ export default {
<alert-settings-form
:loading="isUpdating"
:can-add-integration="canAddIntegration"
:alert-fields="alertFields"
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
......
......@@ -6,67 +6,67 @@
{
"path": ["dashboardId"],
"label": "Dashboard Id",
"type": "STRING"
"type": "string"
},
{
"path": ["evalMatches"],
"label": "Eval Matches",
"type": "ARRAY"
"type": "array"
},
{
"path": ["createdAt"],
"label": "Created At",
"type": "DATETIME"
"type": "datetime"
},
{
"path": ["imageUrl"],
"label": "Image Url",
"type": "STRING"
"type": "string"
},
{
"path": ["message"],
"label": "Message",
"type": "STRING"
"type": "string"
},
{
"path": ["orgId"],
"label": "Org Id",
"type": "STRING"
"type": "string"
},
{
"path": ["panelId"],
"label": "Panel Id",
"type": "STRING"
"type": "string"
},
{
"path": ["ruleId"],
"label": "Rule Id",
"type": "STRING"
"type": "string"
},
{
"path": ["ruleName"],
"label": "Rule Name",
"type": "STRING"
"type": "string"
},
{
"path": ["ruleUrl"],
"label": "Rule Url",
"type": "STRING"
"type": "string"
},
{
"path": ["state"],
"label": "State",
"type": "STRING"
"type": "string"
},
{
"path": ["title"],
"label": "Title",
"type": "STRING"
"type": "string"
},
{
"path": ["tags", "tag"],
"label": "Tags",
"type": "STRING"
"type": "string"
}
]
}
......
......@@ -31,6 +31,7 @@ export default (el) => {
url,
projectPath,
multiIntegrations,
alertFields,
} = el.dataset;
return new Vue({
......@@ -60,7 +61,14 @@ export default (el) => {
},
apolloProvider,
render(createElement) {
return createElement('alert-settings-wrapper');
return createElement('alert-settings-wrapper', {
props: {
alertFields:
gon.features?.multipleHttpIntegrationsCustomMapping && parseBoolean(multiIntegrations)
? JSON.parse(alertFields)
: null,
},
});
},
});
};
......@@ -10,9 +10,7 @@
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),
);
const mappingFields = payloadFields.filter(({ type }) => gitlabField.types.includes(type));
// find the mapping that was previously stored
const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
......@@ -42,9 +40,9 @@ export const transformForSave = (mappingData) => {
if (mapped) {
const { path, type, label } = mapped;
acc.push({
fieldName: field.name,
fieldName: field.name.toUpperCase(),
path,
type,
type: type.toUpperCase(),
label,
});
}
......
......@@ -19,6 +19,7 @@ RSpec.describe 'Alert integrations settings form', :js do
describe 'when viewing alert integrations as a maintainer' do
context 'with the default page permissions' do
before do
stub_feature_flags(multiple_http_integrations_custom_mapping: false)
visit project_settings_operations_path(project, anchor: 'js-alert-management-settings')
wait_for_requests
end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = `
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
<form
class="gl-mt-6"
>
......
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 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';
import alertFields from '../mocks/alertFields.json';
describe('AlertMappingBuilder', () => {
let wrapper;
......@@ -14,6 +14,7 @@ describe('AlertMappingBuilder', () => {
propsData: {
parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
savedMapping: parsedMapping.storedMapping.nodes,
alertFields,
},
});
}
......@@ -44,28 +45,28 @@ describe('AlertMappingBuilder', () => {
});
it('renders disabled form input for each mapped field', () => {
gitlabFields.forEach((field, index) => {
alertFields.forEach((field, index) => {
const input = findColumnInRow(index + 1, 0).find(GlFormInput);
const types = field.type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
expect(input.attributes('value')).toBe(`${field.label} (${types})`);
expect(input.attributes('disabled')).toBe('');
});
});
it('renders right arrow next to each input', () => {
gitlabFields.forEach((field, index) => {
alertFields.forEach((field, index) => {
const arrow = findColumnInRow(index + 1, 1).find('.right-arrow');
expect(arrow.exists()).toBe(true);
});
});
it('renders mapping dropdown for each field', () => {
gitlabFields.forEach(({ compatibleTypes }, index) => {
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 }) => compatibleTypes.includes(type));
const mappingOptions = nodes.filter(({ type }) => types.includes(type));
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
......@@ -74,7 +75,7 @@ describe('AlertMappingBuilder', () => {
});
it('renders fallback dropdown only for the fields that have fallback', () => {
gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => {
alertFields.forEach(({ types, numberOfFallbacks }, index) => {
const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown);
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
......@@ -82,7 +83,7 @@ describe('AlertMappingBuilder', () => {
const searchBox = dropdown.findComponent(GlSearchBoxByType);
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type));
const mappingOptions = nodes.filter(({ type }) => types.includes(type));
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
expect(dropdownItems).toHaveLength(mappingOptions.length);
......
......@@ -11,9 +11,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
import { typeSet } from '~/alerts_settings/constants';
import alertFields from '../mocks/alertFields.json';
import { defaultAlertSettingsConfig } from './util';
describe('AlertsSettingsFormNew', () => {
describe('AlertsSettingsForm', () => {
let wrapper;
const mockToastShow = jest.fn();
......@@ -21,6 +22,7 @@ describe('AlertsSettingsFormNew', () => {
data = {},
props = {},
multipleHttpIntegrationsCustomMapping = false,
multiIntegrations = true,
} = {}) => {
wrapper = mount(AlertsSettingsForm, {
data() {
......@@ -32,8 +34,9 @@ describe('AlertsSettingsFormNew', () => {
...props,
},
provide: {
glFeatures: { multipleHttpIntegrationsCustomMapping },
...defaultAlertSettingsConfig,
glFeatures: { multipleHttpIntegrationsCustomMapping },
multiIntegrations,
},
mocks: {
$toast: {
......@@ -132,7 +135,11 @@ describe('AlertsSettingsFormNew', () => {
});
it('create with custom mapping', async () => {
createComponent({ multipleHttpIntegrationsCustomMapping: true });
createComponent({
multipleHttpIntegrationsCustomMapping: true,
multiIntegrations: true,
props: { alertFields },
});
const integrationName = 'Test integration';
await selectOptionAtIndex(1);
......@@ -275,6 +282,7 @@ describe('AlertsSettingsFormNew', () => {
currentIntegration: {
type: typeSet.http,
},
alertFields,
},
});
});
......@@ -347,18 +355,27 @@ describe('AlertsSettingsFormNew', () => {
describe('Mapping builder section', () => {
describe.each`
featureFlag | integrationOption | visible
${true} | ${1} | ${true}
${true} | ${2} | ${false}
${false} | ${1} | ${false}
${false} | ${2} | ${false}
`('', ({ featureFlag, integrationOption, visible }) => {
alertFieldsProvided | multiIntegrations | featureFlag | integrationOption | visible
${true} | ${true} | ${true} | ${1} | ${true}
${true} | ${true} | ${true} | ${2} | ${false}
${true} | ${true} | ${false} | ${1} | ${false}
${true} | ${true} | ${false} | ${2} | ${false}
${true} | ${false} | ${true} | ${1} | ${false}
${false} | ${true} | ${true} | ${1} | ${false}
`('', ({ alertFieldsProvided, multiIntegrations, featureFlag, integrationOption, visible }) => {
const visibleMsg = visible ? 'is rendered' : 'is not rendered';
const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled';
const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided';
const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => {
createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag });
it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => {
createComponent({
multipleHttpIntegrationsCustomMapping: featureFlag,
multiIntegrations,
props: {
alertFields: alertFieldsProvided ? alertFields : [],
},
});
await selectOptionAtIndex(integrationOption);
expect(findMappingBuilderSection().exists()).toBe(visible);
......
[
{
"name": "TITLE",
"name": "title",
"label": "Title",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
],
"numberOfFallbacks": 1
},
{
"name": "DESCRIPTION",
"name": "description",
"label": "Description",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
]
},
{
"name": "START_TIME",
"name": "start_time",
"label": "Start time",
"type": [
"DATETIME"
"datetime"
],
"compatibleTypes": [
"NUMBER",
"DATETIME"
"types": [
"number",
"datetime"
]
},
{
"name": "END_TIME",
"name": "end_time",
"label": "End time",
"type": [
"DATETIME"
"datetime"
],
"compatibleTypes": [
"NUMBER",
"DATETIME"
"types": [
"number",
"datetime"
]
},
{
"name": "SERVICE",
"name": "service",
"label": "Service",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
]
},
{
"name": "MONITORING_TOOL",
"name": "monitoring_tool",
"label": "Monitoring tool",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
]
},
{
"name": "HOSTS",
"name": "hosts",
"label": "Hosts",
"type": [
"STRING",
"string",
"ARRAY"
],
"compatibleTypes": [
"STRING",
"types": [
"string",
"ARRAY",
"NUMBER",
"DATETIME"
"number",
"datetime"
]
},
{
"name": "SEVERITY",
"name": "severity",
"label": "Severity",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
]
},
{
"name": "FINGERPRINT",
"name": "fingerprint",
"label": "Fingerprint",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
]
},
{
"name": "GITLAB_ENVIRONMENT_NAME",
"name": "gitlab_environment_name",
"label": "Environment",
"type": [
"STRING"
"string"
],
"compatibleTypes": [
"STRING",
"NUMBER",
"DATETIME"
"types": [
"string",
"number",
"datetime"
]
}
]
......@@ -3,24 +3,23 @@ import {
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';
import alertFields from '../mocks/alertFields.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 alertFields = gitlabFieldsMock.slice(0, 3);
const result = getMappingData(
alertFields,
getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
......@@ -51,7 +50,9 @@ describe('Mapping Transformation Utilities', () => {
];
const result = transformForSave(mockMappingData);
const { path, type, label } = nameField;
expect(result).toEqual([{ fieldName, path, type, label }]);
expect(result).toEqual([
{ fieldName: fieldName.toUpperCase(), path, type: type.toUpperCase(), label },
]);
});
it('should return empty array if no mapping provided', () => {
......
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