Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-26 00:09:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-26 00:09:13 +0300
commit659ad26b4e0e1f792ab6d15ac8d6e47ec943e214 (patch)
tree5e395d172b681cfe396c671ebf7eef8789ef7334 /app/assets/javascripts/alerts_settings
parente93121b42cb4ca672c893c6be0a8af0c8d9f7987 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/alerts_settings')
-rw-r--r--app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue18
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue411
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue123
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js83
-rw-r--r--app/assets/javascripts/alerts_settings/index.js35
-rw-r--r--app/assets/javascripts/alerts_settings/utils/error_messages.js8
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">&nbsp;</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">&nbsp;</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.');