Alerting: misc ui fixes volume 4 (#34503)

This commit is contained in:
Domas
2021-05-25 10:26:10 +03:00
committed by GitHub
parent 1d3bcb0e90
commit d666defaea
11 changed files with 86 additions and 55 deletions

View File

@@ -54,21 +54,21 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<FieldArray name="matchers" control={control}> <FieldArray name="matchers" control={control}>
{({ fields, append, remove }) => ( {({ fields, append, remove }) => (
<> <>
<div>Matchers</div> <div>Matching labels</div>
<div className={styles.matchersContainer}> <div className={styles.matchersContainer}>
{fields.map((field, index) => { {fields.map((field, index) => {
const localPath = `matchers[${index}]`; const localPath = `matchers[${index}]`;
return ( return (
<HorizontalGroup key={field.id} align="flex-start"> <HorizontalGroup key={field.id} align="flex-start">
<Field <Field
label="Name" label="Label"
invalid={!!errors.matchers?.[index]?.name} invalid={!!errors.matchers?.[index]?.name}
error={errors.matchers?.[index]?.name?.message} error={errors.matchers?.[index]?.name?.message}
> >
<Input <Input
{...register(`${localPath}.name`, { required: 'Field is required' })} {...register(`${localPath}.name`, { required: 'Field is required' })}
defaultValue={field.name} defaultValue={field.name}
placeholder="label"
/> />
</Field> </Field>
<Field <Field
@@ -79,6 +79,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<Input <Input
{...register(`${localPath}.value`, { required: 'Field is required' })} {...register(`${localPath}.value`, { required: 'Field is required' })}
defaultValue={field.value} defaultValue={field.value}
placeholder="value"
/> />
</Field> </Field>
<Field className={styles.matcherRegexField} label="Regex"> <Field className={styles.matcherRegexField} label="Regex">

View File

@@ -38,30 +38,34 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({ onChange,
<div className={gridStyles.valueCell}>{repeatInterval}</div> <div className={gridStyles.valueCell}>{repeatInterval}</div>
<div className={gridStyles.titleCell}>Nested policies</div> <div className={gridStyles.titleCell}>Nested policies</div>
<div className={gridStyles.valueCell}> <div className={gridStyles.valueCell}>
<AmRoutesTable {!!subroutes.length ? (
isAddMode={isAddMode} <AmRoutesTable
onCancelAdd={() => { isAddMode={isAddMode}
setIsAddMode(false); onCancelAdd={() => {
setSubroutes((subroutes) => {
const newSubroutes = [...subroutes];
newSubroutes.pop();
return newSubroutes;
});
}}
onChange={(newRoutes) => {
onChange({
...routes,
routes: newRoutes,
});
if (isAddMode) {
setIsAddMode(false); setIsAddMode(false);
} setSubroutes((subroutes) => {
}} const newSubroutes = [...subroutes];
receivers={receivers} newSubroutes.pop();
routes={subroutes}
/> return newSubroutes;
});
}}
onChange={(newRoutes) => {
onChange({
...routes,
routes: newRoutes,
});
if (isAddMode) {
setIsAddMode(false);
}
}}
receivers={receivers}
routes={subroutes}
/>
) : (
<p>No nested policies configured.</p>
)}
{!isAddMode && ( {!isAddMode && (
<Button <Button
className={styles.addNestedRoutingBtn} className={styles.addNestedRoutingBtn}

View File

@@ -65,7 +65,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({ isAddMode, onCancelAdd,
const cols: RouteTableColumnProps[] = [ const cols: RouteTableColumnProps[] = [
{ {
id: 'matchingCriteria', id: 'matchingCriteria',
label: 'Matching criteria', label: 'Matching labels',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: (item) => <Matchers matchers={item.data.matchers} />, renderCell: (item) => <Matchers matchers={item.data.matchers} />,
size: 10, size: 10,

View File

@@ -3,7 +3,7 @@ import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/ty
import React, { FC, useMemo, useState } from 'react'; import React, { FC, useMemo, useState } from 'react';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { getAlertTableStyles } from '../../styles/table'; import { getAlertTableStyles } from '../../styles/table';
import { extractReadableNotifierTypes } from '../../utils/receivers'; import { extractNotifierTypeCounts } from '../../utils/receivers';
import { ActionIcon } from '../rules/ActionIcon'; import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection'; import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
@@ -48,7 +48,14 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
() => () =>
config.alertmanager_config.receivers?.map((receiver) => ({ config.alertmanager_config.receivers?.map((receiver) => ({
name: receiver.name, name: receiver.name,
types: extractReadableNotifierTypes(receiver, grafanaNotifiers.result ?? []), types: Object.entries(extractNotifierTypeCounts(receiver, grafanaNotifiers.result ?? [])).map(
([type, count]) => {
if (count > 1) {
return `${type} (${count})`;
}
return type;
}
),
})) ?? [], })) ?? [],
[config, grafanaNotifiers.result] [config, grafanaNotifiers.result]
); );

View File

@@ -66,8 +66,14 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
values: { values: {
...defaultValues, ...defaultValues,
...values, ...values,
annotations: values.annotations?.filter(({ key, value }) => !!key && !!value) ?? [], annotations:
labels: values.labels?.filter(({ key }) => !!key) ?? [], values.annotations
?.map(({ key, value }) => ({ key: key.trim(), value: value.trim() }))
.filter(({ key, value }) => !!key && !!value) ?? [],
labels:
values.labels
?.map(({ key, value }) => ({ key: key.trim(), value: value.trim() }))
.filter(({ key }) => !!key) ?? [],
}, },
existing, existing,
redirectOnSave: exitOnSave ? returnTo : undefined, redirectOnSave: exitOnSave ? returnTo : undefined,

View File

@@ -8,7 +8,7 @@ interface Props {
} }
export const AlertInstanceDetails: FC<Props> = ({ instance }) => { export const AlertInstanceDetails: FC<Props> = ({ instance }) => {
const annotations = Object.entries(instance.annotations || {}) || []; const annotations = (Object.entries(instance.annotations || {}) || []).filter(([_, value]) => !!value.trim());
return ( return (
<div> <div>

View File

@@ -24,7 +24,7 @@ export const RuleDetails: FC<Props> = ({ rule, rulesSource }) => {
const { promRule } = rule; const { promRule } = rule;
const annotations = Object.entries(rule.annotations); const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim());
const dataSources: Array<{ name: string; icon?: string }> = useMemo(() => { const dataSources: Array<{ name: string; icon?: string }> = useMemo(() => {
if (isCloudRulesSource(rulesSource)) { if (isCloudRulesSource(rulesSource)) {

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
import { HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui'; import { HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { isAlertingRule, isRecordingRule } from '../../utils/rules'; import { isAlertingRule, isRecordingRule } from '../../utils/rules';
import { AlertStateTag } from './AlertStateTag'; import { AlertStateTag } from './AlertStateTag';
@@ -27,7 +27,7 @@ export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
) { ) {
// find earliest alert // find earliest alert
const firstActiveAt = promRule.alerts.reduce((prev, alert) => { const firstActiveAt = promRule.alerts.reduce((prev, alert) => {
if (alert.activeAt) { if (alert.activeAt && alert.state !== GrafanaAlertState.Normal) {
const activeAt = new Date(alert.activeAt); const activeAt = new Date(alert.activeAt);
if (prev === null || prev.getTime() > activeAt.getTime()) { if (prev === null || prev.getTime() > activeAt.getTime()) {
return activeAt; return activeAt;
@@ -36,7 +36,7 @@ export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
return prev; return prev;
}, null as Date | null); }, null as Date | null);
// caclulate time elapsed from earliest alert // calculate time elapsed from earliest alert
if (firstActiveAt) { if (firstActiveAt) {
return ( return (
<span title={String(firstActiveAt)} className={style.for}> <span title={String(firstActiveAt)} className={style.for}>

View File

@@ -21,15 +21,14 @@ const MatchersField: FC<Props> = ({ className }) => {
return ( return (
<div className={cx(className, styles.wrapper)}> <div className={cx(className, styles.wrapper)}>
<Field label="Matchers" required> <Field label="Matching labels" required>
<div> <div>
<div className={styles.matchers}> <div className={styles.matchers}>
{matchers.map((matcher, index) => { {matchers.map((matcher, index) => {
console.log(matcher);
return ( return (
<div className={styles.row} key={`${matcher.id}`}> <div className={styles.row} key={`${matcher.id}`}>
<Field <Field
label="Name" label="Label"
invalid={!!errors?.matchers?.[index]?.name} invalid={!!errors?.matchers?.[index]?.name}
error={errors?.matchers?.[index]?.name?.message} error={errors?.matchers?.[index]?.name?.message}
> >
@@ -38,7 +37,7 @@ const MatchersField: FC<Props> = ({ className }) => {
required: { value: true, message: 'Required.' }, required: { value: true, message: 'Required.' },
})} })}
defaultValue={matcher.name} defaultValue={matcher.name}
placeholder="name" placeholder="label"
/> />
</Field> </Field>
<Field <Field

View File

@@ -48,7 +48,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
<tr> <tr>
<th /> <th />
<th>State</th> <th>State</th>
<th>Matchers</th> <th>Matching labels</th>
<th>Alerts</th> <th>Alerts</th>
<th>Schedule</th> <th>Schedule</th>
{contextSrv.isEditor && <th>Action</th>} {contextSrv.isEditor && <th>Action</th>}

View File

@@ -3,29 +3,43 @@ import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/a
import { NotifierDTO } from 'app/types'; import { NotifierDTO } from 'app/types';
import { capitalize } from 'lodash'; import { capitalize } from 'lodash';
// extract readable notifier types that are in use for a receiver, eg ['Slack', 'Email', 'PagerDuty'] // extract notifier type name to count map, eg { Slack: 1, Email: 2 }
export function extractReadableNotifierTypes(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): string[] {
return [ type NotifierTypeCounts = Record<string, number>; // name : count
// grafana specific receivers
...getReadabaleGrafanaNotifierTypes(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers), export function extractNotifierTypeCounts(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): NotifierTypeCounts {
// cortex alert manager receivers if (receiver['grafana_managed_receiver_configs']) {
...getReadableCortexAlertManagerNotifierTypes(receiver), return getGrafanaNotifierTypeCounts(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers);
]; }
return getCortexAlertManagerNotifierTypeCounts(receiver);
} }
function getReadableCortexAlertManagerNotifierTypes(receiver: Receiver): string[] { function getCortexAlertManagerNotifierTypeCounts(receiver: Receiver): NotifierTypeCounts {
return Object.entries(receiver) return Object.entries(receiver)
.filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) // filter out only properties that are alert manager notifier .filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) // filter out only properties that are alert manager notifier
.filter(([_, value]) => Array.isArray(value) && !!value.length) // check that there are actually notifiers of this type configured .filter(([_, value]) => Array.isArray(value) && !!value.length) // check that there are actually notifiers of this type configured
.map(([key]) => key.replace('_configs', '')) // remove the `_config` part from the key, making it intto a notifier name .reduce<NotifierTypeCounts>((acc, [key, value]) => {
.map((type) => receiverTypeNames[type] ?? capitalize(type)); // either map to readable name or, failing that, capitalize const type = key.replace('_configs', ''); // remove the `_config` part from the key, making it intto a notifier name
const name = receiverTypeNames[type] ?? capitalize(type);
return {
...acc,
[name]: (acc[name] ?? 0) + (Array.isArray(value) ? value.length : 1),
};
}, {});
} }
function getReadabaleGrafanaNotifierTypes( function getGrafanaNotifierTypeCounts(
configs: GrafanaManagedReceiverConfig[], configs: GrafanaManagedReceiverConfig[],
grafanaNotifiers: NotifierDTO[] grafanaNotifiers: NotifierDTO[]
): string[] { ): NotifierTypeCounts {
return configs return configs
.map((recv) => recv.type) // extract types from config .map((recv) => recv.type) // extract types from config
.map((type) => grafanaNotifiers.find((r) => r.type === type)?.name ?? capitalize(type)); // get readable name from notifier cofnig, or if not available, just capitalize .map((type) => grafanaNotifiers.find((r) => r.type === type)?.name ?? capitalize(type)) // get readable name from notifier cofnig, or if not available, just capitalize
.reduce<NotifierTypeCounts>(
(acc, type) => ({
...acc,
[type]: (acc[type] ?? 0) + 1,
}),
{}
);
} }