Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Monitoring] Some progress on making alerts better in the UI #81569

Merged
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
50e4d95
Some progress on making alerts better in the UI
chrisronline Oct 23, 2020
6aa082d
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Oct 26, 2020
03a9731
Handle edge case
chrisronline Oct 26, 2020
aa612ad
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Oct 27, 2020
bc8b7ac
Merge in master -a
chrisronline Nov 3, 2020
ca865c2
Updates
chrisronline Nov 3, 2020
c228539
More updates
chrisronline Nov 4, 2020
0d28e5c
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 5, 2020
e07860f
Show kibana instances alerts better
chrisronline Nov 5, 2020
46b3b9c
Stop showing missing nodes and improve the detail alert UI
chrisronline Nov 6, 2020
73c199f
WIP
chrisronline Nov 6, 2020
270b660
Fix the badge display
chrisronline Nov 10, 2020
6d4027a
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 10, 2020
91ebbf9
Okay I think this is finally working
chrisronline Nov 10, 2020
a2b8abb
Fix type issues
chrisronline Nov 11, 2020
c36ed5a
Fix tests
chrisronline Nov 11, 2020
2c8c752
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 11, 2020
70aa1c5
Fix tests
chrisronline Nov 11, 2020
b383f3f
Fix alert counts
chrisronline Nov 11, 2020
fa250d1
Fix setup mode listing
chrisronline Nov 11, 2020
8231ce0
Better detail page view of alerts
chrisronline Nov 12, 2020
d5ba0a9
Feedback
chrisronline Nov 16, 2020
07c7ac2
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 17, 2020
85e3e08
Sorting
chrisronline Nov 17, 2020
44fcb2b
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 18, 2020
3b95c70
Fix a couple small issues
chrisronline Nov 18, 2020
fb4cb7b
Start of unit tests
chrisronline Nov 19, 2020
7b38bea
I don't think we need this Mock type
chrisronline Nov 19, 2020
9cc615e
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 19, 2020
0a2a011
Fix types
chrisronline Nov 20, 2020
0e194ef
More tests
chrisronline Nov 20, 2020
95a7080
Improve tests and fix sorting
chrisronline Nov 20, 2020
b8c26bb
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Nov 23, 2020
b8d37e5
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Dec 1, 2020
fa16e1e
Make this test more resilient
chrisronline Dec 1, 2020
e7e7a25
Merge in master
chrisronline Dec 8, 2020
f450379
Updates after merging master
chrisronline Dec 8, 2020
e16b13d
Fix tests
chrisronline Dec 8, 2020
c548654
Fix types, and improve tests
chrisronline Dec 8, 2020
d487967
PR comments
chrisronline Dec 9, 2020
ad08ad8
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Dec 9, 2020
b88d8b3
Merge remote-tracking branch 'elastic/master' into monitoring/better_…
chrisronline Dec 10, 2020
9c7baee
Remove nextStep logic
chrisronline Dec 10, 2020
9e7a756
PR feedback
chrisronline Dec 10, 2020
88256a1
Merge in master
chrisronline Dec 10, 2020
cc659c6
PR feedback
chrisronline Dec 11, 2020
6abc318
Removing unnecessary changes
chrisronline Dec 11, 2020
5eae22b
Fixing bad merge issues
chrisronline Dec 11, 2020
8184b86
Remove unused imports
chrisronline Dec 11, 2020
a33cf5e
Add tooltip to alerts grouped by node
chrisronline Dec 11, 2020
e4f7d04
Fix up stateFilter usage
chrisronline Dec 11, 2020
d0c2369
Code clean up
chrisronline Dec 11, 2020
6aa31d4
PR feedback
chrisronline Dec 12, 2020
ae015fb
Fix state filtering in the category list
chrisronline Dec 12, 2020
0711e22
Fix types
chrisronline Dec 12, 2020
d9a7849
Fix test
chrisronline Dec 12, 2020
ee0e99d
Merge branch 'master' into monitoring/better_alerts_in_ui
kibanamachine Dec 13, 2020
5ff14e5
Fix types
chrisronline Dec 13, 2020
c98a4f1
Update snapshots
chrisronline Dec 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 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,41 @@ 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_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();
}
2 changes: 2 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
255 changes: 104 additions & 151 deletions x-pack/plugins/monitoring/public/alerts/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,187 +4,140 @@
* 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';
import {
BEATS_SYSTEM_ID,
ELASTICSEARCH_SYSTEM_ID,
KIBANA_SYSTEM_ID,
LOGSTASH_SYSTEM_ID,
} from '../../common/constants';

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_INSTANCE = i18n.translate('xpack.monitoring.alerts.badge.groupByInstace', {
defaultMessage: 'Group by instance',
});

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));
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
);

React.useEffect(() => {
if (inSetupMode && showByNode) {
setShowByNode(false);
}
}, [inSetupMode, showByNode]);

if (alerts.length === 0) {
if (alertCount === 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[],
};

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,
});
}
let groupByType = GROUP_BY_NODE;
for (const alert of alerts) {
for (const { state } of alert.states) {
switch (state.stackProduct) {
case ELASTICSEARCH_SYSTEM_ID:
case LOGSTASH_SYSTEM_ID:
groupByType = GROUP_BY_NODE;
break;
case KIBANA_SYSTEM_ID:
case BEATS_SYSTEM_ID:
groupByType = GROUP_BY_INSTANCE;
break;
}
}
}
chrisronline marked this conversation as resolved.
Show resolved Hide resolved

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

const button = (
<EuiBadge
iconType="bell"
color={type}
onClickAriaLabel={numberOfAlertsLabel(list.length)}
onClick={() => setShowPopover(type)}
>
{numberOfAlertsLabel(list.length)}
</EuiBadge>
);
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}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need a key here since since it's not iterated (at its nested scope).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can try to remove it. I originally added it because I saw strange behavior when toggling between the modes and React refusing to render any changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up adding this back in because I saw the same behavior. I'd be happy to solve the issue another way if you have ideas.

initialPanelId={0}
panels={panels}
/>
</EuiPopover>
);
};
Loading