diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-24 00:10:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-24 00:10:44 +0300 |
commit | 5e450e9022861d03048cc733c20585ad0891f5aa (patch) | |
tree | 6a5eb2f639fe66b3fa52008e2f99c31e1ce2d60b /app/assets/javascripts/alerts_settings | |
parent | c37dd28c4afd33fee46cff8ddfdada8a3f54564c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/alerts_settings')
19 files changed, 343 insertions, 256 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 1135562834a..07b2e59671e 100644 --- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -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> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index 6cfb4601192..3b8febfbb29 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -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" 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 18372c54b84..ce595e848de 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -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" 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 366f2209fb2..f56dc05a5d2 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -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: {}, }); }, diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json deleted file mode 100644 index 80fbebf2a60..00000000000 --- a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "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" - } - ] - } -} diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index ecd7c921b2f..cb3b4d2fc09 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -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', +}; diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js index 5fd05169533..c6c19d26adb 100644 --- a/app/assets/javascripts/alerts_settings/graphql.js +++ b/app/assets/javascripts/alerts_settings/graphql.js @@ -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, }; } }); diff --git a/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_item.fragment.graphql b/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_item.fragment.graphql new file mode 100644 index 00000000000..36446bbfe47 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_item.fragment.graphql @@ -0,0 +1,7 @@ +#import "./integration_item.fragment.graphql" +#import "./http_integration_payload_data.fragment.graphql" + +fragment HttpIntegrationItem on AlertManagementHttpIntegration { + ...IntegrationItem + ...HttpIntegrationPayloadData +} diff --git a/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql b/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql new file mode 100644 index 00000000000..b7ea50ebc44 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql @@ -0,0 +1,14 @@ +fragment HttpIntegrationPayloadData on AlertManagementHttpIntegration { + payloadExample + payloadAttributeMappings { + fieldName + path + type + label + } + payloadAlertFields { + path + type + label + } +} 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 f3fc10b4bd4..0c7d7627b6a 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,4 +1,4 @@ -#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 } } } diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql index 0a49c140e6a..a3a50651fd0 100644 --- a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql @@ -1,10 +1,10 @@ -#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 } } } diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql index 178d1e13047..c0754d8e32b 100644 --- a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql @@ -1,10 +1,10 @@ -#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 } } } diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql new file mode 100644 index 00000000000..5f3d305993c --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql @@ -0,0 +1,25 @@ +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 +} diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_intergration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql index 3505241309e..5bd63820629 100644 --- a/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_intergration.mutation.graphql +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql @@ -1,4 +1,4 @@ -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 } diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql index bb5b334deeb..631937048c2 100644 --- a/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql @@ -1,10 +1,24 @@ -#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 } } } diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql new file mode 100644 index 00000000000..e50dd89347f --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql @@ -0,0 +1,13 @@ +#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 + } + } + } +} diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql new file mode 100644 index 00000000000..159b2661f0b --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql @@ -0,0 +1,9 @@ +query parsePayloadFields($projectPath: ID!, $payload: String!) { + project(fullPath: $projectPath) { + alertManagementPayloadFields(payloadExample: $payload) { + path + label + type + } + } +} diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js index 758f3eb6dd4..c29160c1e39 100644 --- a/app/assets/javascripts/alerts_settings/utils/cache_updates.js +++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js @@ -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); + } +}; diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js index a86103540c0..5c4b9bcd505 100644 --- a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js +++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js @@ -1,3 +1,4 @@ +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('_') })); -}; |