mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
WIP
This commit is contained in:
parent
c16244234e
commit
80b8879d4e
@ -401,6 +401,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts from an Alertmanager instance", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert history",
|
||||
SubTitle: "History of events that were generated by by your Grafana-managed alert rules. Silences and Mute timmings are ignored.",
|
||||
Id: "alerts-history",
|
||||
Url: s.cfg.AppSubURL + "/alerting/history",
|
||||
Icon: "history",
|
||||
})
|
||||
}
|
||||
|
||||
if c.SignedInUser.GetOrgRole() == org.RoleAdmin {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin",
|
||||
|
@ -169,6 +169,16 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/history/',
|
||||
roles: evaluateAccess([
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstancesExternalRead,
|
||||
]),
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "CentralAlertHistory" */ 'app/features/alerting/unified/CentralAlertHistory')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/new/:type?',
|
||||
pageClass: 'page-alerting',
|
||||
|
151
public/app/features/alerting/unified/CentralAlertHistory.tsx
Normal file
151
public/app/features/alerting/unified/CentralAlertHistory.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { NavModelItem, getDefaultTimeRange } from '@grafana/data';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import { Alert, Button, Field, Icon, Input, Label, Stack, withErrorBoundary } from '@grafana/ui';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
|
||||
import { stateHistoryApi } from './api/stateHistoryApi';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { STATE_HISTORY_POLLING_INTERVAL } from './components/rules/state-history/LokiStateHistory';
|
||||
import { LogRecord } from './components/rules/state-history/common';
|
||||
import { useRuleHistoryRecords } from './components/rules/state-history/useRuleHistoryRecords';
|
||||
import { stringifyErrorLike } from './utils/misc';
|
||||
|
||||
const CentralAlertHistory = (): JSX.Element => {
|
||||
const { useGetRuleHistoryQuery } = stateHistoryApi;
|
||||
// Filter state
|
||||
const [eventsFilter, setEventsFilter] = useState('');
|
||||
const { getValues, setValue, register, handleSubmit } = useForm({ defaultValues: { query: '' } }); // form for search field
|
||||
|
||||
const onFilterCleared = useCallback(() => {
|
||||
setEventsFilter('');
|
||||
setValue('query', '');
|
||||
}, [setEventsFilter, setValue]);
|
||||
|
||||
// We prefer log count-based limit rather than time-based, but the API doesn't support it yet
|
||||
const queryTimeRange = useMemo(() => getDefaultTimeRange(), []);
|
||||
|
||||
const {
|
||||
currentData: stateHistory,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useGetRuleHistoryQuery(
|
||||
{
|
||||
from: queryTimeRange.from.unix(),
|
||||
to: queryTimeRange.to.unix(),
|
||||
limit: 250,
|
||||
},
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
pollingInterval: STATE_HISTORY_POLLING_INTERVAL,
|
||||
}
|
||||
);
|
||||
const { historyRecords, findCommonLabels } = useRuleHistoryRecords(
|
||||
stateHistory
|
||||
// instancesFilter
|
||||
);
|
||||
const defaultPageNav: NavModelItem = {
|
||||
id: 'alerts-history',
|
||||
text: '',
|
||||
};
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<AlertingPageWrapper pageNav={defaultPageNav} navId="alerts-history">
|
||||
<HistoryErrorMessage error={error} />
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<AlertingPageWrapper pageNav={defaultPageNav} navId="alerts-history" isLoading={true}>
|
||||
<></>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper navId="alerts-history" isLoading={false}>
|
||||
<Stack direction="column" gap={1}>
|
||||
<form onSubmit={handleSubmit((data) => setEventsFilter(data.query))}>
|
||||
<SearchFieldInput
|
||||
{...register('query')}
|
||||
showClearFilterSuffix={!!eventsFilter}
|
||||
onClearFilterClick={onFilterCleared}
|
||||
/>
|
||||
<input type="submit" hidden />
|
||||
</form>
|
||||
<HistoryLogEntry logRecords={historyRecords} />
|
||||
</Stack>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
interface HistoryLogEntryProps {
|
||||
logRecords: LogRecord[];
|
||||
}
|
||||
function HistoryLogEntry({ logRecords }: HistoryLogEntryProps) {
|
||||
// display log records
|
||||
return (
|
||||
<ul>
|
||||
{logRecords.map((record, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{record.timestamp} - {JSON.stringify(record.line.previous)} {`->`} {JSON.stringify(record.line.current)}-{' '}
|
||||
{JSON.stringify(record.line.labels)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryErrorMessageProps {
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
function HistoryErrorMessage({ error }: HistoryErrorMessageProps) {
|
||||
if (isFetchError(error) && error.status === 404) {
|
||||
return <EntityNotFound entity="History" />;
|
||||
}
|
||||
|
||||
return <Alert title={'Something went wrong loading the alert state history'}>{stringifyErrorLike(error)}</Alert>;
|
||||
}
|
||||
|
||||
interface SearchFieldInputProps {
|
||||
showClearFilterSuffix: boolean;
|
||||
onClearFilterClick: () => void;
|
||||
}
|
||||
const SearchFieldInput = ({ showClearFilterSuffix, onClearFilterClick }: SearchFieldInputProps) => {
|
||||
return (
|
||||
<Field
|
||||
label={
|
||||
<Label htmlFor="instancesSearchInput">
|
||||
<Stack gap={0.5}>
|
||||
<span>Filter instances</span>
|
||||
</Stack>
|
||||
</Label>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id="eventsSearchInput"
|
||||
prefix={<Icon name="search" />}
|
||||
suffix={
|
||||
showClearFilterSuffix && (
|
||||
<Button fill="text" icon="times" size="sm" onClick={onClearFilterClick}>
|
||||
Clear
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
placeholder="Filter events"
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default withErrorBoundary(CentralAlertHistory, { style: 'page' });
|
@ -4,7 +4,7 @@ import { alertingApi } from './alertingApi';
|
||||
|
||||
export const stateHistoryApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getRuleHistory: build.query<DataFrameJSON, { ruleUid: string; from?: number; to?: number; limit?: number }>({
|
||||
getRuleHistory: build.query<DataFrameJSON, { ruleUid?: string; from?: number; to?: number; limit?: number }>({
|
||||
query: ({ ruleUid, from, to, limit = 100 }) => ({
|
||||
url: '/api/v1/rules/history',
|
||||
params: { ruleUID: ruleUid, from, to, limit },
|
||||
|
@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||
import { Alert, Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
||||
import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
||||
@ -19,7 +19,7 @@ interface Props {
|
||||
ruleUID: string;
|
||||
}
|
||||
|
||||
const STATE_HISTORY_POLLING_INTERVAL = 10 * 1000; // 10 seconds
|
||||
export const STATE_HISTORY_POLLING_INTERVAL = 10 * 1000; // 10 seconds
|
||||
const MAX_TIMELINE_SERIES = 12;
|
||||
|
||||
const LokiStateHistory = ({ ruleUID }: Props) => {
|
||||
|
@ -149,6 +149,13 @@ export const navIndex: NavIndex = {
|
||||
icon: 'layer-group',
|
||||
url: '/alerting/groups',
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
text: 'History',
|
||||
subTitle: 'Alert state history',
|
||||
icon: 'history',
|
||||
url: '/alerting/history',
|
||||
},
|
||||
{
|
||||
id: 'alerting-admin',
|
||||
text: 'Settings',
|
||||
|
Loading…
Reference in New Issue
Block a user