Skip to content

Commit

Permalink
adds the AlertDetails page (elastic#55671)
Browse files Browse the repository at this point in the history
This PR adds an Alerts Details page, linked from the Alerts List.

It includes:
Header containing details about the Alert.
Quick Enable / Mute buttons for the Alert
Disabled buttons to the _Edit Alert+ flyout (waiting on elastic#51545), View in App (waiting on elastic#56298) and Activity Log (waiting on elastic#51548)
  • Loading branch information
gmmorris authored and John Schulz committed Feb 4, 2020
1 parent c1c7360 commit 777b6bc
Show file tree
Hide file tree
Showing 36 changed files with 2,527 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import {
IUiSettingsClient,
ApplicationStart,
} from 'kibana/public';
import { BASE_PATH, Section } from './constants';
import { BASE_PATH, Section, routeToAlertDetails } from './constants';
import { TriggersActionsUIHome } from './home';
import { AppContextProvider, useAppDependencies } from './app_context';
import { hasShowAlertsCapability } from './lib/capabilities';
import { LegacyDependencies, ActionTypeModel, AlertTypeModel } from '../types';
import { TypeRegistry } from './type_registry';
import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route';

export interface AppDeps {
chrome: ChromeStart;
Expand Down Expand Up @@ -53,11 +54,8 @@ export const AppWithoutRouter = ({ sectionsRegex }: any) => {
const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors';
return (
<Switch>
<Route
exact
path={`${BASE_PATH}/:section(${sectionsRegex})`}
component={TriggersActionsUIHome}
/>
<Route path={`${BASE_PATH}/:section(${sectionsRegex})`} component={TriggersActionsUIHome} />
{canShowAlerts && <Route path={routeToAlertDetails} component={AlertDetailsRoute} />}
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
</Switch>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Section = 'connectors' | 'alerts';
export const routeToHome = `${BASE_PATH}`;
export const routeToConnectors = `${BASE_PATH}/connectors`;
export const routeToAlerts = `${BASE_PATH}/alerts`;
export const routeToAlertDetails = `${BASE_PATH}/alert/:alertId`;

export { TIME_UNITS } from './time_units';
export enum SORT_ORDERS {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('loadActionTypes', () => {
{
id: 'test',
name: 'Test',
enabled: true,
},
];
http.get.mockResolvedValueOnce(resolvedValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ import { Alert, AlertType } from '../../types';
import { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
import {
createAlert,
deleteAlert,
deleteAlerts,
disableAlerts,
enableAlerts,
disableAlert,
enableAlert,
loadAlert,
loadAlerts,
loadAlertTypes,
muteAlerts,
unmuteAlerts,
muteAlert,
unmuteAlert,
updateAlert,
} from './alert_api';
import uuid from 'uuid';

const http = httpServiceMock.createStartContract();

Expand All @@ -42,6 +49,31 @@ describe('loadAlertTypes', () => {
});
});

describe('loadAlert', () => {
test('should call get API with base parameters', async () => {
const alertId = uuid.v4();
const resolvedValue = {
id: alertId,
name: 'name',
tags: [],
enabled: true,
alertTypeId: '.noop',
schedule: { interval: '1s' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
throttle: null,
muteAll: false,
mutedInstanceIds: [],
};
http.get.mockResolvedValueOnce(resolvedValue);

expect(await loadAlert({ http, alertId })).toEqual(resolvedValue);
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`);
});
});

describe('loadAlerts', () => {
test('should call find API with base parameters', async () => {
const resolvedValue = {
Expand Down Expand Up @@ -230,6 +262,19 @@ describe('loadAlerts', () => {
});
});

describe('deleteAlert', () => {
test('should call delete API for alert', async () => {
const id = '1';
const result = await deleteAlert({ http, id });
expect(result).toEqual(undefined);
expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/api/alert/1",
]
`);
});
});

describe('deleteAlerts', () => {
test('should call delete API for each alert', async () => {
const ids = ['1', '2', '3'];
Expand Down Expand Up @@ -335,6 +380,62 @@ describe('updateAlert', () => {
});
});

describe('enableAlert', () => {
test('should call enable alert API', async () => {
const result = await enableAlert({ http, id: '1' });
expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"/api/alert/1/_enable",
],
]
`);
});
});

describe('disableAlert', () => {
test('should call disable alert API', async () => {
const result = await disableAlert({ http, id: '1' });
expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"/api/alert/1/_disable",
],
]
`);
});
});

describe('muteAlert', () => {
test('should call mute alert API', async () => {
const result = await muteAlert({ http, id: '1' });
expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"/api/alert/1/_mute_all",
],
]
`);
});
});

describe('unmuteAlert', () => {
test('should call unmute alert API', async () => {
const result = await unmuteAlert({ http, id: '1' });
expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"/api/alert/1/_unmute_all",
],
]
`);
});
});

describe('enableAlerts', () => {
test('should call enable alert API per alert', async () => {
const ids = ['1', '2', '3'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<Ale
return await http.get(`${BASE_ALERT_API_PATH}/types`);
}

export async function loadAlert({
http,
alertId,
}: {
http: HttpSetup;
alertId: string;
}): Promise<Alert> {
return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`);
}

export async function loadAlerts({
http,
page,
Expand Down Expand Up @@ -55,14 +65,18 @@ export async function loadAlerts({
});
}

export async function deleteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.delete(`${BASE_ALERT_API_PATH}/${id}`);
}

export async function deleteAlerts({
ids,
http,
}: {
ids: string[];
http: HttpSetup;
}): Promise<void> {
await Promise.all(ids.map(id => http.delete(`${BASE_ALERT_API_PATH}/${id}`)));
await Promise.all(ids.map(id => deleteAlert({ http, id })));
}

export async function createAlert({
Expand Down Expand Up @@ -91,14 +105,22 @@ export async function updateAlert({
});
}

export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`);
}

export async function enableAlerts({
ids,
http,
}: {
ids: string[];
http: HttpSetup;
}): Promise<void> {
await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`)));
await Promise.all(ids.map(id => enableAlert({ id, http })));
}

export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`);
}

export async function disableAlerts({
Expand All @@ -108,11 +130,19 @@ export async function disableAlerts({
ids: string[];
http: HttpSetup;
}): Promise<void> {
await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`)));
await Promise.all(ids.map(id => disableAlert({ id, http })));
}

export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`);
}

export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise<void> {
await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`)));
await Promise.all(ids.map(id => muteAlert({ http, id })));
}

export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`);
}

export async function unmuteAlerts({
Expand All @@ -122,5 +152,5 @@ export async function unmuteAlerts({
ids: string[];
http: HttpSetup;
}): Promise<void> {
await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`)));
await Promise.all(ids.map(id => unmuteAlert({ id, http })));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { throwIfAbsent, throwIfIsntContained } from './value_validators';
import uuid from 'uuid';

describe('throwIfAbsent', () => {
test('throws if value is absent', () => {
[undefined, null].forEach(val => {
expect(() => {
throwIfAbsent('OMG no value')(val);
}).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`);
});
});

test('doesnt throws if value is present but falsey', () => {
[false, ''].forEach(val => {
expect(throwIfAbsent('OMG no value')(val)).toEqual(val);
});
});

test('doesnt throw if value is present', () => {
expect(throwIfAbsent('OMG no value')({})).toEqual({});
});
});

describe('throwIfIsntContained', () => {
test('throws if value is absent', () => {
expect(() => {
throwIfIsntContained<string>(new Set([uuid.v4()]), 'OMG no value', val => val)([uuid.v4()]);
}).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`);
});

test('throws if value is absent using custom message', () => {
const id = uuid.v4();
expect(() => {
throwIfIsntContained<string>(
new Set([id]),
(value: string) => `OMG no ${value}`,
val => val
)([uuid.v4()]);
}).toThrow(`OMG no ${id}`);
});

test('returns values if value is present', () => {
const id = uuid.v4();
const values = [uuid.v4(), uuid.v4(), id, uuid.v4()];
expect(throwIfIsntContained<string>(new Set([id]), 'OMG no value', val => val)(values)).toEqual(
values
);
});

test('returns values if multiple values is present', () => {
const [firstId, secondId] = [uuid.v4(), uuid.v4()];
const values = [uuid.v4(), uuid.v4(), secondId, uuid.v4(), firstId];
expect(
throwIfIsntContained<string>(new Set([firstId, secondId]), 'OMG no value', val => val)(values)
).toEqual(values);
});

test('allows a custom value extractor', () => {
const [firstId, secondId] = [uuid.v4(), uuid.v4()];
const values = [
{ id: firstId, some: 'prop' },
{ id: secondId, someOther: 'prop' },
];
expect(
throwIfIsntContained<{ id: string }>(
new Set([firstId, secondId]),
'OMG no value',
(val: { id: string }) => val.id
)(values)
).toEqual(values);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { constant } from 'lodash';

export function throwIfAbsent<T>(message: string) {
return (value: T | undefined): T => {
if (value === undefined || value === null) {
throw new Error(message);
}
return value;
};
}

export function throwIfIsntContained<T>(
requiredValues: Set<string>,
message: string | ((requiredValue: string) => string),
valueExtractor: (value: T) => string
) {
const toError = typeof message === 'function' ? message : constant(message);
return (values: T[]) => {
const availableValues = new Set(values.map(valueExtractor));
for (const value of requiredValues.values()) {
if (!availableValues.has(value)) {
throw new Error(toError(value));
}
}
return values;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ describe('action_connector_form', () => {
editFlyoutVisible: false,
setEditFlyoutVisibility: () => {},
actionTypesIndex: {
'my-action-type': { id: 'my-action-type', name: 'my-action-type-name' },
'my-action-type': {
id: 'my-action-type',
name: 'my-action-type-name',
enabled: true,
},
},
reloadConnectors: () => {
return new Promise<void>(() => {});
Expand Down
Loading

0 comments on commit 777b6bc

Please sign in to comment.