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

Add Notifications coloumn in alerts table #5943

Merged
merged 2 commits into from Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -66,10 +66,8 @@ export const MonitoringAlerts: React.FC<props> = ({ match, rules, filters, listS
);

React.useEffect(() => {
const sortThanosRules = _.sortBy(thanosAlertsAndRules.rules, (rule) =>
alertingRuleStateOrder(rule),
);
dispatch(monitoringSetRules('devRules', sortThanosRules));
const sortThanosRules = _.sortBy(thanosAlertsAndRules.rules, alertingRuleStateOrder);
dispatch(monitoringSetRules('devRules', sortThanosRules, 'dev'));
dispatch(monitoringLoaded('devAlerts', thanosAlertsAndRules.alerts, 'dev'));
}, [dispatch, thanosAlertsAndRules]);

Expand Down Expand Up @@ -98,7 +96,7 @@ export const MonitoringAlerts: React.FC<props> = ({ match, rules, filters, listS
const { id } = rows[rowKey].cells[0];
if (!_.includes(collapsedRowsIds, id)) {
setCollapsedRowsIds([...collapsedRowsIds, id]);
} else if (_.includes(collapsedRowsIds, id) && isOpen) {
} else if (_.includes(collapsedRowsIds, id)) {
setCollapsedRowsIds(_.without(collapsedRowsIds, id));
}
setRows([...rows]);
Expand Down
Expand Up @@ -7,7 +7,10 @@ import { useDispatch } from 'react-redux';
import { history, StatusBox, LoadingBox } from '@console/internal/components/utils';
import { ALL_NAMESPACES_KEY } from '@console/shared';
import NamespacedPage, { NamespacedPageVariants } from '../../NamespacedPage';
import { AlertsDetailsPage } from '@console/internal/components/monitoring/alerting';
import {
AlertsDetailsPage,
AlertRulesDetailsPage,
} from '@console/internal/components/monitoring/alerting';
import { PrometheusRulesResponse } from '@console/internal/components/monitoring/types';
import { useURLPoll } from '@console/internal/components/utils/url-poll-hook';
import { PROMETHEUS_TENANCY_BASE_PATH } from '@console/internal/components/graphs';
Expand All @@ -23,6 +26,8 @@ interface MonitoringAlertsDetailsPageProps {
}

const POLL_DELAY = 15 * 1000;
const ALERT_DETAILS_PATH = '/dev-monitoring/ns/:ns/alerts/:ruleID';
const RULE_DETAILS_PATH = '/dev-monitoring/ns/:ns/rules/:id';

const handleNamespaceChange = (newNamespace: string): void => {
if (newNamespace === ALL_NAMESPACES_KEY) {
Expand All @@ -34,6 +39,7 @@ const handleNamespaceChange = (newNamespace: string): void => {

const MonitoringAlertsDetailsPage: React.FC<MonitoringAlertsDetailsPageProps> = ({ match }) => {
const namespace = match.params.ns;
const { path } = match;
const dispatch = useDispatch();
const [response, loadError, loading] = useURLPoll<PrometheusRulesResponse>(
`${PROMETHEUS_TENANCY_BASE_PATH}/api/v1/rules?namespace=${namespace}`,
Expand All @@ -46,10 +52,8 @@ const MonitoringAlertsDetailsPage: React.FC<MonitoringAlertsDetailsPageProps> =
);

React.useEffect(() => {
const sortThanosRules = _.sortBy(thanosAlertsAndRules.rules, (rule) =>
alertingRuleStateOrder(rule),
);
dispatch(monitoringSetRules('devRules', sortThanosRules));
const sortThanosRules = _.sortBy(thanosAlertsAndRules.rules, alertingRuleStateOrder);
dispatch(monitoringSetRules('devRules', sortThanosRules, 'dev'));
dispatch(monitoringLoaded('devAlerts', thanosAlertsAndRules.alerts, 'dev'));
}, [dispatch, thanosAlertsAndRules]);

Expand All @@ -67,7 +71,8 @@ const MonitoringAlertsDetailsPage: React.FC<MonitoringAlertsDetailsPageProps> =
hideApplications
onNamespaceChange={handleNamespaceChange}
>
<AlertsDetailsPage match={match} />
{path === ALERT_DETAILS_PATH && <AlertsDetailsPage match={match} />}
{path === RULE_DETAILS_PATH && <AlertRulesDetailsPage match={match} />}
</NamespacedPage>
);
};
Expand Down
@@ -0,0 +1,55 @@
import * as React from 'react';
import * as _ from 'lodash';
import { Switch } from '@patternfly/react-core';
import { Rule } from '@console/internal/components/monitoring/types';
import { RuleStates } from '@console/internal/reducers/monitoring';
import { StateTimestamp } from '@console/internal/components/monitoring/alerting';
import { coFetchJSON } from '@console/internal/co-fetch';
import SilenceDurationDropDown from './SilenceDurationDropdown';

type SilenceAlertProps = {
rule: Rule;
};

const { alertManagerBaseURL } = window.SERVER_FLAGS;

const SilenceUntil = ({ rule }) => {
if (!_.isEmpty(rule.silencedBy)) {
return <StateTimestamp text="Until" timestamp={_.max(_.map(rule.silencedBy, 'endsAt'))} />;
}
return null;
};

const SilenceAlert: React.FC<SilenceAlertProps> = ({ rule }) => {
const [isChecked, setIsChecked] = React.useState(rule.state !== RuleStates.Silenced);
const [ruleState] = React.useState(rule);
React.useEffect(() => setIsChecked(ruleState.state !== RuleStates.Silenced), [ruleState]);

const handleChange = (checked: boolean) => {
if (checked) {
_.each(rule.silencedBy, (silence) => {
coFetchJSON.delete(`${alertManagerBaseURL}/api/v2/silence/${silence.id}`);
});
}
setIsChecked(checked);
};

return (
<Switch
aria-label="Silence switch"
className="odc-silence-alert"
label={null}
labelOff={
rule.state === RuleStates.Silenced ? (
<SilenceUntil rule={rule} />
) : (
<SilenceDurationDropDown rule={rule} />
)
}
isChecked={isChecked}
onChange={handleChange}
/>
);
};

export default SilenceAlert;
@@ -0,0 +1,67 @@
import * as React from 'react';
import * as _ from 'lodash';
// FIXME upgrading redux types is causing many errors at this time
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import { useSelector } from 'react-redux';
import { Dropdown } from '@console/internal/components/utils';
import { RootState } from '@console/internal/redux';
import { parsePrometheusDuration } from '@console/internal/components/utils/datetime';
import { coFetchJSON } from '@console/internal/co-fetch';
import { Rule } from '@console/internal/components/monitoring/types';

type SilenceDurationDropDownProps = {
rule: Rule;
};

const durations = {
silenceFor: 'Silence for',
'30m': '30 minutes',
'1h': '1 hour',
'2h': '2 hours',
'1d': '1 day',
};

const { alertManagerBaseURL } = window.SERVER_FLAGS;

const SilenceDurationDropDown: React.FC<SilenceDurationDropDownProps> = ({ rule }) => {
const createdBy = useSelector((state: RootState) => state.UI.get('user')?.metadata?.name);
const ruleMatchers = _.map(rule?.labels, (value, key) => ({ isRegex: false, name: key, value }));

const matchers = [
Copy link
Member

Choose a reason for hiding this comment

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

Should also add matchers for any additional labels found in rule.labels.

{
isRegex: false,
name: 'alertname',
value: rule.name,
},
...ruleMatchers,
];

const setDuration = (duration: string) => {
if (duration !== 'silenceFor') {
const startsAt = new Date();
const endsAt = new Date(startsAt.getTime() + parsePrometheusDuration(duration));

const payload = {
createdBy,
endsAt: endsAt.toISOString(),
startsAt: startsAt.toISOString(),
matchers,
comment: '',
};

coFetchJSON.post(`${alertManagerBaseURL}/api/v2/silences`, payload);
}
};

return (
<Dropdown
dropDownClassName="dropdown--full-width"
items={durations}
onChange={(v: string) => setDuration(v)}
selectedKey={'silenceFor'}
/>
);
};

export default SilenceDurationDropDown;
Expand Up @@ -6,12 +6,12 @@ import { RuleStates } from '@console/internal/reducers/monitoring';
describe('monitoring-alerts-utils', () => {
it('row should be expanded if rule state is FIRING', () => {
const alertRules = getAlertsAndRules(rules?.data).rules;
const rows = monitoringAlertRows(alertRules, ['KubeNodeReadinessFlapping'], 'ns');
const rows = monitoringAlertRows(alertRules, [alertRules[1].id], 'ns');
expect(rows[0].isOpen).toBe(true);
});
it('row should be collapsed if rule state is FIRING but collapsed by user', () => {
const alertRules = getAlertsAndRules(rules?.data).rules;
const rows = monitoringAlertRows(alertRules, ['KubeNodeNotReady'], 'ns');
const rows = monitoringAlertRows(alertRules, [alertRules[0].id], 'ns');
expect(rows[0].isOpen).toBe(false);
});
it('row should be collapse if rule state is not FIRING', () => {
Expand Down
Expand Up @@ -27,10 +27,12 @@ import {
import { Alert, Rule } from '@console/internal/components/monitoring/types';
import { YellowExclamationTriangleIcon } from '@console/shared';
import { labelsToParams } from '@console/internal/components/monitoring/utils';
import SilenceAlert from './SilenceAlert';

const viewAlertRule = {
const viewAlertRule = (rule: Rule, ns: string) => ({
label: 'View Alerting Rule',
};
href: `/dev-monitoring/ns/${ns}/rules/${rule.id}`,
});

type MonitoringAlertColumn = {
title: string;
Expand All @@ -50,16 +52,22 @@ export const monitoringAlertColumn: MonitoringAlertColumn[] = [
},
{
title: 'Severity',
transforms: [sortable, cellWidth(20)],
transforms: [sortable, cellWidth(10)],
fieldName: 'severity',
sortFunc: 'alertSeverityOrder',
},
{
title: 'Alert State',
transforms: [sortable, cellWidth(20)],
transforms: [sortable, cellWidth(15)],
fieldName: 'alertState',
sortFunc: 'alertingRuleStateOrder',
},
{
title: 'Notifications',
transforms: [sortable, cellWidth(20)],
fieldName: 'notifications',
sortFunc: 'alertingRuleNotificationsOrder',
},
{ title: '' },
];

Expand All @@ -72,7 +80,9 @@ export const monitoringAlertRows = (
_.forEach(alertrules, (rls) => {
rows.push({
...(rls.state !== RuleStates.Inactive && {
isOpen: rls.state === RuleStates.Firing && !_.includes(collapsedRowsIds, rls.name),
isOpen:
(rls.state === RuleStates.Firing && !_.includes(collapsedRowsIds, rls.id)) ||
(rls.state !== RuleStates.Firing && _.includes(collapsedRowsIds, rls.id)),
}),
cells: [
{
Expand All @@ -84,26 +94,29 @@ export const monitoringAlertRows = (
<YellowExclamationTriangleIcon /> {rls.name}
</>
),
id: rls.name,
id: rls.id,
},
{
title: <Severity severity={rls.labels?.severity} />,
},
{
title: _.isEmpty(rls.alerts) ? 'Not Firing' : <StateCounts alerts={rls.alerts} />,
},
{
title: <SilenceAlert rule={rls} />,
},
{
title: (
<div className="odc-monitoring-alerts--kebab">
<Kebab options={[viewAlertRule]} />
<Kebab options={[viewAlertRule(rls, namespace)]} />
</div>
),
},
],
});
_.forEach(rls.alerts, (alert: Alert) => {
rows.push({
parent: _.findIndex(rows, (r) => r.cells[0].id === rls.name),
parent: _.findIndex(rows, (r) => r.cells[0].id === rls.id),
fullWidth: true,
cells: [
{
Expand All @@ -124,7 +137,7 @@ export const monitoringAlertRows = (
<AlertState state={alertState(alert)} />
</div>
),
props: { colSpan: 2 },
props: { colSpan: 3 },
},
],
});
Expand All @@ -138,7 +151,7 @@ export const alertFilters = [
filterGroupName: 'Alert State',
type: 'alert-state',
reducer: alertState,
items: alertsRowFilters[0].items,
items: [...alertsRowFilters[0].items, ...[{ id: 'inactive', title: 'Not Firing' }]],
},
severityRowFilter,
];
Expand All @@ -147,10 +160,16 @@ const setOrderBy = (orderBy: SortByDirection, data: Rule[]): Rule[] => {
return orderBy === SortByDirection.asc ? data : data.reverse();
};

const alertingRuleNotificationsOrder = (rule: Rule) => [
rule.state === RuleStates.Silenced ? 1 : 0,
rule.state,
];

const sortFunc = {
nameOrder: (rule) => rule.name,
alertSeverityOrder,
alertingRuleStateOrder,
alertingRuleNotificationsOrder,
};

export const applyListSort = (rules: Rule[], orderBy: SortByDirection, func: string): Rule[] => {
Expand Down
15 changes: 14 additions & 1 deletion frontend/packages/dev-console/src/plugin.tsx
Expand Up @@ -805,7 +805,20 @@ const plugin: Plugin<ConsumedExtensions> = [
loader: async () =>
(
await import(
'./components/monitoring/alerts/MonitoringAlertsDetailsPage' /* webpackChunkName: "dev-console-monitoring-alerts" */
'./components/monitoring/alerts/MonitoringAlertsRulesDetailsPage' /* webpackChunkName: "dev-console-monitoring-alerts" */
)
).default,
},
},
{
type: 'Page/Route',
properties: {
exact: false,
path: ['/dev-monitoring/ns/:ns/rules/:id'],
loader: async () =>
(
await import(
'./components/monitoring/alerts/MonitoringAlertsRulesDetailsPage' /* webpackChunkName: "dev-console-monitoring-rules" */
)
).default,
},
Expand Down
9 changes: 6 additions & 3 deletions frontend/public/actions/ui.ts
Expand Up @@ -328,16 +328,19 @@ export const monitoringLoaded = (
data: { loaded: true, loadError: null, data: alerts, perspective },
});
export const monitoringErrored = (
key: 'alerts' | 'silences' | 'notificationAlerts',
key: 'alerts' | 'silences' | 'notificationAlerts' | 'devAlerts',
loadError: any,
perspective = 'admin',
) =>
action(ActionType.SetMonitoringData, {
key,
data: { loaded: true, loadError, data: null, perspective },
});
export const monitoringSetRules = (key: 'rules' | 'devRules', rules: Rule[]) =>
action(ActionType.MonitoringSetRules, { key, data: rules });
export const monitoringSetRules = (
key: 'rules' | 'devRules',
rules: Rule[],
perspective = 'admin',
) => action(ActionType.MonitoringSetRules, { key, data: rules, perspective });
export const monitoringToggleGraphs = () => action(ActionType.ToggleMonitoringGraphs);
export const notificationDrawerToggleExpanded = () =>
action(ActionType.NotificationDrawerToggleExpanded);
Expand Down