mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add actions extension point to alert instances table view (#77900)
This PR adds a new [extension point][] to each row of the alert instances table. This allows plugins to add actions to a dropdown menu in the rightmost column of the table. Those actions are passed the alert instance so can use it for contextual handling. See https://github.com/grafana/machine-learning/pull/3461 for an example of how this can be used (e.g. by Grafana Sift here).
This commit is contained in:
parent
259ecb1793
commit
35b1e49024
@ -117,6 +117,7 @@ export type PluginExtensionEventHelpers<Context extends object = object> = {
|
||||
|
||||
// Extension Points available in core Grafana
|
||||
export enum PluginExtensionPoints {
|
||||
AlertInstanceAction = 'grafana/alerting/instance/action',
|
||||
DashboardPanelMenu = 'grafana/dashboard/panel/menu',
|
||||
DataSourceConfig = 'grafana/datasources/config',
|
||||
ExploreToolbarAction = 'grafana/explore/toolbar/action',
|
||||
|
@ -5,7 +5,14 @@ import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { DataSourceSrv, locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
import {
|
||||
DataSourceSrv,
|
||||
getPluginLinkExtensions,
|
||||
locationService,
|
||||
setBackendSrv,
|
||||
setDataSourceSrv,
|
||||
} from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||
import * as actions from 'app/features/alerting/unified/state/actions';
|
||||
@ -32,6 +39,10 @@ import {
|
||||
import * as config from './utils/config';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
}));
|
||||
jest.mock('./api/buildInfo');
|
||||
jest.mock('./api/prometheus');
|
||||
jest.mock('./api/ruler');
|
||||
@ -53,6 +64,7 @@ jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]);
|
||||
|
||||
const mocks = {
|
||||
getAllDataSourcesMock: jest.mocked(config.getAllDataSources),
|
||||
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
|
||||
rulesInSameGroupHaveInvalidForMock: jest.mocked(actions.rulesInSameGroupHaveInvalidFor),
|
||||
|
||||
api: {
|
||||
@ -137,6 +149,19 @@ describe('RuleList', () => {
|
||||
AccessControlAction.AlertingRuleExternalWrite,
|
||||
]);
|
||||
mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]);
|
||||
mocks.getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Run investigation',
|
||||
category: 'Sift',
|
||||
description: 'Run a Sift investigation for this alert',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -0,0 +1,65 @@
|
||||
import React, { ReactElement, useMemo, useState } from 'react';
|
||||
|
||||
import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data';
|
||||
import { getPluginLinkExtensions } from '@grafana/runtime';
|
||||
import { Dropdown, IconButton } from '@grafana/ui';
|
||||
import { ConfirmNavigationModal } from 'app/features/explore/extensions/ConfirmNavigationModal';
|
||||
import { Alert, CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertExtensionPointMenu } from './AlertInstanceExtensionPointMenu';
|
||||
|
||||
interface AlertInstanceExtensionPointProps {
|
||||
rule?: CombinedRule;
|
||||
instance: Alert;
|
||||
extensionPointId: PluginExtensionPoints;
|
||||
}
|
||||
|
||||
export const AlertInstanceExtensionPoint = ({
|
||||
rule,
|
||||
instance,
|
||||
extensionPointId,
|
||||
}: AlertInstanceExtensionPointProps): ReactElement | null => {
|
||||
const [selectedExtension, setSelectedExtension] = useState<PluginExtensionLink | undefined>();
|
||||
const context = { instance, rule };
|
||||
const extensions = useExtensionLinks(context, extensionPointId);
|
||||
|
||||
if (extensions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menu = <AlertExtensionPointMenu extensions={extensions} onSelect={setSelectedExtension} />;
|
||||
return (
|
||||
<>
|
||||
<Dropdown placement="bottom-start" overlay={menu}>
|
||||
<IconButton name="ellipsis-v" aria-label="Actions" variant="secondary" />
|
||||
</Dropdown>
|
||||
{!!selectedExtension && !!selectedExtension.path && (
|
||||
<ConfirmNavigationModal
|
||||
path={selectedExtension.path}
|
||||
title={selectedExtension.title}
|
||||
onDismiss={() => setSelectedExtension(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type PluginExtensionAlertInstanceContext = {
|
||||
rule?: CombinedRule;
|
||||
instance: Alert;
|
||||
};
|
||||
|
||||
function useExtensionLinks(
|
||||
context: PluginExtensionAlertInstanceContext,
|
||||
extensionPointId: PluginExtensionPoints
|
||||
): PluginExtensionLink[] {
|
||||
return useMemo(() => {
|
||||
const { extensions } = getPluginLinkExtensions({
|
||||
extensionPointId,
|
||||
context,
|
||||
limitPerPlugin: 3,
|
||||
});
|
||||
|
||||
return extensions;
|
||||
}, [context, extensionPointId]);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// We might want to customise this in future but right now the toolbar menu from the Explore view is fine.
|
||||
export { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu';
|
@ -4,7 +4,8 @@ import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
import { getPluginLinkExtensions, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
@ -48,10 +49,15 @@ const mockRoute = (id?: string): GrafanaRouteComponentProps<{ id?: string; sourc
|
||||
staticContext: {},
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../hooks/useIsRuleEditable');
|
||||
jest.mock('../../api/buildInfo');
|
||||
|
||||
const mocks = {
|
||||
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
|
||||
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
||||
};
|
||||
|
||||
@ -144,6 +150,19 @@ beforeEach(() => {
|
||||
},
|
||||
status: 'success',
|
||||
});
|
||||
mocks.getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Run investigation',
|
||||
category: 'Sift',
|
||||
description: 'Run a Sift investigation for this alert',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('RuleViewer', () => {
|
||||
|
@ -1,16 +1,18 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { dateTime, findCommonLabels } from '@grafana/data';
|
||||
import { Alert, PaginationProps } from 'app/types/unified-alerting';
|
||||
import { PluginExtensionPoints, dateTime, findCommonLabels } from '@grafana/data';
|
||||
import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting';
|
||||
|
||||
import { alertInstanceKey } from '../../utils/rules';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { AlertInstanceExtensionPoint } from '../extensions/AlertInstanceExtensionPoint';
|
||||
|
||||
import { AlertInstanceDetails } from './AlertInstanceDetails';
|
||||
import { AlertStateTag } from './AlertStateTag';
|
||||
|
||||
interface Props {
|
||||
rule?: CombinedRule;
|
||||
instances: Alert[];
|
||||
pagination?: PaginationProps;
|
||||
footerRow?: JSX.Element;
|
||||
@ -20,10 +22,15 @@ interface AlertWithCommonLabels extends Alert {
|
||||
commonLabels?: Record<string, string>;
|
||||
}
|
||||
|
||||
type AlertTableColumnProps = DynamicTableColumnProps<AlertWithCommonLabels>;
|
||||
type AlertTableItemProps = DynamicTableItemProps<AlertWithCommonLabels>;
|
||||
interface RuleAndAlert {
|
||||
rule?: CombinedRule;
|
||||
alert: AlertWithCommonLabels;
|
||||
}
|
||||
|
||||
export const AlertInstancesTable = ({ instances, pagination, footerRow }: Props) => {
|
||||
type AlertTableColumnProps = DynamicTableColumnProps<RuleAndAlert>;
|
||||
type AlertTableItemProps = DynamicTableItemProps<RuleAndAlert>;
|
||||
|
||||
export const AlertInstancesTable = ({ rule, instances, pagination, footerRow }: Props) => {
|
||||
const commonLabels = useMemo(() => {
|
||||
// only compute the common labels if we have more than 1 instance, if we don't then that single instance
|
||||
// will have the complete set of common labels and no unique ones
|
||||
@ -33,10 +40,10 @@ export const AlertInstancesTable = ({ instances, pagination, footerRow }: Props)
|
||||
const items = useMemo(
|
||||
(): AlertTableItemProps[] =>
|
||||
instances.map((instance) => ({
|
||||
data: { ...instance, commonLabels },
|
||||
data: { rule, alert: { ...instance, commonLabels } },
|
||||
id: alertInstanceKey(instance),
|
||||
})),
|
||||
[commonLabels, instances]
|
||||
[commonLabels, instances, rule]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -44,7 +51,7 @@ export const AlertInstancesTable = ({ instances, pagination, footerRow }: Props)
|
||||
cols={columns}
|
||||
isExpandable={true}
|
||||
items={items}
|
||||
renderExpandedContent={({ data }) => <AlertInstanceDetails instance={data} />}
|
||||
renderExpandedContent={({ data }) => <AlertInstanceDetails instance={data.alert} />}
|
||||
pagination={pagination}
|
||||
footerRow={footerRow}
|
||||
/>
|
||||
@ -56,24 +63,45 @@ const columns: AlertTableColumnProps[] = [
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: { state } }) => <AlertStateTag state={state} />,
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { state },
|
||||
},
|
||||
}) => <AlertStateTag state={state} />,
|
||||
size: '80px',
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: { labels, commonLabels } }) => (
|
||||
<AlertLabels labels={labels} commonLabels={commonLabels} size="sm" />
|
||||
),
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { labels, commonLabels },
|
||||
},
|
||||
}) => <AlertLabels labels={labels} commonLabels={commonLabels} size="sm" />,
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
label: 'Created',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: { activeAt } }) => (
|
||||
<>{activeAt.startsWith('0001') ? '-' : dateTime(activeAt).format('YYYY-MM-DD HH:mm:ss')}</>
|
||||
),
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { activeAt },
|
||||
},
|
||||
}) => <>{activeAt.startsWith('0001') ? '-' : dateTime(activeAt).format('YYYY-MM-DD HH:mm:ss')}</>,
|
||||
size: '150px',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
label: '',
|
||||
renderCell: ({ data: { alert, rule } }) => (
|
||||
<AlertInstanceExtensionPoint
|
||||
rule={rule}
|
||||
instance={alert}
|
||||
extensionPointId={PluginExtensionPoints.AlertInstanceAction}
|
||||
key="alert-instance-extension-point"
|
||||
/>
|
||||
),
|
||||
size: '40px',
|
||||
},
|
||||
];
|
||||
|
@ -5,7 +5,8 @@ import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
import { getPluginLinkExtensions, setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
@ -20,9 +21,15 @@ import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
|
||||
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/useIsRuleEditable');
|
||||
|
||||
const mocks = {
|
||||
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
|
||||
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
||||
};
|
||||
|
||||
@ -52,6 +59,19 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Run investigation',
|
||||
category: 'Sift',
|
||||
description: 'Run a Sift investigation for this alert',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
|
@ -4,6 +4,9 @@ import { times } from 'lodash';
|
||||
import React from 'react';
|
||||
import { byLabelText, byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
import { getPluginLinkExtensions } from '@grafana/runtime';
|
||||
|
||||
import { CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
||||
import { GrafanaAlertState, PromAlertingRuleState } from '../../../../../types/unified-alerting-dto';
|
||||
import { mockCombinedRule, mockDataSource, mockPromAlert, mockPromAlertingRule } from '../../mocks';
|
||||
@ -11,6 +14,15 @@ import { alertStateToReadable } from '../../utils/rules';
|
||||
|
||||
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mocks = {
|
||||
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
|
||||
};
|
||||
|
||||
const ui = {
|
||||
stateFilter: byTestId('alert-instance-state-filter'),
|
||||
stateButton: byRole('radio'),
|
||||
@ -30,6 +42,22 @@ const ui = {
|
||||
};
|
||||
|
||||
describe('RuleDetailsMatchingInstances', () => {
|
||||
beforeEach(() => {
|
||||
mocks.getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Run investigation',
|
||||
category: 'Sift',
|
||||
description: 'Run a Sift investigation for this alert',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering', () => {
|
||||
it('For Grafana Managed rules instances filter should contain five states', () => {
|
||||
const rule = mockCombinedRule();
|
||||
|
@ -53,12 +53,8 @@ function ShowMoreInstances(props: { onClick: () => void; stats: ShowMoreStats })
|
||||
|
||||
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const {
|
||||
rule: { promRule, namespace, instanceTotals },
|
||||
itemsDisplayLimit = Number.POSITIVE_INFINITY,
|
||||
pagination,
|
||||
enableFiltering = false,
|
||||
} = props;
|
||||
const { rule, itemsDisplayLimit = Number.POSITIVE_INFINITY, pagination, enableFiltering = false } = props;
|
||||
const { promRule, namespace, instanceTotals } = rule;
|
||||
|
||||
const [queryString, setQueryString] = useState<string>();
|
||||
const [alertState, setAlertState] = useState<InstanceStateFilter>();
|
||||
@ -129,7 +125,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
||||
</div>
|
||||
)}
|
||||
{!enableFiltering && <div className={styles.stats}>{statsComponents}</div>}
|
||||
<AlertInstancesTable instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
|
||||
<AlertInstancesTable rule={rule} instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
|
||||
</DetailsField>
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { Button, clearButtonStyles, Icon, useStyles2 } from '@grafana/ui';
|
||||
import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable';
|
||||
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
|
||||
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
||||
import { Alert } from 'app/types/unified-alerting';
|
||||
import { Alert, CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants';
|
||||
|
||||
@ -16,6 +16,7 @@ import { GroupMode, UnifiedAlertListOptions } from './types';
|
||||
import { filterAlerts } from './util';
|
||||
|
||||
interface Props {
|
||||
rule?: CombinedRule;
|
||||
alerts: Alert[];
|
||||
options: PanelProps<UnifiedAlertListOptions>['options'];
|
||||
grafanaTotalInstances?: number;
|
||||
@ -25,6 +26,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AlertInstances = ({
|
||||
rule,
|
||||
alerts,
|
||||
options,
|
||||
grafanaTotalInstances,
|
||||
@ -125,6 +127,7 @@ export const AlertInstances = ({
|
||||
)}
|
||||
{displayInstances && (
|
||||
<AlertInstancesTable
|
||||
rule={rule}
|
||||
instances={filteredAlerts}
|
||||
pagination={{ itemsPerPage: 2 * DEFAULT_PER_PAGE_PAGINATION }}
|
||||
footerRow={footerRow}
|
||||
|
@ -5,8 +5,8 @@ import { Provider } from 'react-redux';
|
||||
import { act } from 'react-test-renderer';
|
||||
import { byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { FieldConfigSource, getDefaultTimeRange, LoadingState, PanelProps } from '@grafana/data';
|
||||
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
|
||||
import { FieldConfigSource, getDefaultTimeRange, LoadingState, PanelProps, PluginExtensionTypes } from '@grafana/data';
|
||||
import { getPluginLinkExtensions, TimeRangeUpdatedEvent } from '@grafana/runtime';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { mockPromRulesApiResponse } from 'app/features/alerting/unified/mocks/alertRuleApi';
|
||||
import { mockRulerRulesApiResponse } from 'app/features/alerting/unified/mocks/rulerApi';
|
||||
@ -56,8 +56,16 @@ const grafanaRuleMock = {
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
}));
|
||||
jest.mock('app/features/alerting/unified/api/alertmanager');
|
||||
|
||||
const mocks = {
|
||||
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
|
||||
};
|
||||
|
||||
const fakeResponse: PromRulesResponse = {
|
||||
data: { groups: grafanaRuleMock.promRules.grafana.result[0].groups as PromRuleGroupDTO[] },
|
||||
status: 'success',
|
||||
@ -78,6 +86,19 @@ beforeEach(() => {
|
||||
mockRulerRulesApiResponse(server, 'grafana', {
|
||||
'folder-one': [{ name: 'group1', interval: '20s', rules: [originRule] }],
|
||||
});
|
||||
mocks.getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Run investigation',
|
||||
category: 'Sift',
|
||||
description: 'Run a Sift investigation for this alert',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const defaultOptions: UnifiedAlertListOptions = {
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Alert } from 'app/types/unified-alerting';
|
||||
|
||||
export enum SortOrder {
|
||||
AlphaAsc = 1,
|
||||
AlphaDesc,
|
||||
@ -65,5 +63,3 @@ export interface UnifiedAlertListOptions {
|
||||
datasource: string;
|
||||
viewMode: ViewMode;
|
||||
}
|
||||
|
||||
export type GroupedRules = Map<string, Alert[]>;
|
||||
|
@ -8,7 +8,7 @@ import { Alert } from 'app/types/unified-alerting';
|
||||
import { CombinedRuleWithLocation } from '../../../../types/unified-alerting';
|
||||
import { AlertInstances } from '../AlertInstances';
|
||||
import { getStyles } from '../UnifiedAlertList';
|
||||
import { GroupedRules, UnifiedAlertListOptions } from '../types';
|
||||
import { UnifiedAlertListOptions } from '../types';
|
||||
import { filterAlerts } from '../util';
|
||||
|
||||
type Props = {
|
||||
@ -16,14 +16,19 @@ type Props = {
|
||||
options: UnifiedAlertListOptions;
|
||||
};
|
||||
|
||||
type RuleWithAlerts = {
|
||||
rule?: CombinedRuleWithLocation;
|
||||
alerts: Alert[];
|
||||
};
|
||||
|
||||
export const UNGROUPED_KEY = '__ungrouped__';
|
||||
|
||||
const GroupedModeView = ({ rules, options }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const groupBy = options.groupBy;
|
||||
|
||||
const groupedRules = useMemo<GroupedRules>(() => {
|
||||
const groupedRules = new Map<string, Alert[]>();
|
||||
const groupedRules = useMemo<Map<string, RuleWithAlerts>>(() => {
|
||||
const groupedRules = new Map<string, RuleWithAlerts>();
|
||||
|
||||
const hasInstancesWithMatchingLabels = (rule: CombinedRuleWithLocation) =>
|
||||
groupBy ? alertHasEveryLabelForCombinedRules(rule, groupBy) : true;
|
||||
@ -35,33 +40,36 @@ const GroupedModeView = ({ rules, options }: Props) => {
|
||||
(alertingRule?.alerts ?? []).forEach((alert) => {
|
||||
const mapKey = hasInstancesMatching ? createMapKey(groupBy, alert.labels) : UNGROUPED_KEY;
|
||||
|
||||
const existingAlerts = groupedRules.get(mapKey) ?? [];
|
||||
groupedRules.set(mapKey, [...existingAlerts, alert]);
|
||||
const existingAlerts = groupedRules.get(mapKey)?.alerts ?? [];
|
||||
groupedRules.set(mapKey, { rule, alerts: [...existingAlerts, alert] });
|
||||
});
|
||||
});
|
||||
|
||||
// move the "UNGROUPED" key to the last item in the Map, items are shown in insertion order
|
||||
const ungrouped = groupedRules.get(UNGROUPED_KEY) ?? [];
|
||||
const ungrouped = groupedRules.get(UNGROUPED_KEY)?.alerts ?? [];
|
||||
groupedRules.delete(UNGROUPED_KEY);
|
||||
groupedRules.set(UNGROUPED_KEY, ungrouped);
|
||||
groupedRules.set(UNGROUPED_KEY, { alerts: ungrouped });
|
||||
|
||||
// Remove groups having no instances
|
||||
// This is different from filtering Rules without instances that we do in UnifiedAlertList
|
||||
const filteredGroupedRules = Array.from(groupedRules.entries()).reduce((acc, [groupKey, groupAlerts]) => {
|
||||
const filteredAlerts = filterAlerts(options, groupAlerts);
|
||||
if (filteredAlerts.length > 0) {
|
||||
acc.set(groupKey, filteredAlerts);
|
||||
}
|
||||
const filteredGroupedRules = Array.from(groupedRules.entries()).reduce(
|
||||
(acc, [groupKey, { rule, alerts: groupAlerts }]) => {
|
||||
const filteredAlerts = filterAlerts(options, groupAlerts);
|
||||
if (filteredAlerts.length > 0) {
|
||||
acc.set(groupKey, { rule, alerts: filteredAlerts });
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map<string, Alert[]>());
|
||||
return acc;
|
||||
},
|
||||
new Map<string, RuleWithAlerts>()
|
||||
);
|
||||
|
||||
return filteredGroupedRules;
|
||||
}, [groupBy, rules, options]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from(groupedRules).map(([key, alerts]) => (
|
||||
{Array.from(groupedRules).map(([key, { rule, alerts }]) => (
|
||||
<li className={styles.alertRuleItem} key={key} data-testid={key}>
|
||||
<div>
|
||||
<div className={styles.customGroupDetails}>
|
||||
@ -71,7 +79,7 @@ const GroupedModeView = ({ rules, options }: Props) => {
|
||||
{key === UNGROUPED_KEY && 'No grouping'}
|
||||
</div>
|
||||
</div>
|
||||
<AlertInstances alerts={alerts} options={options} />
|
||||
<AlertInstances rule={rule} alerts={alerts} options={options} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
@ -119,6 +119,7 @@ const UngroupedModeView = ({ rules, options, handleInstancesLimit, limitInstance
|
||||
</div>
|
||||
</div>
|
||||
<AlertInstances
|
||||
rule={ruleWithLocation}
|
||||
alerts={alertingRule.alerts ?? []}
|
||||
options={options}
|
||||
grafanaTotalInstances={grafanaInstancesTotal}
|
||||
|
Loading…
Reference in New Issue
Block a user