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>
/* eslint-disable vue/no-v-html */
import * as Sentry from '@sentry/browser';
import {
GlAlert,
GlBadge,
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
GlTabs,
GlTab,
GlButton,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql';
......@@ -28,6 +29,7 @@ import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.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');
......@@ -39,6 +41,9 @@ export default {
reportedAt: s__('AlertManagement|Reported %{when}'),
reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
severityLabels: ALERTS_SEVERITY_LABELS,
tabsConfig: [
{
......@@ -56,9 +61,11 @@ export default {
],
components: {
AlertDetailsTable,
AlertSummaryRow,
GlBadge,
GlAlert,
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
GlTab,
......@@ -211,7 +218,7 @@ export default {
<template>
<div>
<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
v-if="createIncidentError"
......@@ -283,54 +290,66 @@ export default {
</div>
<gl-tabs v-if="alert" v-model="currentTabIndex" data-testid="alertDetailsTabs">
<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">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Severity') }}:
</div>
<div class="gl-pl-2" data-testid="severity">
<span>
<alert-summary-row v-if="alert.severity" :label="`${s__('AlertManagement|Severity')}:`">
<span data-testid="severity">
<gl-icon
class="gl-vertical-align-middle"
:size="12"
:name="`severity-${alert.severity.toLowerCase()}`"
:class="`icon-${alert.severity.toLowerCase()}`"
/>
</span>
{{ $options.severityLabels[alert.severity] }}
</div>
</div>
<div v-if="alert.startedAt" class="gl-my-5 gl-display-flex">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Start time') }}:
</div>
<div class="gl-pl-2">
</span>
</alert-summary-row>
<alert-summary-row
v-if="alert.environment"
:label="`${s__('AlertManagement|Environment')}:`"
>
<gl-link
v-if="alert.environmentUrl"
class="gl-display-inline-block"
data-testid="environmentUrl"
:href="alert.environmentUrl"
target="_blank"
>
{{ alert.environment }}
</gl-link>
<span v-else data-testid="environment">{{ alert.environment }}</span>
</alert-summary-row>
<alert-summary-row
v-if="alert.startedAt"
:label="`${s__('AlertManagement|Start time')}:`"
>
<time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" />
</div>
</div>
<div v-if="alert.eventCount" class="gl-my-5 gl-display-flex">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Events') }}:
</div>
<div class="gl-pl-2" data-testid="eventCount">{{ alert.eventCount }}</div>
</div>
<div v-if="alert.monitoringTool" class="gl-my-5 gl-display-flex">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Tool') }}:
</div>
<div class="gl-pl-2" data-testid="monitoringTool">{{ alert.monitoringTool }}</div>
</div>
<div v-if="alert.service" class="gl-my-5 gl-display-flex">
<div class="bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Service') }}:
</div>
<div class="gl-pl-2" data-testid="service">{{ alert.service }}</div>
</div>
<div v-if="alert.runbook" class="gl-my-5 gl-display-flex">
<div class="bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Runbook') }}:
</div>
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
</div>
</alert-summary-row>
<alert-summary-row
v-if="alert.eventCount"
:label="`${s__('AlertManagement|Events')}:`"
data-testid="eventCount"
>
{{ alert.eventCount }}
</alert-summary-row>
<alert-summary-row
v-if="alert.monitoringTool"
:label="`${s__('AlertManagement|Tool')}:`"
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" />
</gl-tab>
<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>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { reduce } from 'lodash';
import { s__ } from '~/locale';
import {
capitalizeFirstCharacter,
......@@ -21,10 +22,10 @@ const allowedFields = [
'description',
'endedAt',
'details',
'environment',
];
const filterAllowedFields = ([fieldName]) => allowedFields.includes(fieldName);
const arrayToObject = ([fieldName, value]) => ({ fieldName, value });
const isAllowed = fieldName => allowedFields.includes(fieldName);
export default {
components: {
......@@ -62,9 +63,16 @@ export default {
if (!this.alert) {
return [];
}
return Object.entries(this.alert)
.filter(filterAllowedFields)
.map(arrayToObject);
return reduce(
this.alert,
(allowedItems, value, fieldName) => {
if (isAllowed(fieldName)) {
return [...allowedItems, { fieldName, value }];
}
return allowedItems;
},
[],
);
},
},
};
......
......@@ -8,6 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\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"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -2234,6 +2236,9 @@ msgstr ""
msgid "AlertManagement|Edit"
msgstr ""
msgid "AlertManagement|Environment"
msgstr ""
msgid "AlertManagement|Events"
msgstr ""
......
......@@ -2,8 +2,10 @@ import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import axios from 'axios';
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 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 { joinPaths } from '~/lib/utils/url_utility';
import {
......@@ -24,7 +26,8 @@ describe('AlertDetails', () => {
const $router = { replace: jest.fn() };
function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
wrapper = mountMethod(AlertDetails, {
wrapper = extendedWrapper(
mountMethod(AlertDetails, {
provide: {
alertId: 'alertId',
projectPath,
......@@ -47,8 +50,12 @@ describe('AlertDetails', () => {
$router,
$route: { params: {} },
},
stubs,
});
stubs: {
...stubs,
AlertSummaryRow,
},
}),
);
}
beforeEach(() => {
......@@ -62,9 +69,10 @@ describe('AlertDetails', () => {
mock.restore();
});
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findViewIncidentBtn = () => wrapper.find('[data-testid="viewIncidentBtn"]');
const findIncidentCreationAlert = () => wrapper.find('[data-testid="incidentCreationError"]');
const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn');
const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn');
const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError');
const findEnvironmentLink = () => wrapper.findByTestId('environmentUrl');
const findDetailsTable = () => wrapper.find(AlertDetailsTable);
describe('Alert details', () => {
......@@ -74,7 +82,7 @@ describe('AlertDetails', () => {
});
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', () => {
});
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', () => {
expect(wrapper.find('[data-testid="activity"]').exists()).toBe(true);
expect(wrapper.findByTestId('activity').exists()).toBe(true);
});
it('renders severity', () => {
expect(wrapper.find('[data-testid="severity"]').text()).toBe(
expect(wrapper.findByTestId('severity').text()).toBe(
ALERTS_SEVERITY_LABELS[mockAlert.severity],
);
});
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', () => {
expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="startTimeItem"]').props().time).toBe(
mockAlert.startedAt,
);
expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true);
expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt);
});
});
......@@ -114,6 +120,8 @@ describe('AlertDetails', () => {
field | data | isShown
${'eventCount'} | ${1} | ${true}
${'eventCount'} | ${undefined} | ${false}
${'environment'} | ${undefined} | ${false}
${'environment'} | ${'Production'} | ${true}
${'monitoringTool'} | ${'New Relic'} | ${true}
${'monitoringTool'} | ${undefined} | ${false}
${'service'} | ${'Prometheus'} | ${true}
......@@ -126,15 +134,29 @@ describe('AlertDetails', () => {
});
it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => {
const element = wrapper.findByTestId(field);
if (isShown) {
expect(wrapper.find(`[data-testid="${field}"]`).text()).toBe(data.toString());
expect(element.text()).toContain(data.toString());
} 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', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3';
......@@ -222,7 +244,7 @@ describe('AlertDetails', () => {
mountComponent({
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', () => {
......@@ -232,7 +254,7 @@ describe('AlertDetails', () => {
});
describe('header', () => {
const findHeader = () => wrapper.find('[data-testid="alert-header"]');
const findHeader = () => wrapper.findByTestId('alert-header');
const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } };
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) =>
}
});
});
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 { GlTable, GlLoadingIcon } from '@gitlab/ui';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const mockAlert = {
......@@ -61,8 +61,10 @@ describe('AlertDetails', () => {
});
describe('with table data', () => {
const environment = 'myEnvironment';
const environmentUrl = 'fake/url';
beforeEach(() => {
mountComponent();
mountComponent({ alert: { ...mockAlert, environment, environmentUrl } });
});
it('renders a table', () => {
......@@ -80,6 +82,7 @@ describe('AlertDetails', () => {
expect(findTableField(fields, 'Title').exists()).toBe(true);
expect(findTableField(fields, 'Severity').exists()).toBe(true);
expect(findTableField(fields, 'Status').exists()).toBe(true);
expect(findTableField(fields, 'Environment').exists()).toBe(true);
});
it('should not show disallowed alert fields', () => {
......@@ -89,6 +92,7 @@ describe('AlertDetails', () => {
expect(findTableField(fields, 'Todos').exists()).toBe(false);
expect(findTableField(fields, 'Notes').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