diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-26 00:09:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-26 00:09:13 +0300 |
commit | 659ad26b4e0e1f792ab6d15ac8d6e47ec943e214 (patch) | |
tree | 5e395d172b681cfe396c671ebf7eef8789ef7334 /app/assets/javascripts/alerts_settings | |
parent | e93121b42cb4ca672c893c6be0a8af0c8d9f7987 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/alerts_settings')
7 files changed, 408 insertions, 272 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 07b2e59671e..5171588eb64 100644 --- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -118,17 +118,17 @@ export default { <template> <div class="gl-display-table gl-w-full gl-mt-5"> <div class="gl-display-table-row"> - <h5 id="gitlabFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3"> + <h5 id="gitlabFieldsHeader" class="gl-display-table-cell gl-pb-3 gl-pr-3"> {{ $options.i18n.columns.gitlabKeyTitle }} </h5> - <h5 class="gl-display-table-cell gl-py-3 gl-pr-3"> </h5> - <h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3"> + <h5 class="gl-display-table-cell gl-pb-3 gl-pr-3"> </h5> + <h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-pb-3 gl-pr-3"> {{ $options.i18n.columns.payloadKeyTitle }} </h5> <h5 v-if="hasFallbackColumn" id="fallbackFieldsHeader" - class="gl-display-table-cell gl-py-3 gl-pr-3" + class="gl-display-table-cell gl-pb-3 gl-pr-3" > {{ $options.i18n.columns.fallbackKeyTitle }} <gl-icon @@ -140,11 +140,7 @@ export default { </h5> </div> - <div - v-for="(gitlabField, index) in mappingData" - :key="gitlabField.name" - class="gl-display-table-row" - > + <div v-for="gitlabField in mappingData" :key="gitlabField.name" class="gl-display-table-row"> <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" @@ -153,8 +149,8 @@ export default { /> </div> - <div class="gl-display-table-cell gl-py-3 gl-pr-3"> - <div class="right-arrow" :class="{ 'gl-vertical-align-middle': index === 0 }"> + <div class="gl-display-table-cell gl-pr-3 gl-vertical-align-middle"> + <div class="right-arrow"> <i class="right-arrow-head"></i> </div> </div> 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 a5e17d80f86..a071e011ec3 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -139,7 +139,7 @@ export default { <template> <div class="incident-management-list"> - <h5 class="gl-font-lg">{{ $options.i18n.title }}</h5> + <h5 class="gl-font-lg gl-mt-5">{{ $options.i18n.title }}</h5> <gl-table class="integration-list" :items="integrations" 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 5d9513e5b53..bf92da66b27 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -14,7 +14,7 @@ import { GlTab, } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; -import { isEmpty, omit } from 'lodash'; +import { isEqual, isEmpty, omit } from 'lodash'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { integrationTypes, @@ -24,8 +24,9 @@ import { JSON_VALIDATE_DELAY, targetPrometheusUrlPlaceholder, typeSet, - viewCredentialsTabIndex, i18n, + tabIndices, + testAlertModalId, } from '../constants'; import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql'; @@ -40,6 +41,10 @@ export default { typeSet, integrationSteps, i18n, + primaryProps: { text: i18n.integrationFormSteps.testPayload.savedAndTest }, + secondaryProps: { text: i18n.integrationFormSteps.testPayload.proceedWithoutSave }, + cancelProps: { text: i18n.integrationFormSteps.testPayload.cancel }, + testAlertModalId, components: { ClipboardButton, GlButton, @@ -60,11 +65,8 @@ export default { GlModal: GlModalDirective, }, inject: { - generic: { - default: {}, - }, - prometheus: { - default: {}, + alertsUsageUrl: { + default: '#', }, multiIntegrations: { default: false, @@ -87,6 +89,11 @@ export default { required: false, default: null, }, + tabIndex: { + type: Number, + required: false, + default: tabIndices.configureDetails, + }, }, apollo: { currentIntegration: { @@ -96,11 +103,10 @@ export default { data() { return { integrationTypesOptions: Object.values(integrationTypes), - selectedIntegration: integrationTypes.none.value, - active: false, samplePayload: { json: null, error: null, + loading: false, }, testPayload: { json: null, @@ -108,18 +114,32 @@ export default { }, resetPayloadAndMappingConfirmed: false, mapping: [], - parsingPayload: false, + integrationForm: { + active: false, + type: integrationTypes.none.value, + name: '', + token: '', + url: '', + apiUrl: '', + }, + activeTabIndex: this.tabIndex, currentIntegration: null, parsedPayload: [], - activeTabIndex: 0, + validationState: { + name: true, + apiUrl: true, + }, }; }, computed: { isPrometheus() { - return this.selectedIntegration === this.$options.typeSet.prometheus; + return this.integrationForm.type === typeSet.prometheus; }, isHttp() { - return this.selectedIntegration === this.$options.typeSet.http; + return this.integrationForm.type === typeSet.http; + }, + isNone() { + return !this.isHttp && !this.isPrometheus; }, isCreating() { return !this.currentIntegration; @@ -130,29 +150,6 @@ export default { isTestPayloadValid() { return this.testPayload.error === null; }, - selectedIntegrationType() { - switch (this.selectedIntegration) { - case typeSet.http: - return this.generic; - case typeSet.prometheus: - return this.prometheus; - default: - return {}; - } - }, - integrationForm() { - return { - name: this.currentIntegration?.name || '', - active: this.currentIntegration?.active || false, - token: - this.currentIntegration?.token || - (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.token : ''), - url: - this.currentIntegration?.url || - (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.url : ''), - apiUrl: this.currentIntegration?.apiUrl || '', - }; - }, testAlertPayload() { return { data: this.testPayload.json, @@ -170,13 +167,7 @@ export default { return this.hasSamplePayload && !this.resetPayloadAndMappingConfirmed; }, canParseSamplePayload() { - return !this.active || !this.isSampePayloadValid || !this.samplePayload.json; - }, - isResetAuthKeyDisabled() { - return !this.active && !this.integrationForm.token !== ''; - }, - isPayloadEditDisabled() { - return !this.active || this.canEditPayload; + return this.isSampePayloadValid && this.samplePayload.json; }, isSelectDisabled() { return this.currentIntegration !== null || !this.canAddIntegration; @@ -186,30 +177,101 @@ export default { ? i18n.integrationFormSteps.setupCredentials.prometheusHelp : i18n.integrationFormSteps.setupCredentials.help; }, + isFormValid() { + return ( + Object.values(this.validationState).every(Boolean) && + !this.isNone && + this.isSampePayloadValid + ); + }, + isFormDirty() { + const { type, active, name, apiUrl, payloadAlertFields = [], payloadAttributeMappings = [] } = + this.currentIntegration || {}; + const { + name: formName, + apiUrl: formApiUrl, + active: formActive, + type: formType, + } = this.integrationForm; + + const isDirty = + type !== formType || + active !== formActive || + name !== formName || + apiUrl !== formApiUrl || + !isEqual(this.parsedPayload, payloadAlertFields) || + !isEqual(this.mapping, this.getCleanMapping(payloadAttributeMappings)); + + return isDirty; + }, + canSubmitForm() { + return this.isFormValid && this.isFormDirty; + }, + dataForSave() { + const { name, apiUrl, active } = this.integrationForm; + const customMappingVariables = { + payloadAttributeMappings: this.mapping, + payloadExample: this.samplePayload.json || '{}', + }; + + const variables = this.isHttp + ? { name, active, ...customMappingVariables } + : { apiUrl, active }; + + return { type: this.integrationForm.type, variables }; + }, + testAlertModal() { + return this.isFormDirty ? testAlertModalId : null; + }, }, watch: { + tabIndex(val) { + this.activeTabIndex = val; + }, currentIntegration(val) { if (val === null) { this.reset(); return; } - const { type, active, payloadExample, payloadAlertFields, payloadAttributeMappings } = val; - this.selectedIntegration = type; - this.active = active; - if (type === typeSet.http && this.showMappingBuilder) { + this.resetPayloadAndMapping(); + const { + name, + type, + active, + url, + apiUrl, + token, + payloadExample, + payloadAlertFields, + payloadAttributeMappings, + } = val; + this.integrationForm = { type, name, active, url, apiUrl, token }; + + if (this.showMappingBuilder) { + this.resetPayloadAndMappingConfirmed = false; this.parsedPayload = payloadAlertFields; this.samplePayload.json = this.isValidNonEmptyJSON(payloadExample) ? payloadExample : null; - const mapping = payloadAttributeMappings.map((mappingItem) => - omit(mappingItem, '__typename'), - ); - this.updateMapping(mapping); + this.updateMapping(this.getCleanMapping(payloadAttributeMappings)); } - this.activeTabIndex = viewCredentialsTabIndex; this.$el.scrollIntoView({ block: 'center' }); }, }, methods: { + getCleanMapping(mapping) { + return mapping.map((mappingItem) => omit(mappingItem, '__typename')); + }, + validateName() { + this.validationState.name = Boolean(this.integrationForm.name?.length); + }, + validateApiUrl() { + try { + const parsedUrl = new URL(this.integrationForm.apiUrl); + this.validationState.apiUrl = ['http:', 'https:'].includes(parsedUrl.protocol); + } catch (e) { + this.validationState.apiUrl = false; + } + }, isValidNonEmptyJSON(JSONString) { if (JSONString) { let parsed; @@ -222,29 +284,24 @@ export default { } return false; }, + triggerValidation() { + if (this.isHttp) { + this.validationState.apiUrl = true; + this.validateName(); + } else if (this.isPrometheus) { + this.validationState.name = true; + this.validateApiUrl(); + } + }, sendTestAlert() { this.$emit('test-alert-payload', this.testAlertPayload); }, - submit() { - const { name, apiUrl } = this.integrationForm; - const customMappingVariables = { - payloadAttributeMappings: this.mapping, - payloadExample: this.samplePayload.json || '{}', - }; - - const variables = - this.selectedIntegration === typeSet.http - ? { name, active: this.active, ...customMappingVariables } - : { apiUrl, active: this.active }; - - const integrationPayload = { type: this.selectedIntegration, variables }; - - if (this.currentIntegration) { - return this.$emit('update-integration', integrationPayload); - } - - this.reset(); - return this.$emit('create-new-integration', integrationPayload); + saveAndSendTestAlert() { + this.$emit('save-and-test-alert-payload', this.dataForSave, this.testAlertPayload); + }, + submit(testAfterSubmit = false) { + const event = this.currentIntegration ? 'update-integration' : 'create-new-integration'; + this.$emit(event, this.dataForSave, testAfterSubmit); }, reset() { this.resetFormValues(); @@ -252,14 +309,14 @@ export default { this.$emit('clear-current-integration', { type: this.currentIntegration?.type }); }, resetFormValues() { - this.selectedIntegration = integrationTypes.none.value; + this.integrationForm.type = integrationTypes.none.value; this.integrationForm.name = ''; + this.integrationForm.active = false; this.integrationForm.apiUrl = ''; this.samplePayload = { json: null, error: null, }; - this.active = false; }, resetAuthKey() { if (!this.currentIntegration) { @@ -267,7 +324,7 @@ export default { } this.$emit('reset-token', { - type: this.selectedIntegration, + type: this.integrationForm.type, variables: { id: this.currentIntegration.id }, }); }, @@ -285,8 +342,8 @@ export default { payload.error = JSON.stringify(e.message); } }, - parseMapping() { - this.parsingPayload = true; + parseSamplePayload() { + this.samplePayload.loading = true; return this.$apollo .query({ @@ -303,7 +360,7 @@ export default { this.resetPayloadAndMappingConfirmed = false; this.$toast.show( - this.$options.i18n.integrationFormSteps.setSamplePayload.payloadParsedSucessMsg, + this.$options.i18n.integrationFormSteps.mapFields.payloadParsedSucessMsg, ); }, ) @@ -311,7 +368,7 @@ export default { this.samplePayload.error = message; }) .finally(() => { - this.parsingPayload = false; + this.samplePayload.loading = false; }); }, updateMapping(mapping) { @@ -338,7 +395,7 @@ export default { <template> <gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset"> <gl-tabs v-model="activeTabIndex"> - <gl-tab :title="$options.i18n.integrationTabs.configureDetails"> + <gl-tab :title="$options.i18n.integrationTabs.configureDetails" class="gl-mt-3"> <gl-form-group v-if="isCreating" id="integration-type" @@ -351,10 +408,11 @@ export default { label-for="integration-type" > <gl-form-select - v-model="selectedIntegration" + v-model="integrationForm.type" :disabled="isSelectDisabled" class="gl-max-w-full" :options="integrationTypesOptions" + @change="triggerValidation" /> <alert-settings-form-help-block @@ -369,7 +427,6 @@ export default { <div class="gl-mt-3"> <gl-form-group v-if="isHttp" - id="name-integration" :label=" getLabelWithStepNumber( $options.integrationSteps.nameIntegration, @@ -377,67 +434,81 @@ export default { ) " label-for="name-integration" + :invalid-feedback="$options.i18n.integrationFormSteps.nameIntegration.error" + :state="validationState.name" > <gl-form-input + id="name-integration" v-model="integrationForm.name" type="text" :placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder" + @input="validateName" /> </gl-form-group> - <gl-toggle - v-model="active" - :is-loading="loading" - :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle" - class="gl-my-4 gl-font-weight-normal" - /> - - <div v-if="isPrometheus" class="gl-my-4"> - <span class="gl-font-weight-bold"> - {{ - getLabelWithStepNumber( - $options.integrationSteps.setPrometheusApiUrl, - $options.i18n.integrationFormSteps.prometheusFormUrl.label, - ) - }} - </span> + <gl-form-group + v-if="!isNone" + :label=" + getLabelWithStepNumber( + isHttp + ? $options.integrationSteps.enableHttpIntegration + : $options.integrationSteps.enablePrometheusIntegration, + $options.i18n.integrationFormSteps.enableIntegration.label, + ) + " + > + <span>{{ $options.i18n.integrationFormSteps.enableIntegration.help }}</span> + + <gl-toggle + id="enable-integration" + v-model="integrationForm.active" + :is-loading="loading" + :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle" + class="gl-mt-4 gl-font-weight-normal" + /> + </gl-form-group> + <gl-form-group + v-if="isPrometheus" + class="gl-my-4" + :label="$options.i18n.integrationFormSteps.prometheusFormUrl.label" + label-for="api-url" + :invalid-feedback="$options.i18n.integrationFormSteps.prometheusFormUrl.error" + :state="validationState.apiUrl" + > <gl-form-input - id="integration-apiUrl" + id="api-url" v-model="integrationForm.apiUrl" type="text" :placeholder="$options.placeholders.prometheus" + @input="validateApiUrl" /> - <span class="gl-text-gray-400"> {{ $options.i18n.integrationFormSteps.prometheusFormUrl.help }} </span> - </div> + </gl-form-group> <template v-if="showMappingBuilder"> <gl-form-group data-testid="sample-payload-section" :label=" getLabelWithStepNumber( - $options.integrationSteps.setSamplePayload, - $options.i18n.integrationFormSteps.setSamplePayload.label, + $options.integrationSteps.customizeMapping, + $options.i18n.integrationFormSteps.mapFields.label, ) " label-for="sample-payload" class="gl-mb-0!" :invalid-feedback="samplePayload.error" > - <alert-settings-form-help-block - :message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelpHttp" - :link="generic.alertsUsageUrl" - /> + <span>{{ $options.i18n.integrationFormSteps.mapFields.help }}</span> <gl-form-textarea id="sample-payload" v-model.trim="samplePayload.json" - :disabled="isPayloadEditDisabled" + :disabled="canEditPayload" :state="isSampePayloadValid" - :placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder" + :placeholder="$options.i18n.integrationFormSteps.mapFields.placeholder" class="gl-my-3" :debounce="$options.JSON_VALIDATE_DELAY" rows="6" @@ -450,71 +521,76 @@ export default { v-if="canEditPayload" v-gl-modal.resetPayloadModal data-testid="payload-action-btn" - :disabled="!active" + :disabled="!integrationForm.active" class="gl-mt-3" > - {{ $options.i18n.integrationFormSteps.setSamplePayload.editPayload }} + {{ $options.i18n.integrationFormSteps.mapFields.editPayload }} </gl-button> <gl-button v-else data-testid="payload-action-btn" :class="{ 'gl-mt-3': samplePayload.error }" - :disabled="canParseSamplePayload" - :loading="parsingPayload" - @click="parseMapping" + :disabled="!canParseSamplePayload" + :loading="samplePayload.loading" + @click="parseSamplePayload" > - {{ $options.i18n.integrationFormSteps.setSamplePayload.parsePayload }} + {{ $options.i18n.integrationFormSteps.mapFields.parsePayload }} </gl-button> <gl-modal modal-id="resetPayloadModal" - :title="$options.i18n.integrationFormSteps.setSamplePayload.resetHeader" - :ok-title="$options.i18n.integrationFormSteps.setSamplePayload.resetOk" + :title="$options.i18n.integrationFormSteps.mapFields.resetHeader" + :ok-title="$options.i18n.integrationFormSteps.mapFields.resetOk" ok-variant="danger" - @ok="resetPayloadAndMapping" + @ok="resetPayloadAndMappingConfirmed = true" > - {{ $options.i18n.integrationFormSteps.setSamplePayload.resetBody }} + {{ $options.i18n.integrationFormSteps.mapFields.resetBody }} </gl-modal> - <gl-form-group - id="mapping-builder" - class="gl-mt-5" - :label=" - getLabelWithStepNumber( - $options.integrationSteps.customizeMapping, - $options.i18n.integrationFormSteps.mapFields.label, - ) - " - label-for="mapping-builder" - > - <span>{{ $options.i18n.integrationFormSteps.mapFields.intro }}</span> + <div class="gl-mt-5"> + <span>{{ $options.i18n.integrationFormSteps.mapFields.mapIntro }}</span> <mapping-builder :parsed-payload="parsedPayload" :saved-mapping="mapping" :alert-fields="alertFields" @onMappingUpdate="updateMapping" /> - </gl-form-group> + </div> </template> </div> - <div class="gl-display-flex gl-justify-content-start gl-py-3"> <gl-button - type="submit" + :disabled="!canSubmitForm" variant="confirm" class="js-no-auto-disable" data-testid="integration-form-submit" + @click="submit(false)" > {{ $options.i18n.saveIntegration }} </gl-button> + <gl-button + :disabled="!canSubmitForm" + variant="confirm" + category="secondary" + class="gl-ml-3 js-no-auto-disable" + data-testid="integration-form-test-and-submit" + @click="submit(true)" + > + {{ $options.i18n.saveAndTestIntegration }} + </gl-button> + <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{ $options.i18n.cancelAndClose }}</gl-button> </div> </gl-tab> - <gl-tab :title="$options.i18n.integrationTabs.viewCredentials" :disabled="isCreating"> + <gl-tab + :title="$options.i18n.integrationTabs.viewCredentials" + :disabled="isCreating" + class="gl-mt-3" + > <alert-settings-form-help-block :message="viewCredentialsHelpMsg" link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html" @@ -559,13 +635,15 @@ export default { </div> </gl-form-group> - <gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled" variant="danger"> - {{ $options.i18n.integrationFormSteps.setupCredentials.reset }} - </gl-button> + <div class="gl-display-flex gl-justify-content-start gl-py-3"> + <gl-button v-gl-modal.authKeyModal variant="danger"> + {{ $options.i18n.integrationFormSteps.setupCredentials.reset }} + </gl-button> - <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{ - $options.i18n.cancelAndClose - }}</gl-button> + <gl-button type="reset" class="gl-ml-3 js-no-auto-disable"> + {{ $options.i18n.cancelAndClose }} + </gl-button> + </div> <gl-modal modal-id="authKeyModal" @@ -578,18 +656,22 @@ export default { </gl-modal> </gl-tab> - <gl-tab :title="$options.i18n.integrationTabs.sendTestAlert" :disabled="isCreating"> + <gl-tab + :title="$options.i18n.integrationTabs.sendTestAlert" + :disabled="isCreating" + class="gl-mt-3" + > <gl-form-group id="test-integration" :invalid-feedback="testPayload.error"> <alert-settings-form-help-block - :message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelp" - :link="generic.alertsUsageUrl" + :message="$options.i18n.integrationFormSteps.testPayload.help" + :link="alertsUsageUrl" /> <gl-form-textarea id="test-payload" v-model.trim="testPayload.json" :state="isTestPayloadValid" - :placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder" + :placeholder="$options.i18n.integrationFormSteps.testPayload.placeholder" class="gl-my-3" :debounce="$options.JSON_VALIDATE_DELAY" rows="6" @@ -597,20 +679,35 @@ export default { @input="validateJson(false)" /> </gl-form-group> + <div class="gl-display-flex gl-justify-content-start gl-py-3"> + <gl-button + v-gl-modal="testAlertModal" + :disabled="!isTestPayloadValid" + :loading="loading" + data-testid="send-test-alert" + variant="confirm" + class="js-no-auto-disable" + @click="isFormDirty ? null : sendTestAlert()" + > + {{ $options.i18n.send }} + </gl-button> - <gl-button - :disabled="!isTestPayloadValid" - data-testid="send-test-alert" - variant="confirm" - class="js-no-auto-disable" - @click="sendTestAlert" - > - {{ $options.i18n.send }} - </gl-button> + <gl-button type="reset" class="gl-ml-3 js-no-auto-disable"> + {{ $options.i18n.cancelAndClose }} + </gl-button> + </div> - <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{ - $options.i18n.cancelAndClose - }}</gl-button> + <gl-modal + :modal-id="$options.testAlertModalId" + :title="$options.i18n.integrationFormSteps.testPayload.modalTitle" + :action-primary="$options.primaryProps" + :action-secondary="$options.secondaryProps" + :action-cancel="$options.cancelProps" + @primary="saveAndSendTestAlert" + @secondary="sendTestAlert" + > + {{ $options.i18n.integrationFormSteps.testPayload.modalBody }} + </gl-modal> </gl-tab> </gl-tabs> </gl-form> 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 3ffb652e61b..f51c8d7e9f7 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -1,11 +1,11 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlAlert } from '@gitlab/ui'; import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; import createFlash, { FLASH_TYPES } from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; -import { s__ } from '~/locale'; -import { typeSet } from '../constants'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { typeSet, i18n, tabIndices } from '../constants'; import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql'; import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql'; import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql'; @@ -28,21 +28,12 @@ import { RESET_INTEGRATION_TOKEN_ERROR, UPDATE_INTEGRATION_ERROR, INTEGRATION_PAYLOAD_TEST_ERROR, + INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR, + DEFAULT_ERROR, } from '../utils/error_messages'; import IntegrationsList from './alerts_integrations_list.vue'; import AlertSettingsForm from './alerts_settings_form.vue'; -export const i18n = { - changesSaved: s__( - 'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.', - ), - integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'), - alertSent: s__( - 'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.', - ), - addNewIntegration: s__('AlertSettings|Add new integration'), -}; - export default { typeSet, i18n, @@ -50,14 +41,9 @@ export default { IntegrationsList, AlertSettingsForm, GlButton, + GlAlert, }, inject: { - generic: { - default: {}, - }, - prometheus: { - default: {}, - }, projectPath: { default: '', }, @@ -124,7 +110,10 @@ export default { integrations: {}, httpIntegrations: {}, currentIntegration: null, + newIntegration: null, formVisible: false, + showSuccessfulCreateAlert: false, + tabIndex: tabIndices.configureDetails, }; }, computed: { @@ -139,10 +128,10 @@ export default { isHttp(type) { return type === typeSet.http; }, - createNewIntegration({ type, variables }) { + createNewIntegration({ type, variables }, testAfterSubmit) { const { projectPath } = this; - const isHttp = this.isHttp(type); + this.isUpdating = true; this.$apollo .mutate({ @@ -163,16 +152,19 @@ export default { .then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => { const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0]; if (error) { - return createFlash({ message: error }); + createFlash({ message: error }); + return; } - const { integration } = httpIntegrationCreate || prometheusIntegrationCreate; - this.editIntegration(integration); + const { integration } = httpIntegrationCreate || prometheusIntegrationCreate; + this.newIntegration = integration; + this.showSuccessfulCreateAlert = true; - return createFlash({ - message: this.$options.i18n.changesSaved, - type: FLASH_TYPES.SUCCESS, - }); + if (testAfterSubmit) { + this.viewIntegration(this.newIntegration, tabIndices.sendTestAlert); + } else { + this.setFormVisibility(false); + } }) .catch(() => { createFlash({ message: ADD_INTEGRATION_ERROR }); @@ -181,9 +173,9 @@ export default { this.isUpdating = false; }); }, - updateIntegration({ type, variables }) { + updateIntegration({ type, variables }, testAfterSubmit) { this.isUpdating = true; - this.$apollo + return this.$apollo .mutate({ mutation: this.isHttp(type) ? updateHttpIntegrationMutation @@ -196,12 +188,20 @@ export default { .then(({ data: { httpIntegrationUpdate, prometheusIntegrationUpdate } = {} } = {}) => { const error = httpIntegrationUpdate?.errors[0] || prometheusIntegrationUpdate?.errors[0]; if (error) { - return createFlash({ message: error }); + createFlash({ message: error }); + return; } - this.clearCurrentIntegration({ type }); + const integration = + httpIntegrationUpdate?.integration || prometheusIntegrationUpdate?.integration; - return createFlash({ + if (testAfterSubmit) { + this.viewIntegration(integration, tabIndices.sendTestAlert); + } else { + this.clearCurrentIntegration(type); + } + + createFlash({ message: this.$options.i18n.changesSaved, type: FLASH_TYPES.SUCCESS, }); @@ -261,13 +261,23 @@ export default { currentIntegration = { ...currentIntegration, ...httpIntegrationMappingData }; } - this.$apollo.mutate({ - mutation: this.isHttp(type) - ? updateCurrentHttpIntegrationMutation - : updateCurrentPrometheusIntegrationMutation, - variables: currentIntegration, - }); - this.setFormVisibility(true); + this.viewIntegration(currentIntegration, tabIndices.viewCredentials); + }, + viewIntegration(integration, tabIndex) { + this.$apollo + .mutate({ + mutation: this.isHttp(integration.type) + ? updateCurrentHttpIntegrationMutation + : updateCurrentPrometheusIntegrationMutation, + variables: integration, + }) + .then(() => { + this.setFormVisibility(true); + this.tabIndex = tabIndex; + }) + .catch(() => { + createFlash({ message: DEFAULT_ERROR }); + }); }, deleteIntegration({ id, type }) { const { projectPath } = this; @@ -319,19 +329,44 @@ export default { type: FLASH_TYPES.SUCCESS, }); }) - .catch(() => { - createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); + .catch((error) => { + let message = INTEGRATION_PAYLOAD_TEST_ERROR; + if (error.response?.status === httpStatusCodes.FORBIDDEN) { + message = INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR; + } + createFlash({ message }); }); }, + saveAndTestAlertPayload(integration, payload) { + return this.updateIntegration(integration, false).then(() => { + this.testAlertPayload(payload); + }); + }, setFormVisibility(visible) { this.formVisible = visible; }, + viewCreatedIntegration() { + this.viewIntegration(this.newIntegration, tabIndices.viewCredentials); + this.showSuccessfulCreateAlert = false; + this.newIntegration = null; + }, }, }; </script> <template> <div> + <gl-alert + v-if="showSuccessfulCreateAlert" + class="gl-mt-n2" + :primary-button-text="$options.i18n.integrationCreated.btnCaption" + :title="$options.i18n.integrationCreated.title" + @primaryAction="viewCreatedIntegration" + @dismiss="showSuccessfulCreateAlert = false" + > + {{ $options.i18n.integrationCreated.successMsg }} + </gl-alert> + <integrations-list :integrations="integrations.list" :loading="loading" @@ -353,11 +388,13 @@ export default { :loading="isUpdating" :can-add-integration="canAddIntegration" :alert-fields="alertFields" + :tab-index="tabIndex" @create-new-integration="createNewIntegration" @update-integration="updateIntegration" @reset-token="resetToken" @clear-current-integration="clearCurrentIntegration" @test-alert-payload="testAlertPayload" + @save-and-test-alert-payload="saveAndTestAlertPayload" /> </div> </template> diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index ce6cf61b5dd..d64ac55ff98 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -17,6 +17,13 @@ export const i18n = { label: s__('AlertSettings|Name integration'), placeholder: s__('AlertSettings|Enter integration name'), activeToggle: __('Active'), + error: __("Name can't be blank"), + }, + enableIntegration: { + label: s__('AlertSettings|Enable integration'), + help: s__( + 'AlertSettings|A webhook URL and authorization key will be generated for the integration. Both will be visible after saving the integration in the “View credentials” tab.', + ), }, setupCredentials: { help: s__( @@ -29,35 +36,41 @@ export const i18n = { authorizationKey: s__('AlertSettings|Authorization key'), reset: s__('AlertSettings|Reset Key'), }, - setSamplePayload: { - label: s__('AlertSettings|Sample alert payload (optional)'), - testPayloadHelpHttp: s__( - 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional).', - ), - testPayloadHelp: s__( - 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.', + mapFields: { + label: s__('AlertSettings|Customize alert payload mapping (optional)'), + help: s__( + 'AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click the "parse payload fields" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration.', ), placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'), + editPayload: s__('AlertSettings|Edit payload'), + parsePayload: s__('AlertSettings|Parse payload fields'), + payloadParsedSucessMsg: s__( + 'AlertSettings|Sample payload has been parsed. You can now map the fields.', + ), resetHeader: s__('AlertSettings|Reset the mapping'), resetBody: s__( "AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields.", ), resetOk: s__('AlertSettings|Proceed with editing'), - editPayload: s__('AlertSettings|Edit payload'), - parsePayload: s__('AlertSettings|Parse payload for custom mapping'), - payloadParsedSucessMsg: s__( - 'AlertSettings|Sample payload has been parsed. You can now map the fields.', + mapIntro: s__( + "AlertSettings|The default GitLab alert fields are listed below. If you choose to map your payload keys to GitLab's, please make a selection in the dropdowns below. You may also opt to leave the fields unmapped and move straight to saving your integration.", ), }, - mapFields: { - label: s__('AlertSettings|Customize alert payload mapping (optional)'), - intro: s__( - 'AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click "parse payload fields" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration.', + testPayload: { + help: s__( + 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.', ), + placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'), + modalTitle: s__('AlertSettings|The form has unsaved changes'), + modalBody: s__('AlertSettings|The form has unsaved changes. How would you like to proceed?'), + savedAndTest: s__('AlertSettings|Save integration & send'), + proceedWithoutSave: s__('AlertSettings|Send without saving'), + cancel: __('Cancel'), }, prometheusFormUrl: { label: s__('AlertSettings|Prometheus API base URL'), help: s__('AlertSettings|URL cannot be blank and must start with http or https'), + error: s__('AlertSettings|URL is invalid.'), }, restKeyInfo: { label: s__( @@ -66,40 +79,52 @@ export const i18n = { }, }, saveIntegration: s__('AlertSettings|Save integration'), - changesSaved: s__('AlertSettings|Your integration was successfully updated.'), + saveAndTestIntegration: s__('AlertSettings|Save & create test alert'), cancelAndClose: __('Cancel and close'), - send: s__('AlertSettings|Send'), + send: __('Send'), copy: __('Copy'), + integrationCreated: { + title: s__('AlertSettings|Integration successfully saved'), + successMsg: s__( + 'AlertSettings|A URL and authorization key have been created for your integration. You will need them to setup a webhook and authorize your endpoint to send alerts to GitLab.', + ), + btnCaption: s__('AlertSettings|View URL and authorization key'), + }, + changesSaved: s__('AlertsIntegrations|The integration has been successfully saved.'), + integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'), + alertSent: s__( + 'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.', + ), + addNewIntegration: s__('AlertSettings|Add new integration'), }; export const integrationSteps = { selectType: 'SELECT_TYPE', nameIntegration: 'NAME_INTEGRATION', - setPrometheusApiUrl: 'SET_PROMETHEUS_API_URL', - setSamplePayload: 'SET_SAMPLE_PAYLOAD', + enableHttpIntegration: 'ENABLE_HTTP_INTEGRATION', + enablePrometheusIntegration: 'ENABLE_PROMETHEUS_INTEGRATION', customizeMapping: 'CUSTOMIZE_MAPPING', }; export const createStepNumbers = { [integrationSteps.selectType]: 1, [integrationSteps.nameIntegration]: 2, - [integrationSteps.setPrometheusApiUrl]: 2, - [integrationSteps.setSamplePayload]: 3, + [integrationSteps.enableHttpIntegration]: 3, + [integrationSteps.enablePrometheusIntegration]: 2, [integrationSteps.customizeMapping]: 4, }; export const editStepNumbers = { - [integrationSteps.selectType]: 1, [integrationSteps.nameIntegration]: 1, - [integrationSteps.setPrometheusApiUrl]: null, - [integrationSteps.setSamplePayload]: 2, + [integrationSteps.enableHttpIntegration]: 2, + [integrationSteps.enablePrometheusIntegration]: null, [integrationSteps.customizeMapping]: 3, }; 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') }, + prometheus: { value: 'PROMETHEUS', text: s__('AlertSettings|Prometheus') }, }; export const typeSet = { @@ -127,4 +152,10 @@ export const mappingFields = { fallback: 'fallback', }; -export const viewCredentialsTabIndex = 1; +export const tabIndices = { + configureDetails: 0, + viewCredentials: 1, + sendTestAlert: 2, +}; + +export const testAlertModalId = 'confirmSendTestAlert'; diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index e830f53a55e..953a867b2b7 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -19,23 +19,7 @@ export default (el) => { return null; } - const { - prometheusActivated, - prometheusUrl, - prometheusAuthorizationKey, - prometheusFormPath, - prometheusResetKeyPath, - prometheusApiUrl, - activated: activatedStr, - alertsSetupUrl, - alertsUsageUrl, - formPath, - authorizationKey, - url, - projectPath, - multiIntegrations, - alertFields, - } = el.dataset; + const { alertsUsageUrl, projectPath, multiIntegrations, alertFields } = el.dataset; return new Vue({ el, @@ -43,22 +27,7 @@ export default (el) => { AlertSettingsWrapper, }, provide: { - prometheus: { - active: parseBoolean(prometheusActivated), - url: prometheusUrl, - token: prometheusAuthorizationKey, - prometheusFormPath, - prometheusResetKeyPath, - prometheusApiUrl, - }, - generic: { - alertsSetupUrl, - alertsUsageUrl, - active: parseBoolean(activatedStr), - formPath, - token: authorizationKey, - url, - }, + alertsUsageUrl, projectPath, multiIntegrations: parseBoolean(multiIntegrations), }, diff --git a/app/assets/javascripts/alerts_settings/utils/error_messages.js b/app/assets/javascripts/alerts_settings/utils/error_messages.js index e380257f983..9a0644b4e22 100644 --- a/app/assets/javascripts/alerts_settings/utils/error_messages.js +++ b/app/assets/javascripts/alerts_settings/utils/error_messages.js @@ -1,4 +1,4 @@ -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; export const DELETE_INTEGRATION_ERROR = s__( 'AlertsIntegrations|The integration could not be deleted. Please try again.', @@ -19,3 +19,9 @@ export const RESET_INTEGRATION_TOKEN_ERROR = s__( export const INTEGRATION_PAYLOAD_TEST_ERROR = s__( 'AlertsIntegrations|Integration payload is invalid.', ); + +export const INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR = s__( + 'AlertsIntegrations|The integration is currently inactive. Enable the integration to send the test alert.', +); + +export const DEFAULT_ERROR = __('Something went wrong on our end.'); |