Skip to content
Snippets Groups Projects
Commit b21436c7 authored by David O'Regan's avatar David O'Regan
Browse files

Merge branch '262707-integrate-parsing-of-sample-payload-for-mapping' into 'master'

Parse sample payload integration

See merge request gitlab-org/gitlab!53516
parents 225bfcb5 e9c24b9d
No related branches found
No related tags found
No related merge requests found
Showing
with 358 additions and 269 deletions
Loading
Loading
@@ -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: {
Loading
Loading
@@ -33,6 +30,7 @@ export const i18n = {
 
export default {
i18n,
mappingFields,
components: {
GlIcon,
GlFormInput,
Loading
Loading
@@ -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);
Loading
Loading
@@ -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
);
},
Loading
Loading
@@ -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
Loading
Loading
@@ -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"
Loading
Loading
@@ -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>
Loading
Loading
@@ -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"
Loading
Loading
@@ -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>
Loading
Loading
Loading
Loading
@@ -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>
Loading
Loading
@@ -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"
Loading
Loading
Loading
Loading
@@ -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';
Loading
Loading
@@ -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: {
Loading
Loading
@@ -92,7 +91,6 @@ export const i18n = {
};
 
export default {
integrationTypes,
placeholders: {
prometheus: targetPrometheusUrlPlaceholder,
},
Loading
Loading
@@ -128,6 +126,9 @@ export default {
multiIntegrations: {
default: false,
},
projectPath: {
default: '',
},
},
props: {
loading: {
Loading
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: {
Loading
Loading
@@ -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 !== '';
Loading
Loading
@@ -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);
Loading
Loading
@@ -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);
}
Loading
Loading
@@ -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();
Loading
Loading
@@ -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>
Loading
Loading
@@ -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">
Loading
Loading
@@ -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>
Loading
Loading
@@ -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"
Loading
Loading
Loading
Loading
@@ -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,
Loading
Loading
@@ -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,
},
Loading
Loading
@@ -93,6 +118,7 @@ export default {
isUpdating: false,
testAlertPayload: null,
integrations: {},
httpIntegrations: {},
currentIntegration: null,
};
},
Loading
Loading
@@ -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 } = {} } = {}) => {
Loading
Loading
@@ -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,
Loading
Loading
@@ -176,7 +207,7 @@ export default {
return this.validateAlertPayload();
}
 
this.clearCurrentIntegration();
this.clearCurrentIntegration({ type });
 
return createFlash({
message: this.$options.i18n.changesSaved,
Loading
Loading
@@ -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 });
}
Loading
Loading
@@ -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({
Loading
Loading
@@ -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 });
},
Loading
Loading
@@ -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,
Loading
Loading
@@ -282,9 +307,11 @@ export default {
this.isUpdating = false;
});
},
clearCurrentIntegration() {
clearCurrentIntegration({ type }) {
this.$apollo.mutate({
mutation: updateCurrentIntergrationMutation,
mutation: this.isHttp(type)
? updateCurrentHttpIntegrationMutation
: updateCurrentPrometheusIntegrationMutation,
variables: {},
});
},
Loading
Loading
{
"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"
}
]
}
}
Loading
Loading
@@ -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',
Loading
Loading
@@ -68,3 +68,8 @@ export const trackAlertIntegrationsViewsOptions = {
category: 'Alert Integrations',
action: 'view_alert_integrations_list',
};
export const mappingFields = {
mapping: 'mapping',
fallback: 'fallback',
};
Loading
Loading
@@ -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 });
Loading
Loading
@@ -28,6 +39,9 @@ const resolvers = {
type,
url,
apiUrl,
payloadExample,
payloadAttributeMappings,
payloadAlertFields,
};
}
});
Loading
Loading
#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!
Loading
Loading
@@ -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
Loading
Loading
@@ -6,6 +6,7 @@ mutation updateCurrentIntegration(
$type: String
$url: String
$apiUrl: String
$samplePayload: String
) {
updateCurrentIntegration(
id: $id
Loading
Loading
@@ -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
}
}
}
Loading
Loading
@@ -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);
Loading
Loading
@@ -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
Loading
Loading
@@ -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,
Loading
Loading
@@ -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({
Loading
Loading
@@ -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;
Loading
Loading
@@ -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,
},
});
Loading
Loading
@@ -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);
Loading
Loading
@@ -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);
Loading
Loading
@@ -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);
}
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment