Skip to content

Commit

Permalink
[Monitoring] Some progress on making alerts better in the UI (elastic…
Browse files Browse the repository at this point in the history
…#81569)

* Some progress on making alerts better in the UI

* Handle edge case

* Updates

* More updates

* Show kibana instances alerts better

* Stop showing missing nodes and improve the detail alert UI

* WIP

* Fix the badge display

* Okay I think this is finally working

* Fix type issues

* Fix tests

* Fix tests

* Fix alert counts

* Fix setup mode listing

* Better detail page view of alerts

* Feedback

* Sorting

* Fix a couple small issues

* Start of unit tests

* I don't think we need this Mock type

* Fix types

* More tests

* Improve tests and fix sorting

* Make this test more resilient

* Updates after merging master

* Fix tests

* Fix types, and improve tests

* PR comments

* Remove nextStep logic

* PR feedback

* PR feedback

* Removing unnecessary changes

* Fixing bad merge issues

* Remove unused imports

* Add tooltip to alerts grouped by node

* Fix up stateFilter usage

* Code clean up

* PR feedback

* Fix state filtering in the category list

* Fix types

* Fix test

* Fix types

* Update snapshots

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
chrisronline and kibanamachine committed Dec 13, 2020
1 parent a2771a3 commit b5af419
Show file tree
Hide file tree
Showing 51 changed files with 3,289 additions and 418 deletions.
36 changes: 36 additions & 0 deletions x-pack/plugins/monitoring/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,42 @@ export const ALERT_DETAILS = {
},
};

export const ALERT_PANEL_MENU = [
{
label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.clusterHealth', {
defaultMessage: 'Cluster health',
}),
alerts: [
{ alertName: ALERT_NODES_CHANGED },
{ alertName: ALERT_CLUSTER_HEALTH },
{ alertName: ALERT_ELASTICSEARCH_VERSION_MISMATCH },
{ alertName: ALERT_KIBANA_VERSION_MISMATCH },
{ alertName: ALERT_LOGSTASH_VERSION_MISMATCH },
],
},
{
label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.resourceUtilization', {
defaultMessage: 'Resource utilization',
}),
alerts: [
{ alertName: ALERT_CPU_USAGE },
{ alertName: ALERT_DISK_USAGE },
{ alertName: ALERT_MEMORY_USAGE },
],
},
{
label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.errors', {
defaultMessage: 'Errors and exceptions',
}),
alerts: [
{ alertName: ALERT_MISSING_MONITORING_DATA },
{ alertName: ALERT_LICENSE_EXPIRATION },
{ alertName: ALERT_THREAD_POOL_SEARCH_REJECTIONS },
{ alertName: ALERT_THREAD_POOL_WRITE_REJECTIONS },
],
},
];

/**
* A listing of all alert types
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ export const SMALL_FLOAT = '0.[00]';
export const LARGE_BYTES = '0,0.0 b';
export const SMALL_BYTES = '0.0 b';
export const LARGE_ABBREVIATED = '0,0.[0]a';
export const ROUNDED_FLOAT = '00.[00]';

/**
* Format the {@code date} in the user's expected date/time format using their <em>guessed</em> local time zone.
* @param date Either a numeric Unix timestamp or a {@code Date} object
* @returns The date formatted using 'LL LTS'
*/
export function formatDateTimeLocal(date, useUTC = false, timezone = null) {
export function formatDateTimeLocal(date: number | Date, useUTC = false, timezone = null) {
return useUTC
? moment.utc(date).format('LL LTS')
: moment.tz(date, timezone || moment.tz.guess()).format('LL LTS');
Expand All @@ -28,6 +29,18 @@ export function formatDateTimeLocal(date, useUTC = false, timezone = null) {
* @param {string} hash The complete hash
* @return {string} The shortened hash
*/
export function shortenPipelineHash(hash) {
export function shortenPipelineHash(hash: string) {
return hash.substr(0, 6);
}

export function getDateFromNow(timestamp: string | number | Date, tz: string) {
return moment(timestamp)
.tz(tz === 'Browser' ? moment.tz.guess() : tz)
.fromNow();
}

export function getCalendar(timestamp: string | number | Date, tz: string) {
return moment(timestamp)
.tz(tz === 'Browser' ? moment.tz.guess() : tz)
.calendar();
}
3 changes: 3 additions & 0 deletions x-pack/plugins/monitoring/common/types/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import { Alert, SanitizedAlert } from '../../../alerts/common';
import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums';

export type CommonAlert = Alert | SanitizedAlert;

export interface CommonAlertStatus {
states: CommonAlertState[];
rawAlert: Alert | SanitizedAlert;
Expand Down Expand Up @@ -179,6 +181,7 @@ export interface LegacyAlert {
message: string;
resolved_timestamp: string;
metadata: LegacyAlertMetadata;
nodeName: string;
nodes?: LegacyAlertNodesChangedList;
}

Expand Down
236 changes: 82 additions & 154 deletions x-pack/plugins/monitoring/public/alerts/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,187 +4,115 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Fragment } from 'react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiContextMenu,
EuiPopover,
EuiBadge,
EuiFlexGrid,
EuiFlexItem,
EuiText,
} from '@elastic/eui';
import { CommonAlertStatus, CommonAlertState } from '../../common/types/alerts';
import { EuiContextMenu, EuiPopover, EuiBadge, EuiSwitch } from '@elastic/eui';
import { AlertState, CommonAlertStatus } from '../../common/types/alerts';
import { AlertSeverity } from '../../common/enums';
// @ts-ignore
import { formatDateTimeLocal } from '../../common/formatting';
import { AlertState } from '../../common/types/alerts';
import { AlertPanel } from './panel';
import { Legacy } from '../legacy_shims';
import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';

function getDateFromState(state: CommonAlertState) {
const timestamp = state.state.ui.triggeredMS;
const tz = Legacy.shims.uiSettings.get('dateFormat:tz');
return formatDateTimeLocal(timestamp, false, tz === 'Browser' ? null : tz);
}
import { AlertsContext } from './context';
import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';

export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`;

interface AlertInPanel {
alert: CommonAlertStatus;
alertState: CommonAlertState;
}
const MAX_TO_SHOW_BY_CATEGORY = 8;

const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
});

const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', {
defaultMessage: 'Group by node',
});

const GROUP_BY_TYPE = i18n.translate('xpack.monitoring.alerts.badge.groupByType', {
defaultMessage: 'Group by alert type',
});

interface Props {
alerts: { [alertTypeId: string]: CommonAlertStatus };
stateFilter: (state: AlertState) => boolean;
}
export const AlertsBadge: React.FC<Props> = (props: Props) => {
// We do not always have the alerts that each consumer wants due to licensing
const { stateFilter = () => true } = props;
const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert));
const [showPopover, setShowPopover] = React.useState<AlertSeverity | boolean | null>(null);
const inSetupMode = isInSetupMode(React.useContext(SetupModeContext));
const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert));

if (alerts.length === 0) {
return null;
}

const badges = [];

if (inSetupMode) {
const button = (
<EuiBadge
iconType="bell"
onClickAriaLabel={numberOfAlertsLabel(alerts.length)}
onClick={() => setShowPopover(true)}
>
{numberOfAlertsLabel(alerts.length)}
</EuiBadge>
);
const panels = [
{
id: 0,
title: i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
}),
items: alerts.map(({ rawAlert }, index) => {
return {
name: <EuiText>{rawAlert.name}</EuiText>,
panel: index + 1,
};
}),
},
...alerts.map((alertStatus, index) => {
return {
id: index + 1,
title: alertStatus.rawAlert.name,
width: 400,
content: <AlertPanel alert={alertStatus} />,
};
}),
];

badges.push(
<EuiPopover
id="monitoringAlertMenu"
button={button}
isOpen={showPopover === true}
closePopover={() => setShowPopover(null)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
} else {
const byType = {
[AlertSeverity.Danger]: [] as AlertInPanel[],
[AlertSeverity.Warning]: [] as AlertInPanel[],
[AlertSeverity.Success]: [] as AlertInPanel[],
};
const alertsContext = React.useContext(AlertsContext);
const alertCount = inSetupMode
? alerts.length
: alerts.reduce(
(sum, { states }) => sum + states.filter(({ state }) => stateFilter(state)).length,
0
);
const [showByNode, setShowByNode] = React.useState(
!inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY
);

for (const alert of alerts) {
for (const alertState of alert.states) {
if (alertState.firing && stateFilter(alertState.state)) {
const state = alertState.state as AlertState;
byType[state.ui.severity].push({
alertState,
alert,
});
}
}
React.useEffect(() => {
if (inSetupMode && showByNode) {
setShowByNode(false);
}
}, [inSetupMode, showByNode]);

const typesToShow = [AlertSeverity.Danger, AlertSeverity.Warning];
for (const type of typesToShow) {
const list = byType[type];
if (list.length === 0) {
continue;
}
if (alertCount === 0) {
return null;
}

const button = (
<EuiBadge
iconType="bell"
color={type}
onClickAriaLabel={numberOfAlertsLabel(list.length)}
onClick={() => setShowPopover(type)}
>
{numberOfAlertsLabel(list.length)}
</EuiBadge>
);
const groupByType = GROUP_BY_NODE;
const panels = showByNode
? getAlertPanelsByNode(PANEL_TITLE, alerts, stateFilter)
: getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, alertsContext, stateFilter);

const panels = [
if (panels.length && !inSetupMode && panels[0].items) {
panels[0].items.push(
...[
{
id: 0,
title: `Alerts`,
items: list.map(({ alert, alertState }, index) => {
return {
name: (
<Fragment>
<EuiText size="s">
<h4>{getDateFromState(alertState)}</h4>
</EuiText>
<EuiText>{alert.rawAlert.name}</EuiText>
</Fragment>
),
panel: index + 1,
};
}),
isSeparator: true as const,
},
...list.map((alertStatus, index) => {
return {
id: index + 1,
title: getDateFromState(alertStatus.alertState),
width: 400,
content: <AlertPanel alert={alertStatus.alert} alertState={alertStatus.alertState} />,
};
}),
];

badges.push(
<EuiPopover
id="monitoringAlertMenu"
button={button}
isOpen={showPopover === type}
closePopover={() => setShowPopover(null)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
{
name: (
<EuiSwitch
checked={false}
onChange={() => setShowByNode(!showByNode)}
label={showByNode ? GROUP_BY_TYPE : groupByType}
/>
),
},
]
);
}

const button = (
<EuiBadge
iconType="bell"
color={inSetupMode ? 'default' : 'danger'}
onClickAriaLabel={numberOfAlertsLabel(alertCount)}
onClick={() => setShowPopover(true)}
>
{numberOfAlertsLabel(alertCount)}
</EuiBadge>
);

return (
<EuiFlexGrid data-test-subj="monitoringSetupModeAlertBadges">
{badges.map((badge, index) => (
<EuiFlexItem key={index} grow={false}>
{badge}
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiPopover
id="monitoringAlertMenu"
button={button}
isOpen={showPopover === true}
closePopover={() => setShowPopover(null)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu
key={`${showByNode ? 'byNode' : 'byType'}_${panels.length}`}
initialPanelId={0}
panels={panels}
/>
</EuiPopover>
);
};
Loading

0 comments on commit b5af419

Please sign in to comment.