mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: misc ui fixes volume 4 (#34503)
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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}>
|
||||||
|
{!!subroutes.length ? (
|
||||||
<AmRoutesTable
|
<AmRoutesTable
|
||||||
isAddMode={isAddMode}
|
isAddMode={isAddMode}
|
||||||
onCancelAdd={() => {
|
onCancelAdd={() => {
|
||||||
@@ -62,6 +63,9 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({ onChange,
|
|||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={subroutes}
|
routes={subroutes}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<p>No nested policies configured.</p>
|
||||||
|
)}
|
||||||
{!isAddMode && (
|
{!isAddMode && (
|
||||||
<Button
|
<Button
|
||||||
className={styles.addNestedRoutingBtn}
|
className={styles.addNestedRoutingBtn}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user