Commit 9c5a27f0 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'tr-incident-alert-details' into 'master'

Surface alert details in a tab on incidents

See merge request gitlab-org/gitlab!41850
parents 392e4aff 1273b6b5
...@@ -468,7 +468,6 @@ export default { ...@@ -468,7 +468,6 @@ export default {
<component <component
:is="descriptionComponent" :is="descriptionComponent"
v-if="state.descriptionHtml"
:can-update="canUpdate" :can-update="canUpdate"
:description-html="state.descriptionHtml" :description-html="state.descriptionHtml"
:description-text="state.descriptionText" :description-text="state.descriptionText"
......
query getHighlightBarInfo($iid: String!, $fullPath: ID!) { query getAlert($iid: String!, $fullPath: ID!) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
issue(iid: $iid) { issue(iid: $iid) {
alertManagementAlert { alertManagementAlert {
iid
title title
detailsUrl detailsUrl
createdAt severity
status
startedAt
eventCount eventCount
monitoringTool
service
description
endedAt
details
} }
} }
} }
......
<script> <script>
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import getHighlightBarInfo from './graphql/queries/get_highlight_bar_info.graphql';
export default { export default {
components: { components: {
GlLink, GlLink,
}, },
inject: ['fullPath', 'iid'], props: {
apollo: {
alert: { alert: {
query: getHighlightBarInfo, type: Object,
variables() { required: true,
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update: data => data.project?.issue?.alertManagementAlert,
}, },
}, },
computed: { computed: {
startTime() { startTime() {
return formatDate(this.alert.createdAt, 'yyyy-mm-dd Z'); return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z');
}, },
}, },
}; };
...@@ -30,7 +22,6 @@ export default { ...@@ -30,7 +22,6 @@ export default {
<template> <template>
<div <div
v-if="alert"
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between" class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
> >
<div class="text-truncate gl-pr-3"> <div class="text-truncate gl-pr-3">
......
<script> <script>
import { GlTab, GlTabs } from '@gitlab/ui'; import { GlTab, GlTabs } from '@gitlab/ui';
import DescriptionComponent from '../description.vue'; import DescriptionComponent from '../description.vue';
import HighlightBar from './highlight_bar/higlight_bar.vue'; import HighlightBar from './highlight_bar.vue';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import getAlert from './graphql/queries/get_alert.graphql';
export default { export default {
components: { components: {
AlertDetailsTable,
DescriptionComponent,
GlTab, GlTab,
GlTabs, GlTabs,
DescriptionComponent,
HighlightBar, HighlightBar,
}, },
inject: ['fullPath', 'iid'],
apollo: {
alert: {
query: getAlert,
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return data?.project?.issue?.alertManagementAlert;
},
error() {
createFlash({
message: s__('Incident|There was an issue loading alert data. Please try again.'),
});
},
},
},
data() {
return {
alert: null,
};
},
computed: {
loading() {
return this.$apollo.queries.alert.loading;
},
alertTableFields() {
if (this.alert) {
const { detailsUrl, __typename, ...restDetails } = this.alert;
return restDetails;
}
return null;
},
},
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs"> <gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs">
<gl-tab :title="__('Summary')"> <gl-tab :title="s__('Incident|Summary')">
<highlight-bar /> <highlight-bar v-if="alert" :alert="alert" />
<description-component v-bind="$attrs" /> <description-component v-bind="$attrs" />
</gl-tab> </gl-tab>
<gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')">
<alert-details-table :alert="alertTableFields" :loading="loading" />
</gl-tab>
</gl-tabs> </gl-tabs>
</div> </div>
</template> </template>
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
}, },
}, },
tableHeader: { tableHeader: {
[s__('AlertManagement|Full Alert Payload')]: s__('AlertManagement|Value'), [s__('AlertManagement|Key')]: s__('AlertManagement|Value'),
}, },
computed: { computed: {
items() { items() {
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
</script> </script>
<template> <template>
<gl-table <gl-table
class="alert-management-details-table" class="alert-management-details-table gl-mb-0!"
:busy="loading" :busy="loading"
:empty-text="s__('AlertManagement|No alert data to display.')" :empty-text="s__('AlertManagement|No alert data to display.')"
:items="items" :items="items"
......
...@@ -6,7 +6,10 @@ ...@@ -6,7 +6,10 @@
@include gl-border-0; @include gl-border-0;
@include gl-p-5; @include gl-p-5;
border-color: transparent; border-color: transparent;
border-bottom: 1px solid $table-border-color;
&:not(:last-child) {
border-bottom: 1px solid $table-border-color;
}
&:first-child { &:first-child {
div { div {
...@@ -31,6 +34,12 @@ ...@@ -31,6 +34,12 @@
} }
} }
} }
&:last-child {
&::after {
content: none !important;
}
}
} }
} }
......
---
title: Surface alert details in a tab on incidents
merge_request: 41850
author:
type: added
...@@ -2214,9 +2214,6 @@ msgstr "" ...@@ -2214,9 +2214,6 @@ msgstr ""
msgid "AlertManagement|Events" msgid "AlertManagement|Events"
msgstr "" msgstr ""
msgid "AlertManagement|Full Alert Payload"
msgstr ""
msgid "AlertManagement|High" msgid "AlertManagement|High"
msgstr "" msgstr ""
...@@ -2226,6 +2223,9 @@ msgstr "" ...@@ -2226,6 +2223,9 @@ msgstr ""
msgid "AlertManagement|Issue" msgid "AlertManagement|Issue"
msgstr "" msgstr ""
msgid "AlertManagement|Key"
msgstr ""
msgid "AlertManagement|Low" msgid "AlertManagement|Low"
msgstr "" msgstr ""
...@@ -13415,6 +13415,15 @@ msgstr "" ...@@ -13415,6 +13415,15 @@ msgstr ""
msgid "Incidents" msgid "Incidents"
msgstr "" msgstr ""
msgid "Incident|Alert details"
msgstr ""
msgid "Incident|Summary"
msgstr ""
msgid "Incident|There was an issue loading alert data. Please try again."
msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept." msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr "" msgstr ""
......
...@@ -36,7 +36,7 @@ describe('Issuable output', () => { ...@@ -36,7 +36,7 @@ describe('Issuable output', () => {
const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]'); const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
const mountComponent = (props = {}) => { const mountComponent = (props = {}, options = {}) => {
wrapper = mount(IssuableApp, { wrapper = mount(IssuableApp, {
propsData: { ...appProps, ...props }, propsData: { ...appProps, ...props },
provide: { provide: {
...@@ -45,7 +45,9 @@ describe('Issuable output', () => { ...@@ -45,7 +45,9 @@ describe('Issuable output', () => {
}, },
stubs: { stubs: {
HighlightBar: true, HighlightBar: true,
IncidentTabs: true,
}, },
...options,
}); });
}; };
...@@ -582,10 +584,23 @@ describe('Issuable output', () => { ...@@ -582,10 +584,23 @@ describe('Issuable output', () => {
describe('when using incident tabs description wrapper', () => { describe('when using incident tabs description wrapper', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent(
descriptionComponent: IncidentTabs, {
showTitleBorder: false, descriptionComponent: IncidentTabs,
}); showTitleBorder: false,
},
{
mocks: {
$apollo: {
queries: {
alert: {
loading: false,
},
},
},
},
},
);
}); });
it('renders the description component', () => { it('renders the description component', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar/higlight_bar.vue'; import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
jest.mock('~/lib/utils/datetime_utility'); jest.mock('~/lib/utils/datetime_utility');
...@@ -9,7 +9,7 @@ describe('Highlight Bar', () => { ...@@ -9,7 +9,7 @@ describe('Highlight Bar', () => {
let wrapper; let wrapper;
const alert = { const alert = {
createdAt: '2020-05-29T10:39:22Z', startedAt: '2020-05-29T10:39:22Z',
detailsUrl: 'http://127.0.0.1:3000/root/unique-alerts/-/alert_management/1/details', detailsUrl: 'http://127.0.0.1:3000/root/unique-alerts/-/alert_management/1/details',
eventCount: 1, eventCount: 1,
title: 'Alert 1', title: 'Alert 1',
...@@ -17,12 +17,8 @@ describe('Highlight Bar', () => { ...@@ -17,12 +17,8 @@ describe('Highlight Bar', () => {
const mountComponent = () => { const mountComponent = () => {
wrapper = shallowMount(HighlightBar, { wrapper = shallowMount(HighlightBar, {
provide: { propsData: {
fullPath: 'project/id', alert,
iid: '1',
},
data() {
return { alert };
}, },
}); });
}; };
...@@ -50,7 +46,7 @@ describe('Highlight Bar', () => { ...@@ -50,7 +46,7 @@ describe('Highlight Bar', () => {
const formattedDate = '2020-05-29 UTC'; const formattedDate = '2020-05-29 UTC';
formatDate.mockReturnValueOnce(formattedDate); formatDate.mockReturnValueOnce(formattedDate);
mountComponent(); mountComponent();
expect(formatDate).toHaveBeenCalledWith(alert.createdAt, 'yyyy-mm-dd Z'); expect(formatDate).toHaveBeenCalledWith(alert.startedAt, 'yyyy-mm-dd Z');
expect(wrapper.text()).toContain(formattedDate); expect(wrapper.text()).toContain(formattedDate);
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui'; import { GlTab } from '@gitlab/ui';
import INVALID_URL from '~/lib/utils/invalid_url';
import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue'; import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
import { descriptionProps } from '../mock_data'; import { descriptionProps } from '../../mock_data';
import DescriptionComponent from '~/issue_show/components/description.vue'; import DescriptionComponent from '~/issue_show/components/description.vue';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar/higlight_bar.vue'; import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const mockAlert = {
__typename: 'AlertManagementAlert',
detailsUrl: INVALID_URL,
iid: '1',
};
describe('Incident Tabs component', () => { describe('Incident Tabs component', () => {
let wrapper; let wrapper;
const mountComponent = () => { const mountComponent = (data = {}) => {
wrapper = shallowMount(IncidentTabs, { wrapper = shallowMount(IncidentTabs, {
propsData: { propsData: {
...descriptionProps, ...descriptionProps,
...@@ -16,30 +24,76 @@ describe('Incident Tabs component', () => { ...@@ -16,30 +24,76 @@ describe('Incident Tabs component', () => {
stubs: { stubs: {
DescriptionComponent: true, DescriptionComponent: true,
}, },
provide: {
fullPath: '',
iid: '',
},
data() {
return { alert: mockAlert, ...data };
},
mocks: {
$apollo: {
queries: {
alert: {
loading: true,
},
},
},
},
}); });
}; };
beforeEach(() => {
mountComponent();
});
const findTabs = () => wrapper.findAll(GlTab); const findTabs = () => wrapper.findAll(GlTab);
const findSummaryTab = () => findTabs().at(0); const findSummaryTab = () => findTabs().at(0);
const findAlertDetailsTab = () => findTabs().at(1);
const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.find(DescriptionComponent); const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.find(HighlightBar); const findHighlightBarComponent = () => wrapper.find(HighlightBar);
describe('default state', () => { describe('empty state', () => {
it('renders the summary tab', async () => { beforeEach(() => {
expect(findTabs()).toHaveLength(1); mountComponent({ alert: null });
});
it('does not show the alert details tab', () => {
expect(findAlertDetailsComponent().exists()).toBe(false);
expect(findHighlightBarComponent().exists()).toBe(false);
});
});
describe('with an alert present', () => {
beforeEach(() => {
mountComponent();
});
it('renders the summary tab', () => {
expect(findSummaryTab().exists()).toBe(true); expect(findSummaryTab().exists()).toBe(true);
expect(findSummaryTab().attributes('title')).toBe('Summary'); expect(findSummaryTab().attributes('title')).toBe('Summary');
}); });
it('renders the alert details tab', () => {
expect(findAlertDetailsTab().exists()).toBe(true);
expect(findAlertDetailsTab().attributes('title')).toBe('Alert details');
});
it('renders the alert details table with the correct props', () => {
const alert = { iid: mockAlert.iid };
expect(findAlertDetailsComponent().props('alert')).toEqual(alert);
expect(findAlertDetailsComponent().props('loading')).toBe(true);
});
it('renders the description component with highlight bar', () => { it('renders the description component with highlight bar', () => {
expect(findDescriptionComponent().exists()).toBe(true); expect(findDescriptionComponent().exists()).toBe(true);
expect(findHighlightBarComponent().exists()).toBe(true); expect(findHighlightBarComponent().exists()).toBe(true);
}); });
it('renders the highlight bar component with the correct props', () => {
const alert = { detailsUrl: mockAlert.detailsUrl };
expect(findHighlightBarComponent().props('alert')).toMatchObject(alert);
});
it('passes all props to the description component', () => { it('passes all props to the description component', () => {
expect(findDescriptionComponent().props()).toMatchObject(descriptionProps); expect(findDescriptionComponent().props()).toMatchObject(descriptionProps);
}); });
......
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