Commit d20e1d21 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '262707-custom-alert-mapping' into 'master'

Create custom mapping UI based on mocks

See merge request gitlab-org/gitlab!45797
parents 3f2a31ab b6237e3e
<script>
import Vue from 'vue';
import {
GlIcon,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
// Mocks will be removed when integrating with BE is ready
// data format most likely will differ but UI will not
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
import gitlabFields from './mocks/gitlabFields.json';
import parsedMapping from './mocks/parsedMapping.json';
export const i18n = {
columns: {
gitlabKeyTitle: s__('AlertMappingBuilder|GitLab alert key'),
payloadKeyTitle: s__('AlertMappingBuilder|Payload alert key'),
fallbackKeyTitle: s__('AlertMappingBuilder|Define fallback'),
},
selectMappingKey: s__('AlertMappingBuilder|Select key'),
makeSelection: s__('AlertMappingBuilder|Make selection'),
fallbackTooltip: s__(
'AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. ',
),
noResults: __('No matching results'),
};
export default {
i18n,
components: {
GlIcon,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
directives: {
GlTooltip,
},
data() {
return {
gitlabFields,
};
},
computed: {
mappingData() {
return this.gitlabFields.map(gitlabField => {
const mappingFields = parsedMapping.filter(field => field.type === gitlabField.type);
return {
mapping: null,
fallback: null,
searchTerm: '',
fallbackSearchTerm: '',
mappingFields,
...gitlabField,
};
});
},
},
methods: {
setMapping(gitlabKey, mappingKey, valueKey) {
const fieldIndex = this.gitlabFields.findIndex(field => field.key === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
},
setSearchTerm(search = '', searchFieldKey, gitlabKey) {
const fieldIndex = this.gitlabFields.findIndex(field => field.key === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [searchFieldKey]: search } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
},
filterFields(searchTerm = '', fields) {
const search = searchTerm.toLowerCase();
return fields.filter(field => field.label.toLowerCase().includes(search));
},
isSelected(fieldValue, mapping) {
return fieldValue === mapping;
},
selectedValue(key) {
return (
parsedMapping.find(item => item.key === key)?.label || this.$options.i18n.makeSelection
);
},
getFieldValue({ label, type }) {
return `${label} (${type})`;
},
noResults(searchTerm, fields) {
return !this.filterFields(searchTerm, fields).length;
},
},
};
</script>
<template>
<div class="gl-display-table gl-w-full gl-mt-5">
<div class="gl-display-table-row">
<h5 class="gl-display-table-cell gl-py-3 gl-pr-3">
{{ $options.i18n.columns.gitlabKeyTitle }}
</h5>
<h5 class="gl-display-table-cell gl-py-3 gl-pr-3">&nbsp;</h5>
<h5 class="gl-display-table-cell gl-py-3 gl-pr-3">
{{ $options.i18n.columns.payloadKeyTitle }}
</h5>
<h5 class="gl-display-table-cell gl-py-3 gl-pr-3">
{{ $options.i18n.columns.fallbackKeyTitle }}
<gl-icon
v-gl-tooltip
name="question"
class="gl-text-gray-500"
:title="$options.i18n.fallbackTooltip"
/>
</h5>
</div>
<div v-for="gitlabField in mappingData" :key="gitlabField.key" class="gl-display-table-row">
<div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p">
<gl-form-input
disabled
:value="getFieldValue(gitlabField)"
class="gl-bg-transparent! gl-text-gray-900!"
/>
</div>
<div class="gl-display-table-cell gl-py-3 gl-pr-3">
<div class="right-arrow">
<i class="right-arrow-head"></i>
</div>
</div>
<div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p">
<gl-dropdown
:text="selectedValue(gitlabField.mapping)"
class="gl-w-full"
:header-text="$options.i18n.selectMappingKey"
>
<gl-search-box-by-type @input="setSearchTerm($event, 'searchTerm', gitlabField.key)" />
<gl-dropdown-item
v-for="mappingField in filterFields(gitlabField.searchTerm, gitlabField.mappingFields)"
:key="`${mappingField.key}__mapping`"
:is-checked="isSelected(gitlabField.mapping, mappingField.key)"
is-check-item
@click="setMapping(gitlabField.key, mappingField.key, 'mapping')"
>
{{ mappingField.label }}
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults(gitlabField.searchTerm, gitlabField.mappingFields)">
>
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<div class="gl-display-table-cell gl-py-3 w-30p">
<gl-dropdown
v-if="gitlabField.hasFallback"
:text="selectedValue(gitlabField.fallback)"
class="gl-w-full"
:header-text="$options.i18n.selectMappingKey"
>
<gl-search-box-by-type
@input="setSearchTerm($event, 'fallbackSearchTerm', gitlabField.key)"
/>
<gl-dropdown-item
v-for="mappingField in filterFields(
gitlabField.fallbackSearchTerm,
gitlabField.mappingFields,
)"
:key="`${mappingField.key}__fallback`"
:is-checked="isSelected(gitlabField.fallback, mappingField.key)"
is-check-item
@click="setMapping(gitlabField.key, mappingField.key, 'fallback')"
>
{{ mappingField.label }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="noResults(gitlabField.fallbackSearchTerm, gitlabField.mappingFields)"
>
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
</div>
</template>
......@@ -12,6 +12,7 @@ import {
GlModalDirective,
GlToggle,
} from '@gitlab/ui';
import MappingBuilder from './alert_mapping_builder.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
......@@ -84,6 +85,7 @@ export default {
GlModal,
GlToggle,
AlertSettingsFormHelpBlock,
MappingBuilder,
},
directives: {
'gl-modal': GlModalDirective,
......@@ -344,7 +346,7 @@ export default {
label-for="mapping-builder"
>
<span class="gl-text-gray-500">{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<!--mapping builder will be added here-->
<mapping-builder />
</gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button type="reset" class="gl-mr-3 js-no-auto-disable">{{ __('Cancel') }}</gl-button>
......
......@@ -477,6 +477,7 @@ export default {
max-rows="10"
/>
</gl-form-group>
<gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
$options.i18n.testAlertInfo
}}</gl-button>
......
[
{
"key":"title",
"label":"Title",
"type":"String",
"hasFallback": true
},
{
"key":"description",
"label":"Description",
"type":"String"
},
{
"key":"startTime",
"label":"Start time",
"type":"DateTime"
},
{
"key":"service",
"label":"Service",
"type":"String"
},
{
"key":"monitoringTool",
"label":"Monitoring tool",
"type":"String"
},
{
"key":"hosts",
"label":"Hosts",
"type":"String or Array"
},
{
"key":"severity",
"label":"Severity",
"type":"String"
},
{
"key":"fingerprint",
"label":"Fingerprint",
"type":"String"
},
{
"key":"environment",
"label":"Environment",
"type":"String"
}
]
[
{
"key":"title",
"label":"Title",
"type":"String"
},
{
"key":"description",
"label":"Description",
"type":"String"
},
{
"key":"startTime",
"label":"Start time",
"type":"DateTime"
},
{
"key":"service",
"label":"Service",
"type":"String"
},
{
"key":"monitoringTool",
"label":"Monitoring tool",
"type":"String"
},
{
"key":"hosts",
"label":"Hosts",
"type":"String or Array"
},
{
"key":"severity",
"label":"Severity",
"type":"String"
},
{
"key":"fingerprint",
"label":"Fingerprint",
"type":"String"
},
{
"key":"environment",
"label":"Environment",
"type":"String"
}
]
@import 'mixins_and_variables_and_functions';
$stroke-size: 1px;
.right-arrow {
@include gl-relative;
@include gl-w-full;
height: $stroke-size;
@include gl-display-inline-block;
background-color: var(--gray-400, $gray-400);
min-width: $gl-spacing-scale-5;
&-head {
@include gl-absolute;
top: -$gl-spacing-scale-2;
left: calc(100% - #{$gl-spacing-scale-3} - #{2 * $stroke-size});
border-color: var(--gray-400, $gray-400);
@include gl-border-solid;
border-width: 0 $stroke-size $stroke-size 0;
@include gl-display-inline-block;
@include gl-p-2;
transform: rotate(-45deg);
}
}
- return unless can?(current_user, :admin_operations, @project)
- expanded = expanded_by_default?
- add_page_specific_style 'page_bundles/alert_management_settings'
%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) }
.settings-header
......
......@@ -200,6 +200,7 @@ module Gitlab
config.assets.precompile << "page_bundles/reports.css"
config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/alert_management_settings.css"
config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "disable_animations.css"
......
......@@ -2488,6 +2488,24 @@ msgstr ""
msgid "AlertManagement|You have enabled the Opsgenie integration. Your alerts will be visible directly in Opsgenie."
msgstr ""
msgid "AlertMappingBuilder|Define fallback"
msgstr ""
msgid "AlertMappingBuilder|GitLab alert key"
msgstr ""
msgid "AlertMappingBuilder|Make selection"
msgstr ""
msgid "AlertMappingBuilder|Payload alert key"
msgstr ""
msgid "AlertMappingBuilder|Select key"
msgstr ""
msgid "AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. "
msgstr ""
msgid "AlertService|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
......
import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
describe('AlertMappingBuilder', () => {
let wrapper;
function mountComponent() {
wrapper = shallowMount(AlertMappingBuilder);
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
beforeEach(() => {
mountComponent();
});
const findColumnInRow = (row, column) =>
wrapper
.findAll('.gl-display-table-row')
.at(row)
.findAll('.gl-display-table-cell ')
.at(column);
const fieldsByTypeCount = parsedMapping.reduce((acc, { type }) => {
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
it('renders column captions', () => {
expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle);
expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle);
expect(findColumnInRow(0, 3).text()).toContain(i18n.columns.fallbackKeyTitle);
const fallbackColumnIcon = findColumnInRow(0, 3).find(GlIcon);
expect(fallbackColumnIcon.exists()).toBe(true);
expect(fallbackColumnIcon.attributes('name')).toBe('question');
expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip);
});
it('renders disabled form input for each mapped field', () => {
gitlabFields.forEach((field, index) => {
const input = findColumnInRow(index + 1, 0).find(GlFormInput);
expect(input.attributes('value')).toBe(`${field.label} (${field.type})`);
expect(input.attributes('disabled')).toBe('');
});
});
it('renders right arrow next to each input', () => {
gitlabFields.forEach((field, index) => {
const arrow = findColumnInRow(index + 1, 1).find('.right-arrow');
expect(arrow.exists()).toBe(true);
});
});
it('renders mapping dropdown for each field', () => {
gitlabFields.forEach(({ type }, index) => {
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
const searchBox = dropdown.find(GlSearchBoxByType);
const dropdownItems = dropdown.findAll(GlDropdownItem);
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
expect(dropdownItems.length).toBe(fieldsByTypeCount[type]);
});
});
it('renders fallback dropdown only for the fields that have fallback', () => {
gitlabFields.forEach(({ type, hasFallback }, index) => {
const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown);
expect(dropdown.exists()).toBe(Boolean(hasFallback));
if (hasFallback) {
const searchBox = dropdown.find(GlSearchBoxByType);
const dropdownItems = dropdown.findAll(GlDropdownItem);
expect(searchBox.exists()).toBe(hasFallback);
expect(dropdownItems.length).toBe(fieldsByTypeCount[type]);
}
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment