Skip to content

Commit

Permalink
Add Alerts tab in devconsole monitoring
Browse files Browse the repository at this point in the history
  • Loading branch information
vikram-raj committed Jul 21, 2020
1 parent 6bb23df commit 77d7338
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 47 deletions.
Expand Up @@ -9,7 +9,7 @@ import { match as RMatch } from 'react-router-dom';
import { Table, TableHeader, TableBody, SortByDirection } from '@patternfly/react-table';
import { FilterToolbar } from '@console/internal/components/filter-toolbar';
import { getAlertsAndRules } from '@console/internal/components/monitoring/utils';
import { monitoringSetRules, sortList } from '@console/internal/actions/ui';
import { monitoringSetRules, sortList, monitoringLoaded } from '@console/internal/actions/ui';
import { useURLPoll } from '@console/internal/components/utils/url-poll-hook';
import { PrometheusRulesResponse, Rules } from '@console/internal/components/monitoring/types';
import { PROMETHEUS_TENANCY_BASE_PATH } from '@console/internal/components/graphs';
Expand Down Expand Up @@ -60,15 +60,18 @@ export const MonitoringAlerts: React.FC<props> = ({ match, rules, filters, listS
POLL_DELAY,
namespace,
);
const thanosRules = React.useMemo(
() => (!loading && !loadError ? getAlertsAndRules(response?.data).rules : []),
const thanosAlertsAndRules = React.useMemo(
() => (!loading && !loadError ? getAlertsAndRules(response?.data) : { rules: [], alerts: [] }),
[response, loadError, loading],
);

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

const filteredRules = React.useMemo(() => {
const filtersObj = filters?.toJS();
Expand All @@ -86,16 +89,16 @@ export const MonitoringAlerts: React.FC<props> = ({ match, rules, filters, listS
}, [filters, listSorts, columnIndex, rules, listOrderBy, sortOrder]);

React.useEffect(() => {
const tableRows = monitoringAlertRows(filteredRules, collapsedRowsIds);
const tableRows = monitoringAlertRows(filteredRules, collapsedRowsIds, namespace);
setRows(tableRows);
}, [collapsedRowsIds, filteredRules]);
}, [collapsedRowsIds, filteredRules, namespace]);

const onCollapse = (event: React.MouseEvent, rowKey: number, isOpen: boolean) => {
rows[rowKey].isOpen = isOpen;
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
@@ -0,0 +1,32 @@
import * as React from 'react';
import { AlertRulesDetailsPage } from '@console/internal/components/monitoring/alerting';
import { history } from '@console/internal/components/utils';
import { ALL_NAMESPACES_KEY } from '@console/shared';
import NamespacedPage, { NamespacedPageVariants } from '../../NamespacedPage';
import { match as RMatch } from 'react-router-dom';

type MonitoringRuleDetailsPageProps = {
match: RMatch<{
ns?: string;
}>;
};

const handleNamespaceChange = (ns: string): void => {
ns === ALL_NAMESPACES_KEY
? history.push('/dev-monitoring/all-namespaces')
: history.push('/dev-monitoring/ns/:ns/alerts');
};

const MonitoringRuleDetailsPage: React.FC<MonitoringRuleDetailsPageProps> = ({ match }) => {
return (
<NamespacedPage
variant={NamespacedPageVariants.light}
hideApplications
onNamespaceChange={handleNamespaceChange}
>
<AlertRulesDetailsPage match={match} />
</NamespacedPage>
);
};

export default MonitoringRuleDetailsPage;
@@ -0,0 +1,5 @@
.odc-silence-alert {
.pf-c-switch__label.pf-m-on {
display: none;
}
}
@@ -0,0 +1,51 @@
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 SilenceDurationDropDown from './SilenceDurationDropdown';
import './SilenceAlert.scss';

type SilenceAlertProps = {
rule: Rule;
namespace: string;
};

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

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

const handleChange = (checked: boolean) => {
setIsChecked(checked);
};

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

export default SilenceAlert;
@@ -0,0 +1,64 @@
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;
namespace: string;
};

const SILENCE_FOR = 'Silence for ';
const durations = [SILENCE_FOR, '30m', '1h', '2h', '6h', '12h', '1d', '2d', '1w'];
const durationItems = _.zipObject(durations, durations);
const { alertManagerBaseURL } = window.SERVER_FLAGS;

const SilenceDurationDropDown: React.FC<SilenceDurationDropDownProps> = ({ rule, namespace }) => {
const createdBy = useSelector((state: RootState) => state.UI.get('user')?.metadata?.name);

const matchers = [
{
isRegex: false,
name: 'alertname',
value: rule.name,
},
{
isRegex: false,
name: 'namespace',
value: namespace,
},
];

const setDuration = (duration: string) => {
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={durationItems}
onChange={(v: string) => setDuration(v)}
selectedKey={SILENCE_FOR}
/>
);
};

export default SilenceDurationDropDown;
Expand Up @@ -6,18 +6,18 @@ 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']);
const rows = monitoringAlertRows(alertRules, ['KubeNodeReadinessFlapping'], 'namespace');
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']);
const rows = monitoringAlertRows(alertRules, ['KubeNodeNotReady'], 'namespace');
expect(rows[0].isOpen).toBe(false);
});
it('row should be collapse if rule state is not FIRING', () => {
const alertRules = getAlertsAndRules(rules?.data).rules;
alertRules[0].state = RuleStates.Inactive;
const rows = monitoringAlertRows(alertRules, ['']);
const rows = monitoringAlertRows(alertRules, [''], 'namespace');
expect(rows[0].isOpen).toBe(undefined);
});
});
Expand Up @@ -25,10 +25,12 @@ import {
} from '@console/internal/reducers/monitoring';
import { Alert, Rule } from '@console/internal/components/monitoring/types';
import { YellowExclamationTriangleIcon } from '@console/shared';
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 Down Expand Up @@ -58,15 +60,22 @@ export const monitoringAlertColumn: MonitoringAlertColumn[] = [
fieldName: 'alertState',
sortFunc: 'alertingRuleStateOrder',
},
{ title: 'Notifications', transforms: [cellWidth(20)] },
{ title: '' },
];

export const monitoringAlertRows = (alertrules: Rule[], collapsedRowsIds: string[]) => {
export const monitoringAlertRows = (
alertrules: Rule[],
collapsedRowsIds: string[],
namespace: string,
) => {
const rows = [];
_.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.name)) ||
(rls.state !== RuleStates.Firing && _.includes(collapsedRowsIds, rls.name)),
}),
cells: [
{
Expand All @@ -86,10 +95,13 @@ export const monitoringAlertRows = (alertrules: Rule[], collapsedRowsIds: string
{
title: _.isEmpty(rls.alerts) ? 'Not Firing' : <StateCounts alerts={rls.alerts} />,
},
{
title: <SilenceAlert rule={rls} namespace={namespace} />,
},
{
title: (
<div className="odc-monitoring-alerts--kebab">
<Kebab options={[viewAlertRule]} />
<Kebab options={[viewAlertRule(rls, namespace)]} />
</div>
),
},
Expand All @@ -112,7 +124,7 @@ export const monitoringAlertRows = (alertrules: Rule[], collapsedRowsIds: string
<AlertState state={alertState(alert)} />
</div>
),
props: { colSpan: 2 },
props: { colSpan: 3 },
},
],
});
Expand Down
13 changes: 13 additions & 0 deletions frontend/packages/dev-console/src/plugin.tsx
Expand Up @@ -750,6 +750,19 @@ const plugin: Plugin<ConsumedExtensions> = [
).default,
},
},
{
type: 'Page/Route',
properties: {
exact: false,
path: ['/dev-monitoring/ns/:ns/rules/:id'],
loader: async () =>
(
await import(
'./components/monitoring/alerts/MonitoringRuleDetailsPage' /* webpackChunkName: "dev-console-monitoring-rules" */
)
).default,
},
},
{
type: 'Page/Route',
properties: {
Expand Down
16 changes: 12 additions & 4 deletions frontend/public/actions/ui.ts
Expand Up @@ -310,15 +310,23 @@ export const monitoringDashboardsSetTimespan = (timespan: number) =>
action(ActionType.MonitoringDashboardsSetTimespan, { timespan });
export const monitoringDashboardsVariableOptionsLoaded = (key: string, newOptions: string[]) =>
action(ActionType.MonitoringDashboardsVariableOptionsLoaded, { key, newOptions });
export const monitoringLoading = (key: 'alerts' | 'silences' | 'notificationAlerts') =>
export const monitoringLoading = (
key: 'alerts' | 'silences' | 'notificationAlerts' | 'devAlerts',
) =>
action(ActionType.SetMonitoringData, {
key,
data: { loaded: false, loadError: null, data: null },
});
export const monitoringLoaded = (key: 'alerts' | 'silences' | 'notificationAlerts', data: any) =>
action(ActionType.SetMonitoringData, { key, data: { loaded: true, loadError: null, data } });
export const monitoringLoaded = (
key: 'alerts' | 'silences' | 'notificationAlerts' | 'devAlerts',
alerts: any,
) =>
action(ActionType.SetMonitoringData, {
key,
data: { loaded: true, loadError: null, data: alerts },
});
export const monitoringErrored = (
key: 'alerts' | 'silences' | 'notificationAlerts',
key: 'alerts' | 'silences' | 'notificationAlerts' | 'devAlerts',
loadError: any,
) => action(ActionType.SetMonitoringData, { key, data: { loaded: true, loadError, data: null } });
export const monitoringSetRules = (key: 'rules' | 'devRules', rules: Rule[]) =>
Expand Down

0 comments on commit 77d7338

Please sign in to comment.