diff options
Diffstat (limited to 'app/assets/javascripts/alerts_settings')
9 files changed, 208 insertions, 225 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue index c52e9f5c264..1135562834a 100644 --- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -1,5 +1,4 @@ <script> -import Vue from 'vue'; import { GlIcon, GlFormInput, @@ -8,11 +7,15 @@ import { GlSearchBoxByType, GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import Vue from 'vue'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; 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 gitlabFieldsMock from './mocks/gitlabFields.json'; +import { + getMappingData, + getPayloadFields, + transformForSave, +} from '../utils/mapping_transformations'; export const i18n = { columns: { @@ -40,18 +43,25 @@ export default { directives: { GlTooltip, }, - inject: { - gitlabAlertFields: { - default: gitlabFieldsMock, - }, - }, props: { - payloadFields: { + 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'; + }) + ); + }, + }, + parsedPayload: { type: Array, required: false, default: () => [], }, - mapping: { + savedMapping: { type: Array, required: false, default: () => [], @@ -59,31 +69,18 @@ export default { }, data() { return { - gitlabFields: this.gitlabAlertFields, + gitlabFields: cloneDeep(this.alertFields), }; }, 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); + }, + hasFallbackColumn() { + return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks)); }, }, methods: { @@ -91,6 +88,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 +97,6 @@ export default { }, filterFields(searchTerm = '', fields) { const search = searchTerm.toLowerCase(); - return fields.filter((field) => field.label.toLowerCase().includes(search)); }, isSelected(fieldValue, mapping) { @@ -111,8 +108,10 @@ export default { this.$options.i18n.makeSelection ); }, - getFieldValue({ label, type }) { - return `${label} (${type.join(__(' or '))})`; + getFieldValue({ label, types }) { + const type = types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or ')); + + return `${label} (${type})`; }, noResults(searchTerm, fields) { return !this.filterFields(searchTerm, fields).length; @@ -131,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 diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue index 1ae7f826ce6..18372c54b84 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -12,18 +12,18 @@ import { GlModalDirective, GlToggle, } from '@gitlab/ui'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import MappingBuilder from './alert_mapping_builder.vue'; -import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue'; -import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { integrationTypes, JSON_VALIDATE_DELAY, targetPrometheusUrlPlaceholder, typeSet, } from '../constants'; +import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.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 @@ -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: { @@ -152,6 +160,7 @@ export default { }, resetSamplePayloadConfirmed: false, customMapping: null, + mapping: [], parsingPayload: false, currentIntegration: null, }; @@ -195,14 +204,16 @@ export default { }, showMappingBuilder() { return ( + this.multiIntegrations && this.glFeatures.multipleHttpIntegrationsCustomMapping && - this.selectedIntegration === typeSet.http + this.selectedIntegration === typeSet.http && + this.alertFields?.length ); }, - mappingBuilderFields() { + parsedSamplePayload() { return this.customMapping?.samplePayload?.payloadAlerFields?.nodes; }, - mappingBuilderMapping() { + savedMapping() { return this.customMapping?.storedMapping?.nodes; }, hasSamplePayload() { @@ -255,9 +266,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 +358,9 @@ export default { this.integrationTestPayload.json = res?.samplePayload.body; }); }, + updateMapping(mapping) { + this.mapping = mapping; + }, }, }; </script> @@ -541,8 +566,10 @@ export default { > <span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span> <mapping-builder - :payload-fields="mappingBuilderFields" - :mapping="mappingBuilderMapping" + :parsed-payload="parsedSamplePayload" + :saved-mapping="savedMapping" + :alert-fields="alertFields" + @onMappingUpdate="updateMapping" /> </gl-form-group> </div> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index d0cac066ffa..366f2209fb2 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -1,21 +1,19 @@ <script> -import { s__ } from '~/locale'; -import { fetchPolicies } from '~/lib/graphql'; import createFlash, { FLASH_TYPES } from '~/flash'; -import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql'; -import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; +import { fetchPolicies } from '~/lib/graphql'; +import { s__ } from '~/locale'; +import { typeSet } from '../constants'; import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql'; import createPrometheusIntegrationMutation from '../graphql/mutations/create_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 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 IntegrationsList from './alerts_integrations_list.vue'; -import AlertSettingsForm from './alerts_settings_form.vue'; +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 getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql'; import service from '../services'; -import { typeSet } from '../constants'; import { updateStoreAfterIntegrationDelete, updateStoreAfterIntegrationAdd, @@ -27,6 +25,8 @@ import { UPDATE_INTEGRATION_ERROR, INTEGRATION_PAYLOAD_TEST_ERROR, } from '../utils/error_messages'; +import IntegrationsList from './alerts_integrations_list.vue'; +import AlertSettingsForm from './alerts_settings_form.vue'; export default { typeSet, @@ -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" diff --git a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json deleted file mode 100644 index ac559a30eda..00000000000 --- a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json +++ /dev/null @@ -1,112 +0,0 @@ -[ - { - "name": "title", - "label": "Title", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ], - "numberOfFallbacks": 1 - }, - { - "name": "description", - "label": "Description", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ] - }, - { - "name": "startTime", - "label": "Start time", - "type": [ - "DateTime" - ], - "compatibleTypes": [ - "Number", - "DateTime" - ] - }, - { - "name": "service", - "label": "Service", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ] - }, - { - "name": "monitoringTool", - "label": "Monitoring tool", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ] - }, - { - "name": "hosts", - "label": "Hosts", - "type": [ - "String", - "Array" - ], - "compatibleTypes": [ - "String", - "Array", - "Number", - "DateTime" - ] - }, - { - "name": "severity", - "label": "Severity", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ] - }, - { - "name": "fingerprint", - "label": "Fingerprint", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ] - }, - { - "name": "environment", - "label": "Environment", - "type": [ - "String" - ], - "compatibleTypes": [ - "String", - "Number", - "DateTime" - ] - } -] diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json index 5326678155d..80fbebf2a60 100644 --- a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json +++ b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json @@ -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" } ] } diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js index 3dbfa69a8e9..5fd05169533 100644 --- a/app/assets/javascripts/alerts_settings/graphql.js +++ b/app/assets/javascripts/alerts_settings/graphql.js @@ -1,5 +1,5 @@ -import Vue from 'vue'; import produce from 'immer'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import getCurrentIntegrationQuery from './graphql/queries/get_current_integration.query.graphql'; diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql index d1dacbad40a..f3fc10b4bd4 100644 --- a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql @@ -1,7 +1,21 @@ #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 diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index 85858956987..8506b3fda01 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -1,5 +1,5 @@ -import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; +import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import AlertSettingsWrapper from './components/alerts_settings_wrapper.vue'; import apolloProvider from './graphql'; @@ -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, + }, + }); }, }); }; diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js new file mode 100644 index 00000000000..a86103540c0 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js @@ -0,0 +1,61 @@ +/** + * 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.types.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.toUpperCase(), + path, + type: type.toUpperCase(), + 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('_') })); +}; |