Commit 21dd0ed7 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '232492-allow-for-easier-roll-back-from-alerts-page' into 'master'

Add support for Alerts to display the environment link

See merge request gitlab-org/gitlab!43019
parents 7d1e1637 f1ec6510
<script> <script>
/* eslint-disable vue/no-v-html */
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { import {
GlAlert, GlAlert,
GlBadge, GlBadge,
GlIcon, GlIcon,
GlLink,
GlLoadingIcon, GlLoadingIcon,
GlSprintf, GlSprintf,
GlTabs, GlTabs,
GlTab, GlTab,
GlButton, GlButton,
GlSafeHtmlDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql'; import alertQuery from '../graphql/queries/details.query.graphql';
...@@ -28,6 +29,7 @@ import SystemNote from './system_notes/system_note.vue'; ...@@ -28,6 +29,7 @@ import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue'; import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue'; import AlertMetrics from './alert_metrics.vue';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertSummaryRow from './alert_summary_row.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar'); const containerEl = document.querySelector('.page-with-contextual-sidebar');
...@@ -39,6 +41,9 @@ export default { ...@@ -39,6 +41,9 @@ export default {
reportedAt: s__('AlertManagement|Reported %{when}'), reportedAt: s__('AlertManagement|Reported %{when}'),
reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'), reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
}, },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
severityLabels: ALERTS_SEVERITY_LABELS, severityLabels: ALERTS_SEVERITY_LABELS,
tabsConfig: [ tabsConfig: [
{ {
...@@ -56,9 +61,11 @@ export default { ...@@ -56,9 +61,11 @@ export default {
], ],
components: { components: {
AlertDetailsTable, AlertDetailsTable,
AlertSummaryRow,
GlBadge, GlBadge,
GlAlert, GlAlert,
GlIcon, GlIcon,
GlLink,
GlLoadingIcon, GlLoadingIcon,
GlSprintf, GlSprintf,
GlTab, GlTab,
...@@ -211,7 +218,7 @@ export default { ...@@ -211,7 +218,7 @@ export default {
<template> <template>
<div> <div>
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError"> <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError">
<p v-html="sidebarErrorMessage || $options.i18n.errorMsg"></p> <p v-safe-html="sidebarErrorMessage || $options.i18n.errorMsg"></p>
</gl-alert> </gl-alert>
<gl-alert <gl-alert
v-if="createIncidentError" v-if="createIncidentError"
...@@ -283,54 +290,66 @@ export default { ...@@ -283,54 +290,66 @@ export default {
</div> </div>
<gl-tabs v-if="alert" v-model="currentTabIndex" data-testid="alertDetailsTabs"> <gl-tabs v-if="alert" v-model="currentTabIndex" data-testid="alertDetailsTabs">
<gl-tab :data-testid="$options.tabsConfig[0].id" :title="$options.tabsConfig[0].title"> <gl-tab :data-testid="$options.tabsConfig[0].id" :title="$options.tabsConfig[0].title">
<div v-if="alert.severity" class="gl-mt-3 gl-mb-5 gl-display-flex"> <alert-summary-row v-if="alert.severity" :label="`${s__('AlertManagement|Severity')}:`">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> <span data-testid="severity">
{{ s__('AlertManagement|Severity') }}: <gl-icon
</div> class="gl-vertical-align-middle"
<div class="gl-pl-2" data-testid="severity"> :size="12"
<span> :name="`severity-${alert.severity.toLowerCase()}`"
<gl-icon :class="`icon-${alert.severity.toLowerCase()}`"
class="gl-vertical-align-middle" />
:size="12"
:name="`severity-${alert.severity.toLowerCase()}`"
:class="`icon-${alert.severity.toLowerCase()}`"
/>
</span>
{{ $options.severityLabels[alert.severity] }} {{ $options.severityLabels[alert.severity] }}
</div> </span>
</div> </alert-summary-row>
<div v-if="alert.startedAt" class="gl-my-5 gl-display-flex"> <alert-summary-row
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> v-if="alert.environment"
{{ s__('AlertManagement|Start time') }}: :label="`${s__('AlertManagement|Environment')}:`"
</div> >
<div class="gl-pl-2"> <gl-link
<time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" /> v-if="alert.environmentUrl"
</div> class="gl-display-inline-block"
</div> data-testid="environmentUrl"
<div v-if="alert.eventCount" class="gl-my-5 gl-display-flex"> :href="alert.environmentUrl"
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> target="_blank"
{{ s__('AlertManagement|Events') }}: >
</div> {{ alert.environment }}
<div class="gl-pl-2" data-testid="eventCount">{{ alert.eventCount }}</div> </gl-link>
</div> <span v-else data-testid="environment">{{ alert.environment }}</span>
<div v-if="alert.monitoringTool" class="gl-my-5 gl-display-flex"> </alert-summary-row>
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> <alert-summary-row
{{ s__('AlertManagement|Tool') }}: v-if="alert.startedAt"
</div> :label="`${s__('AlertManagement|Start time')}:`"
<div class="gl-pl-2" data-testid="monitoringTool">{{ alert.monitoringTool }}</div> >
</div> <time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" />
<div v-if="alert.service" class="gl-my-5 gl-display-flex"> </alert-summary-row>
<div class="bold gl-w-13 gl-text-right gl-pr-3"> <alert-summary-row
{{ s__('AlertManagement|Service') }}: v-if="alert.eventCount"
</div> :label="`${s__('AlertManagement|Events')}:`"
<div class="gl-pl-2" data-testid="service">{{ alert.service }}</div> data-testid="eventCount"
</div> >
<div v-if="alert.runbook" class="gl-my-5 gl-display-flex"> {{ alert.eventCount }}
<div class="bold gl-w-13 gl-text-right gl-pr-3"> </alert-summary-row>
{{ s__('AlertManagement|Runbook') }}: <alert-summary-row
</div> v-if="alert.monitoringTool"
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div> :label="`${s__('AlertManagement|Tool')}:`"
</div> data-testid="monitoringTool"
>
{{ alert.monitoringTool }}
</alert-summary-row>
<alert-summary-row
v-if="alert.service"
:label="`${s__('AlertManagement|Service')}:`"
data-testid="service"
>
{{ alert.service }}
</alert-summary-row>
<alert-summary-row
v-if="alert.runbook"
:label="`${s__('AlertManagement|Runbook')}:`"
data-testid="runbook"
>
{{ alert.runbook }}
</alert-summary-row>
<alert-details-table :alert="alert" :loading="loading" /> <alert-details-table :alert="alert" :loading="loading" />
</gl-tab> </gl-tab>
<gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title"> <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
......
<script>
export default {
props: {
label: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="gl-my-5 gl-display-flex">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">{{ label }}</div>
<div class="gl-pl-2">
<slot></slot>
</div>
</div>
</template>
<script> <script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui'; import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { reduce } from 'lodash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { import {
capitalizeFirstCharacter, capitalizeFirstCharacter,
...@@ -21,10 +22,10 @@ const allowedFields = [ ...@@ -21,10 +22,10 @@ const allowedFields = [
'description', 'description',
'endedAt', 'endedAt',
'details', 'details',
'environment',
]; ];
const filterAllowedFields = ([fieldName]) => allowedFields.includes(fieldName); const isAllowed = fieldName => allowedFields.includes(fieldName);
const arrayToObject = ([fieldName, value]) => ({ fieldName, value });
export default { export default {
components: { components: {
...@@ -62,9 +63,16 @@ export default { ...@@ -62,9 +63,16 @@ export default {
if (!this.alert) { if (!this.alert) {
return []; return [];
} }
return Object.entries(this.alert) return reduce(
.filter(filterAllowedFields) this.alert,
.map(arrayToObject); (allowedItems, value, fieldName) => {
if (isAllowed(fieldName)) {
return [...allowedItems, { fieldName, value }];
}
return allowedItems;
},
[],
);
}, },
}, },
}; };
......
...@@ -8,6 +8,8 @@ msgid "" ...@@ -8,6 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-22 19:32+0200\n"
"PO-Revision-Date: 2020-09-22 19:32+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -2234,6 +2236,9 @@ msgstr "" ...@@ -2234,6 +2236,9 @@ msgstr ""
msgid "AlertManagement|Edit" msgid "AlertManagement|Edit"
msgstr "" msgstr ""
msgid "AlertManagement|Environment"
msgstr ""
msgid "AlertManagement|Events" msgid "AlertManagement|Events"
msgstr "" msgstr ""
......
...@@ -2,8 +2,10 @@ import { mount, shallowMount } from '@vue/test-utils'; ...@@ -2,8 +2,10 @@ import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertDetails from '~/alert_management/components/alert_details.vue'; import AlertDetails from '~/alert_management/components/alert_details.vue';
import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql'; import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths } from '~/lib/utils/url_utility';
import { import {
...@@ -24,31 +26,36 @@ describe('AlertDetails', () => { ...@@ -24,31 +26,36 @@ describe('AlertDetails', () => {
const $router = { replace: jest.fn() }; const $router = { replace: jest.fn() };
function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) { function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
wrapper = mountMethod(AlertDetails, { wrapper = extendedWrapper(
provide: { mountMethod(AlertDetails, {
alertId: 'alertId', provide: {
projectPath, alertId: 'alertId',
projectIssuesPath, projectPath,
projectId, projectIssuesPath,
}, projectId,
data() { },
return { alert: { ...mockAlert }, sidebarStatus: false, ...data }; data() {
}, return { alert: { ...mockAlert }, sidebarStatus: false, ...data };
mocks: { },
$apollo: { mocks: {
mutate: jest.fn(), $apollo: {
queries: { mutate: jest.fn(),
alert: { queries: {
loading, alert: {
loading,
},
sidebarStatus: {},
}, },
sidebarStatus: {},
}, },
$router,
$route: { params: {} },
}, },
$router, stubs: {
$route: { params: {} }, ...stubs,
}, AlertSummaryRow,
stubs, },
}); }),
);
} }
beforeEach(() => { beforeEach(() => {
...@@ -62,9 +69,10 @@ describe('AlertDetails', () => { ...@@ -62,9 +69,10 @@ describe('AlertDetails', () => {
mock.restore(); mock.restore();
}); });
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]'); const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn');
const findViewIncidentBtn = () => wrapper.find('[data-testid="viewIncidentBtn"]'); const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn');
const findIncidentCreationAlert = () => wrapper.find('[data-testid="incidentCreationError"]'); const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError');
const findEnvironmentLink = () => wrapper.findByTestId('environmentUrl');
const findDetailsTable = () => wrapper.find(AlertDetailsTable); const findDetailsTable = () => wrapper.find(AlertDetailsTable);
describe('Alert details', () => { describe('Alert details', () => {
...@@ -74,7 +82,7 @@ describe('AlertDetails', () => { ...@@ -74,7 +82,7 @@ describe('AlertDetails', () => {
}); });
it('shows an empty state', () => { it('shows an empty state', () => {
expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false); expect(wrapper.findByTestId('alertDetailsTabs').exists()).toBe(false);
}); });
}); });
...@@ -84,28 +92,26 @@ describe('AlertDetails', () => { ...@@ -84,28 +92,26 @@ describe('AlertDetails', () => {
}); });
it('renders a tab with overview information', () => { it('renders a tab with overview information', () => {
expect(wrapper.find('[data-testid="overview"]').exists()).toBe(true); expect(wrapper.findByTestId('overview').exists()).toBe(true);
}); });
it('renders a tab with an activity feed', () => { it('renders a tab with an activity feed', () => {
expect(wrapper.find('[data-testid="activity"]').exists()).toBe(true); expect(wrapper.findByTestId('activity').exists()).toBe(true);
}); });
it('renders severity', () => { it('renders severity', () => {
expect(wrapper.find('[data-testid="severity"]').text()).toBe( expect(wrapper.findByTestId('severity').text()).toBe(
ALERTS_SEVERITY_LABELS[mockAlert.severity], ALERTS_SEVERITY_LABELS[mockAlert.severity],
); );
}); });
it('renders a title', () => { it('renders a title', () => {
expect(wrapper.find('[data-testid="title"]').text()).toBe(mockAlert.title); expect(wrapper.findByTestId('title').text()).toBe(mockAlert.title);
}); });
it('renders a start time', () => { it('renders a start time', () => {
expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true); expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true);
expect(wrapper.find('[data-testid="startTimeItem"]').props().time).toBe( expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt);
mockAlert.startedAt,
);
}); });
}); });
...@@ -114,6 +120,8 @@ describe('AlertDetails', () => { ...@@ -114,6 +120,8 @@ describe('AlertDetails', () => {
field | data | isShown field | data | isShown
${'eventCount'} | ${1} | ${true} ${'eventCount'} | ${1} | ${true}
${'eventCount'} | ${undefined} | ${false} ${'eventCount'} | ${undefined} | ${false}
${'environment'} | ${undefined} | ${false}
${'environment'} | ${'Production'} | ${true}
${'monitoringTool'} | ${'New Relic'} | ${true} ${'monitoringTool'} | ${'New Relic'} | ${true}
${'monitoringTool'} | ${undefined} | ${false} ${'monitoringTool'} | ${undefined} | ${false}
${'service'} | ${'Prometheus'} | ${true} ${'service'} | ${'Prometheus'} | ${true}
...@@ -126,15 +134,29 @@ describe('AlertDetails', () => { ...@@ -126,15 +134,29 @@ describe('AlertDetails', () => {
}); });
it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => { it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => {
const element = wrapper.findByTestId(field);
if (isShown) { if (isShown) {
expect(wrapper.find(`[data-testid="${field}"]`).text()).toBe(data.toString()); expect(element.text()).toContain(data.toString());
} else { } else {
expect(wrapper.find(`[data-testid="${field}"]`).exists()).toBe(false); expect(wrapper.findByTestId(field).exists()).toBe(false);
} }
}); });
}); });
}); });
describe('environment URL fields', () => {
it('should show the environment URL when available', () => {
const environment = 'Production';
const environmentUrl = 'fake/url';
mountComponent({
data: { alert: { ...mockAlert, environment, environmentUrl } },
});
expect(findEnvironmentLink().text()).toBe(environment);
expect(findEnvironmentLink().attributes('href')).toBe(environmentUrl);
});
});
describe('Create incident from alert', () => { describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => { it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3'; const issueIid = '3';
...@@ -222,7 +244,7 @@ describe('AlertDetails', () => { ...@@ -222,7 +244,7 @@ describe('AlertDetails', () => {
mountComponent({ mountComponent({
data: { errored: true, sidebarErrorMessage: '<span data-testid="htmlError" />' }, data: { errored: true, sidebarErrorMessage: '<span data-testid="htmlError" />' },
}); });
expect(wrapper.find('[data-testid="htmlError"]').exists()).toBe(true); expect(wrapper.findByTestId('htmlError').exists()).toBe(true);
}); });
it('does not display an error when dismissed', () => { it('does not display an error when dismissed', () => {
...@@ -232,7 +254,7 @@ describe('AlertDetails', () => { ...@@ -232,7 +254,7 @@ describe('AlertDetails', () => {
}); });
describe('header', () => { describe('header', () => {
const findHeader = () => wrapper.find('[data-testid="alert-header"]'); const findHeader = () => wrapper.findByTestId('alert-header');
const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } }; const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } };
describe('individual header fields', () => { describe('individual header fields', () => {
......
import { shallowMount } from '@vue/test-utils';
import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
const label = 'a label';
const value = 'a value';
describe('AlertSummaryRow', () => {
let wrapper;
function mountComponent({ mountMethod = shallowMount, props, defaultSlot } = {}) {
wrapper = mountMethod(AlertSummaryRow, {
propsData: props,
scopedSlots: {
default: defaultSlot,
},
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('Alert Summary Row', () => {
beforeEach(() => {
mountComponent({
props: {
label,
},
defaultSlot: `<span class="value">${value}</span>`,
});
});
it('should display a label and a value', () => {
expect(wrapper.text()).toBe(`${label} ${value}`);
});
});
});
...@@ -33,3 +33,10 @@ export const waitForMutation = (store, expectedMutationType) => ...@@ -33,3 +33,10 @@ export const waitForMutation = (store, expectedMutationType) =>
} }
}); });
}); });
export const extendedWrapper = wrapper =>
Object.defineProperty(wrapper, 'findByTestId', {
value(id) {
return this.find(`[data-testid="${id}"]`);
},
});
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const mockAlert = { const mockAlert = {
...@@ -61,8 +61,10 @@ describe('AlertDetails', () => { ...@@ -61,8 +61,10 @@ describe('AlertDetails', () => {
}); });
describe('with table data', () => { describe('with table data', () => {
const environment = 'myEnvironment';
const environmentUrl = 'fake/url';
beforeEach(() => { beforeEach(() => {
mountComponent(); mountComponent({ alert: { ...mockAlert, environment, environmentUrl } });
}); });
it('renders a table', () => { it('renders a table', () => {
...@@ -80,6 +82,7 @@ describe('AlertDetails', () => { ...@@ -80,6 +82,7 @@ describe('AlertDetails', () => {
expect(findTableField(fields, 'Title').exists()).toBe(true); expect(findTableField(fields, 'Title').exists()).toBe(true);
expect(findTableField(fields, 'Severity').exists()).toBe(true); expect(findTableField(fields, 'Severity').exists()).toBe(true);
expect(findTableField(fields, 'Status').exists()).toBe(true); expect(findTableField(fields, 'Status').exists()).toBe(true);
expect(findTableField(fields, 'Environment').exists()).toBe(true);
}); });
it('should not show disallowed alert fields', () => { it('should not show disallowed alert fields', () => {
...@@ -89,6 +92,7 @@ describe('AlertDetails', () => { ...@@ -89,6 +92,7 @@ describe('AlertDetails', () => {
expect(findTableField(fields, 'Todos').exists()).toBe(false); expect(findTableField(fields, 'Todos').exists()).toBe(false);
expect(findTableField(fields, 'Notes').exists()).toBe(false); expect(findTableField(fields, 'Notes').exists()).toBe(false);
expect(findTableField(fields, 'Assignees').exists()).toBe(false); expect(findTableField(fields, 'Assignees').exists()).toBe(false);
expect(findTableField(fields, 'EnvironmentUrl').exists()).toBe(false);
}); });
}); });
}); });
......
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