mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Central alert history (part1) (#88593)
* WIP * Add barchart panel with scenes * Fix timerange in barchart panel * Refactor: component names * Remove not used css styles and rename panel title * Remove unnecessary HistoryEventsListObject class and update text in labels filter * add padding top for filter * Add translations * update limit labels constant * Update showing state reason * Fix scene object * Address review comments * Update icons * use endpoints instead of the autogenerated hook * Address review comments * Add tooltip for alert name * use private polling interval * fix autogenerated translations * Address pr rewview comments * Address review comments * Update text in placeholder * Rename variable and remove spaces in Trans children
This commit is contained in:
parent
32d21356b9
commit
e75fbe10ca
@ -1646,8 +1646,7 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/AlertLabels.tsx:5381": [
|
"public/app/features/alerting/unified/components/AlertLabels.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [
|
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
@ -408,6 +408,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"})
|
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: "History",
|
||||||
|
SubTitle: "History of events that were generated by your Grafana-managed alert rules. Silences and Mute timings are ignored.",
|
||||||
|
Id: "alerts-history",
|
||||||
|
Url: s.cfg.AppSubURL + "/alerting/history",
|
||||||
|
Icon: "history",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if c.SignedInUser.GetOrgRole() == org.RoleAdmin {
|
if c.SignedInUser.GetOrgRole() == org.RoleAdmin {
|
||||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||||
Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin",
|
Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin",
|
||||||
|
@ -166,6 +166,19 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
|||||||
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
|
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/alerting/history/',
|
||||||
|
roles: evaluateAccess([
|
||||||
|
AccessControlAction.AlertingInstanceRead,
|
||||||
|
AccessControlAction.AlertingInstancesExternalRead,
|
||||||
|
]),
|
||||||
|
component: importAlertingComponent(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "HistoryPage" */ 'app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/new/:type?',
|
path: '/alerting/new/:type?',
|
||||||
pageClass: 'page-alerting',
|
pageClass: 'page-alerting',
|
||||||
|
@ -4,7 +4,7 @@ import { alertingApi } from './alertingApi';
|
|||||||
|
|
||||||
export const stateHistoryApi = alertingApi.injectEndpoints({
|
export const stateHistoryApi = alertingApi.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
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 }) => ({
|
query: ({ ruleUid, from, to, limit = 100 }) => ({
|
||||||
url: '/api/v1/rules/history',
|
url: '/api/v1/rules/history',
|
||||||
params: { ruleUID: ruleUid, from, to, limit },
|
params: { ruleUID: ruleUid, from, to, limit },
|
||||||
|
@ -5,6 +5,7 @@ import React, { useState } from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, getTagColorsFromName, useStyles2 } from '@grafana/ui';
|
import { Button, getTagColorsFromName, useStyles2 } from '@grafana/ui';
|
||||||
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { isPrivateLabel } from '../utils/labels';
|
import { isPrivateLabel } from '../utils/labels';
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
|
|||||||
|
|
||||||
const commonLabelsCount = Object.keys(commonLabels).length;
|
const commonLabelsCount = Object.keys(commonLabels).length;
|
||||||
const hasCommonLabels = commonLabelsCount > 0;
|
const hasCommonLabels = commonLabelsCount > 0;
|
||||||
|
const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} role="list" aria-label="Labels">
|
<div className={styles.wrapper} role="list" aria-label="Labels">
|
||||||
@ -39,7 +41,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
fill="text"
|
fill="text"
|
||||||
onClick={() => setShowCommonLabels(true)}
|
onClick={() => setShowCommonLabels(true)}
|
||||||
tooltip="Show common labels"
|
tooltip={tooltip}
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
@ -54,7 +56,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
|
|||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Hide common labels
|
<Trans i18nKey="alert-labels.button.hide">Hide common labels</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,383 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||||
|
import { isFetchError } from '@grafana/runtime';
|
||||||
|
import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
LoadingBar,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
useStyles2,
|
||||||
|
withErrorBoundary,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||||
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
|
import {
|
||||||
|
GrafanaAlertStateWithReason,
|
||||||
|
isAlertStateWithReason,
|
||||||
|
isGrafanaAlertState,
|
||||||
|
mapStateWithReasonToBaseState,
|
||||||
|
mapStateWithReasonToReason,
|
||||||
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||||
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||||
|
import { stringifyErrorLike } from '../../../utils/misc';
|
||||||
|
import { hashLabelsOrAnnotations } from '../../../utils/rule-id';
|
||||||
|
import { AlertLabels } from '../../AlertLabels';
|
||||||
|
import { CollapseToggle } from '../../CollapseToggle';
|
||||||
|
import { LogRecord } from '../state-history/common';
|
||||||
|
import { useRuleHistoryRecords } from '../state-history/useRuleHistoryRecords';
|
||||||
|
|
||||||
|
const LIMIT_EVENTS = 250;
|
||||||
|
|
||||||
|
const HistoryEventsList = ({ timeRange }: { timeRange?: TimeRange }) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
const [eventsFilter, setEventsFilter] = useState('');
|
||||||
|
// form for filter fields
|
||||||
|
const { register, handleSubmit, reset } = useForm({ defaultValues: { query: '' } }); // form for search field
|
||||||
|
const from = timeRange?.from.unix();
|
||||||
|
const to = timeRange?.to.unix();
|
||||||
|
const onFilterCleared = useCallback(() => {
|
||||||
|
setEventsFilter('');
|
||||||
|
reset();
|
||||||
|
}, [setEventsFilter, reset]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: stateHistory,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = stateHistoryApi.endpoints.getRuleHistory.useQuery(
|
||||||
|
{
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
limit: LIMIT_EVENTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnFocus: true,
|
||||||
|
refetchOnReconnect: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { historyRecords } = useRuleHistoryRecords(stateHistory, eventsFilter);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <HistoryErrorMessage error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column" gap={1}>
|
||||||
|
<div className={styles.labelsFilter}>
|
||||||
|
<form onSubmit={handleSubmit((data) => setEventsFilter(data.query))}>
|
||||||
|
<SearchFieldInput
|
||||||
|
{...register('query')}
|
||||||
|
showClearFilterSuffix={!!eventsFilter}
|
||||||
|
onClearFilterClick={onFilterCleared}
|
||||||
|
/>
|
||||||
|
<input type="submit" hidden />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<LoadingIndicator visible={isLoading} />
|
||||||
|
<HistoryLogEvents logRecords={historyRecords} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo: this function has been copied from RuleList.v2.tsx, should be moved to a shared location
|
||||||
|
const LoadingIndicator = ({ visible = false }) => {
|
||||||
|
const [measureRef, { width }] = useMeasure<HTMLDivElement>();
|
||||||
|
return <div ref={measureRef}>{visible && <LoadingBar width={width} />}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HistoryLogEventsProps {
|
||||||
|
logRecords: LogRecord[];
|
||||||
|
}
|
||||||
|
function HistoryLogEvents({ logRecords }: HistoryLogEventsProps) {
|
||||||
|
// display log records
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{logRecords.map((record) => {
|
||||||
|
return <EventRow key={record.timestamp + hashLabelsOrAnnotations(record.line.labels ?? {})} record={record} />;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryErrorMessageProps {
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryErrorMessage({ error }: HistoryErrorMessageProps) {
|
||||||
|
if (isFetchError(error) && error.status === 404) {
|
||||||
|
return <EntityNotFound entity="History" />;
|
||||||
|
}
|
||||||
|
const title = t('central-alert-history.error', 'Something went wrong loading the alert state history');
|
||||||
|
|
||||||
|
return <Alert title={title}>{stringifyErrorLike(error)}</Alert>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchFieldInputProps {
|
||||||
|
showClearFilterSuffix: boolean;
|
||||||
|
onClearFilterClick: () => void;
|
||||||
|
}
|
||||||
|
const SearchFieldInput = React.forwardRef<HTMLInputElement, SearchFieldInputProps>(
|
||||||
|
({ showClearFilterSuffix, onClearFilterClick, ...rest }: SearchFieldInputProps, ref) => {
|
||||||
|
const placeholder = t('central-alert-history.filter.placeholder', 'Filter events in the list with labels');
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
label={
|
||||||
|
<Label htmlFor="eventsSearchInput">
|
||||||
|
<Stack gap={0.5}>
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey="central-alert-history.filter.label">Filter events</Trans>
|
||||||
|
</span>
|
||||||
|
</Stack>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="eventsSearchInput"
|
||||||
|
prefix={<Icon name="search" />}
|
||||||
|
suffix={
|
||||||
|
showClearFilterSuffix && (
|
||||||
|
<Button fill="text" icon="times" size="sm" onClick={onClearFilterClick}>
|
||||||
|
<Trans i18nKey="central-alert-history.filter.button.clear">Clear</Trans>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={placeholder}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SearchFieldInput.displayName = 'SearchFieldInput';
|
||||||
|
|
||||||
|
function EventRow({ record }: { record: LogRecord }) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.header} data-testid="rule-group-header">
|
||||||
|
<CollapseToggle
|
||||||
|
size="sm"
|
||||||
|
className={styles.collapseToggle}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
onToggle={setIsCollapsed}
|
||||||
|
/>
|
||||||
|
<Stack gap={0.5} direction={'row'} alignItems={'center'}>
|
||||||
|
<div className={styles.timeCol}>
|
||||||
|
<Timestamp time={record.timestamp} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.transitionCol}>
|
||||||
|
<EventTransition previous={record.line.previous} current={record.line.current} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.alertNameCol}>
|
||||||
|
{record.line.labels ? <AlertRuleName labels={record.line.labels} ruleUID={record.line.ruleUID} /> : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.labelsCol}>
|
||||||
|
<AlertLabels labels={record.line.labels ?? {}} size="xs" />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertRuleName({ labels, ruleUID }: { labels: Record<string, string>; ruleUID?: string }) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const alertRuleName = labels['alertname'];
|
||||||
|
if (!ruleUID) {
|
||||||
|
return <Text>{alertRuleName}</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip content={alertRuleName ?? ''}>
|
||||||
|
<a
|
||||||
|
href={`/alerting/${GRAFANA_RULES_SOURCE_NAME}/${ruleUID}/view?returnTo=${encodeURIComponent('/alerting/history')}`}
|
||||||
|
className={styles.alertName}
|
||||||
|
>
|
||||||
|
{alertRuleName}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventTransitionProps {
|
||||||
|
previous: GrafanaAlertStateWithReason;
|
||||||
|
current: GrafanaAlertStateWithReason;
|
||||||
|
}
|
||||||
|
function EventTransition({ previous, current }: EventTransitionProps) {
|
||||||
|
return (
|
||||||
|
<Stack gap={0.5} direction={'row'}>
|
||||||
|
<EventState state={previous} />
|
||||||
|
<Icon name="arrow-right" size="lg" />
|
||||||
|
<EventState state={current} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventState({ state }: { state: GrafanaAlertStateWithReason }) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={'No recognized state'}>
|
||||||
|
<Icon name="exclamation-triangle" size="md" />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const baseState = mapStateWithReasonToBaseState(state);
|
||||||
|
const reason = mapStateWithReasonToReason(state);
|
||||||
|
|
||||||
|
switch (baseState) {
|
||||||
|
case 'Normal':
|
||||||
|
return (
|
||||||
|
<Tooltip content={Boolean(reason) ? `Normal (${reason})` : 'Normal'}>
|
||||||
|
<Icon name="check-circle" size="md" className={Boolean(reason) ? styles.warningColor : styles.normalColor} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
case 'Alerting':
|
||||||
|
return (
|
||||||
|
<Tooltip content={'Alerting'}>
|
||||||
|
<Icon name="exclamation-circle" size="md" className={styles.alertingColor} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
case 'NoData': //todo:change icon
|
||||||
|
return (
|
||||||
|
<Tooltip content={'Insufficient data'}>
|
||||||
|
<Icon name="exclamation-triangle" size="md" className={styles.warningColor} />
|
||||||
|
{/* no idea which icon to use */}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
case 'Error':
|
||||||
|
return (
|
||||||
|
<Tooltip content={'Error'}>
|
||||||
|
<Icon name="exclamation-circle" size="md" />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'Pending':
|
||||||
|
return (
|
||||||
|
<Tooltip content={Boolean(reason) ? `Pending (${reason})` : 'Pending'}>
|
||||||
|
<Icon name="circle" size="md" className={styles.warningColor} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <Icon name="exclamation-triangle" size="md" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimestampProps {
|
||||||
|
time: number; // epoch timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
const Timestamp = ({ time }: TimestampProps) => {
|
||||||
|
const dateTime = new Date(time);
|
||||||
|
const formattedDate = dateTime.toLocaleString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text variant="body" weight="light">
|
||||||
|
{formattedDate}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withErrorBoundary(HistoryEventsList, { style: 'page' });
|
||||||
|
|
||||||
|
export const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
header: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.components.table.rowHoverBackground,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
collapseToggle: css({
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
marginTop: `-${theme.spacing(1)}`,
|
||||||
|
marginBottom: `-${theme.spacing(1)}`,
|
||||||
|
|
||||||
|
svg: {
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
normalColor: css({
|
||||||
|
fill: theme.colors.success.text,
|
||||||
|
}),
|
||||||
|
warningColor: css({
|
||||||
|
fill: theme.colors.warning.text,
|
||||||
|
}),
|
||||||
|
alertingColor: css({
|
||||||
|
fill: theme.colors.error.text,
|
||||||
|
}),
|
||||||
|
timeCol: css({
|
||||||
|
width: '150px',
|
||||||
|
}),
|
||||||
|
transitionCol: css({
|
||||||
|
width: '80px',
|
||||||
|
}),
|
||||||
|
alertNameCol: css({
|
||||||
|
width: '300px',
|
||||||
|
}),
|
||||||
|
labelsCol: css({
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingRight: theme.spacing(2),
|
||||||
|
flex: 1,
|
||||||
|
}),
|
||||||
|
alertName: css({
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
display: 'block',
|
||||||
|
color: theme.colors.text.link,
|
||||||
|
}),
|
||||||
|
labelsFilter: css({
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: theme.spacing(4),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class HistoryEventsListObject extends SceneObjectBase {
|
||||||
|
public static Component = HistoryEventsListObjectRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryEventsListObjectRenderer({ model }: SceneComponentProps<HistoryEventsListObject>) {
|
||||||
|
const { value: timeRange } = sceneGraph.getTimeRange(model).useState(); // get time range from scene graph
|
||||||
|
|
||||||
|
return <HistoryEventsList timeRange={timeRange} />;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { withErrorBoundary } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { AlertingPageWrapper } from '../../AlertingPageWrapper';
|
||||||
|
|
||||||
|
import { CentralAlertHistoryScene } from './CentralAlertHistoryScene';
|
||||||
|
|
||||||
|
const HistoryPage = () => {
|
||||||
|
return (
|
||||||
|
<AlertingPageWrapper navId="alerts-history" isLoading={false}>
|
||||||
|
<CentralAlertHistoryScene />
|
||||||
|
</AlertingPageWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default withErrorBoundary(HistoryPage, { style: 'page' });
|
@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
EmbeddedScene,
|
||||||
|
PanelBuilders,
|
||||||
|
SceneControlsSpacer,
|
||||||
|
SceneFlexItem,
|
||||||
|
SceneFlexLayout,
|
||||||
|
SceneQueryRunner,
|
||||||
|
SceneReactObject,
|
||||||
|
SceneRefreshPicker,
|
||||||
|
SceneTimePicker,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import {
|
||||||
|
GraphDrawStyle,
|
||||||
|
GraphGradientMode,
|
||||||
|
LegendDisplayMode,
|
||||||
|
LineInterpolation,
|
||||||
|
ScaleDistribution,
|
||||||
|
StackingMode,
|
||||||
|
TooltipDisplayMode,
|
||||||
|
VisibilityMode,
|
||||||
|
} from '@grafana/schema/dist/esm/index';
|
||||||
|
|
||||||
|
import { DataSourceInformation, PANEL_STYLES } from '../../../home/Insights';
|
||||||
|
import { SectionSubheader } from '../../../insights/SectionSubheader';
|
||||||
|
|
||||||
|
import { HistoryEventsListObjectRenderer } from './CentralAlertHistory';
|
||||||
|
|
||||||
|
export const CentralAlertHistoryScene = () => {
|
||||||
|
const dataSourceSrv = getDataSourceSrv();
|
||||||
|
const alertStateHistoryDatasource: DataSourceInformation = {
|
||||||
|
type: 'loki',
|
||||||
|
uid: 'grafanacloud-alert-state-history',
|
||||||
|
settings: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
alertStateHistoryDatasource.settings = dataSourceSrv.getInstanceSettings(alertStateHistoryDatasource.uid);
|
||||||
|
|
||||||
|
const scene = new EmbeddedScene({
|
||||||
|
controls: [new SceneControlsSpacer(), new SceneTimePicker({}), new SceneRefreshPicker({})],
|
||||||
|
body: new SceneFlexLayout({
|
||||||
|
direction: 'column',
|
||||||
|
children: [
|
||||||
|
new SceneFlexItem({
|
||||||
|
ySizing: 'content',
|
||||||
|
body: getEventsSceneObject(alertStateHistoryDatasource),
|
||||||
|
}),
|
||||||
|
new SceneFlexItem({
|
||||||
|
body: new SceneReactObject({
|
||||||
|
component: HistoryEventsListObjectRenderer,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return <scene.Component model={scene} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEventsSceneObject(ashDs: DataSourceInformation) {
|
||||||
|
return new EmbeddedScene({
|
||||||
|
controls: [
|
||||||
|
new SceneReactObject({
|
||||||
|
component: SectionSubheader,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
body: new SceneFlexLayout({
|
||||||
|
direction: 'column',
|
||||||
|
children: [
|
||||||
|
new SceneFlexItem({
|
||||||
|
ySizing: 'content',
|
||||||
|
body: new SceneFlexLayout({
|
||||||
|
children: [getEventsScenesFlexItem(ashDs)],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSceneQuery(datasource: DataSourceInformation) {
|
||||||
|
const query = new SceneQueryRunner({
|
||||||
|
datasource,
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
expr: 'count_over_time({from="state-history"} |= `` [$__auto])',
|
||||||
|
queryType: 'range',
|
||||||
|
step: '10s',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
|
||||||
|
return new SceneFlexItem({
|
||||||
|
...PANEL_STYLES,
|
||||||
|
body: PanelBuilders.timeseries()
|
||||||
|
.setTitle('Events')
|
||||||
|
.setDescription('Alert events during the period of time.')
|
||||||
|
.setData(getSceneQuery(datasource))
|
||||||
|
.setColor({ mode: 'continuous-BlPu' })
|
||||||
|
.setCustomFieldConfig('fillOpacity', 100)
|
||||||
|
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
|
||||||
|
.setCustomFieldConfig('lineInterpolation', LineInterpolation.Linear)
|
||||||
|
.setCustomFieldConfig('lineWidth', 1)
|
||||||
|
.setCustomFieldConfig('barAlignment', 0)
|
||||||
|
.setCustomFieldConfig('spanNulls', false)
|
||||||
|
.setCustomFieldConfig('insertNulls', false)
|
||||||
|
.setCustomFieldConfig('showPoints', VisibilityMode.Auto)
|
||||||
|
.setCustomFieldConfig('pointSize', 5)
|
||||||
|
.setCustomFieldConfig('stacking', { mode: StackingMode.None, group: 'A' })
|
||||||
|
.setCustomFieldConfig('gradientMode', GraphGradientMode.Hue)
|
||||||
|
.setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear })
|
||||||
|
.setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden })
|
||||||
|
.setOption('tooltip', { mode: TooltipDisplayMode.Single })
|
||||||
|
|
||||||
|
.setNoValue('No events found')
|
||||||
|
.build(),
|
||||||
|
});
|
||||||
|
}
|
@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';
|
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 { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||||
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
||||||
|
@ -7,6 +7,7 @@ export interface Line {
|
|||||||
current: GrafanaAlertStateWithReason;
|
current: GrafanaAlertStateWithReason;
|
||||||
values?: Record<string, number>;
|
values?: Record<string, number>;
|
||||||
labels?: Record<string, string>;
|
labels?: Record<string, string>;
|
||||||
|
ruleUID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogRecord {
|
export interface LogRecord {
|
||||||
|
@ -240,7 +240,7 @@ export function hashRule(rule: Rule): string {
|
|||||||
throw new Error('only recording and alerting rules can be hashed');
|
throw new Error('only recording and alerting rules can be hashed');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
|
export function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
|
||||||
return JSON.stringify(Object.entries(item || {}).sort((a, b) => a[0].localeCompare(b[0])));
|
return JSON.stringify(Object.entries(item || {}).sort((a, b) => a[0].localeCompare(b[0])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +149,13 @@ export const navIndex: NavIndex = {
|
|||||||
icon: 'layer-group',
|
icon: 'layer-group',
|
||||||
url: '/alerting/groups',
|
url: '/alerting/groups',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'history',
|
||||||
|
text: 'History',
|
||||||
|
subTitle: 'Alert state history',
|
||||||
|
icon: 'history',
|
||||||
|
url: '/alerting/history',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'alerting-admin',
|
id: 'alerting-admin',
|
||||||
text: 'Settings',
|
text: 'Settings',
|
||||||
|
@ -42,6 +42,11 @@ export function isAlertStateWithReason(
|
|||||||
return state !== null && state !== undefined && !propAlertingRuleStateValues.includes(state);
|
return state !== null && state !== undefined && !propAlertingRuleStateValues.includes(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapStateWithReasonToReason(state: GrafanaAlertStateWithReason): string {
|
||||||
|
const match = state.match(/\((.*?)\)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function mapStateWithReasonToBaseState(
|
export function mapStateWithReasonToBaseState(
|
||||||
state: GrafanaAlertStateWithReason | PromAlertingRuleState
|
state: GrafanaAlertStateWithReason | PromAlertingRuleState
|
||||||
): GrafanaAlertState | PromAlertingRuleState {
|
): GrafanaAlertState | PromAlertingRuleState {
|
||||||
|
@ -25,6 +25,14 @@
|
|||||||
"user": "User"
|
"user": "User"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"alert-labels": {
|
||||||
|
"button": {
|
||||||
|
"hide": "Hide common labels",
|
||||||
|
"show": {
|
||||||
|
"tooltip": "Show common labels"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"alert-rule-form": {
|
"alert-rule-form": {
|
||||||
"evaluation-behaviour": {
|
"evaluation-behaviour": {
|
||||||
"description": {
|
"description": {
|
||||||
@ -84,15 +92,15 @@
|
|||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"alertRule_one": "{{count}} alert rule",
|
"alertRule_one": "{{count}} alert rule",
|
||||||
"alertRule_other": "{{count}} alert rules",
|
"alertRule_other": "{{count}} alert rule",
|
||||||
"dashboard_one": "{{count}} dashboard",
|
"dashboard_one": "{{count}} dashboard",
|
||||||
"dashboard_other": "{{count}} dashboards",
|
"dashboard_other": "{{count}} dashboard",
|
||||||
"folder_one": "{{count}} folder",
|
"folder_one": "{{count}} folder",
|
||||||
"folder_other": "{{count}} folders",
|
"folder_other": "{{count}} folder",
|
||||||
"libraryPanel_one": "{{count}} library panel",
|
"libraryPanel_one": "{{count}} library panel",
|
||||||
"libraryPanel_other": "{{count}} library panels",
|
"libraryPanel_other": "{{count}} library panel",
|
||||||
"total_one": "{{count}} item",
|
"total_one": "{{count}} item",
|
||||||
"total_other": "{{count}} items"
|
"total_other": "{{count}} item"
|
||||||
},
|
},
|
||||||
"dashboards-tree": {
|
"dashboards-tree": {
|
||||||
"collapse-folder-button": "Collapse folder {{title}}",
|
"collapse-folder-button": "Collapse folder {{title}}",
|
||||||
@ -138,6 +146,16 @@
|
|||||||
"text": "No results found for your query"
|
"text": "No results found for your query"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"central-alert-history": {
|
||||||
|
"error": "Something went wrong loading the alert state history",
|
||||||
|
"filter": {
|
||||||
|
"button": {
|
||||||
|
"clear": "Clear"
|
||||||
|
},
|
||||||
|
"label": "Filter events",
|
||||||
|
"placeholder": "Filter events in the list with labels"
|
||||||
|
}
|
||||||
|
},
|
||||||
"clipboard-button": {
|
"clipboard-button": {
|
||||||
"inline-toast": {
|
"inline-toast": {
|
||||||
"success": "Copied"
|
"success": "Copied"
|
||||||
@ -758,7 +776,7 @@
|
|||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"body_one": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
|
"body_one": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
|
||||||
"body_other": "This panel is being used in {{count}} dashboards. Please choose which dashboard to view the panel in:",
|
"body_other": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
|
||||||
"button-cancel": "Cancel",
|
"button-cancel": "Cancel",
|
||||||
"button-view-panel1": "View panel in {{label}}...",
|
"button-view-panel1": "View panel in {{label}}...",
|
||||||
"button-view-panel2": "View panel in dashboard...",
|
"button-view-panel2": "View panel in dashboard...",
|
||||||
|
@ -25,6 +25,14 @@
|
|||||||
"user": "Ůşęř"
|
"user": "Ůşęř"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"alert-labels": {
|
||||||
|
"button": {
|
||||||
|
"hide": "Ħįđę čőmmőʼn ľäþęľş",
|
||||||
|
"show": {
|
||||||
|
"tooltip": "Ŝĥőŵ čőmmőʼn ľäþęľş"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"alert-rule-form": {
|
"alert-rule-form": {
|
||||||
"evaluation-behaviour": {
|
"evaluation-behaviour": {
|
||||||
"description": {
|
"description": {
|
||||||
@ -84,15 +92,15 @@
|
|||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"alertRule_one": "{{count}} äľęřŧ řūľę",
|
"alertRule_one": "{{count}} äľęřŧ řūľę",
|
||||||
"alertRule_other": "{{count}} äľęřŧ řūľęş",
|
"alertRule_other": "{{count}} äľęřŧ řūľę",
|
||||||
"dashboard_one": "{{count}} đäşĥþőäřđ",
|
"dashboard_one": "{{count}} đäşĥþőäřđ",
|
||||||
"dashboard_other": "{{count}} đäşĥþőäřđş",
|
"dashboard_other": "{{count}} đäşĥþőäřđ",
|
||||||
"folder_one": "{{count}} ƒőľđęř",
|
"folder_one": "{{count}} ƒőľđęř",
|
||||||
"folder_other": "{{count}} ƒőľđęřş",
|
"folder_other": "{{count}} ƒőľđęř",
|
||||||
"libraryPanel_one": "{{count}} ľįþřäřy päʼnęľ",
|
"libraryPanel_one": "{{count}} ľįþřäřy päʼnęľ",
|
||||||
"libraryPanel_other": "{{count}} ľįþřäřy päʼnęľş",
|
"libraryPanel_other": "{{count}} ľįþřäřy päʼnęľ",
|
||||||
"total_one": "{{count}} įŧęm",
|
"total_one": "{{count}} įŧęm",
|
||||||
"total_other": "{{count}} įŧęmş"
|
"total_other": "{{count}} įŧęm"
|
||||||
},
|
},
|
||||||
"dashboards-tree": {
|
"dashboards-tree": {
|
||||||
"collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}",
|
"collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}",
|
||||||
@ -138,6 +146,16 @@
|
|||||||
"text": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy"
|
"text": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"central-alert-history": {
|
||||||
|
"error": "Ŝőmęŧĥįʼnģ ŵęʼnŧ ŵřőʼnģ ľőäđįʼnģ ŧĥę äľęřŧ şŧäŧę ĥįşŧőřy",
|
||||||
|
"filter": {
|
||||||
|
"button": {
|
||||||
|
"clear": "Cľęäř"
|
||||||
|
},
|
||||||
|
"label": "Fįľŧęř ęvęʼnŧş",
|
||||||
|
"placeholder": "Fįľŧęř ęvęʼnŧş įʼn ŧĥę ľįşŧ ŵįŧĥ ľäþęľş"
|
||||||
|
}
|
||||||
|
},
|
||||||
"clipboard-button": {
|
"clipboard-button": {
|
||||||
"inline-toast": {
|
"inline-toast": {
|
||||||
"success": "Cőpįęđ"
|
"success": "Cőpįęđ"
|
||||||
@ -758,7 +776,7 @@
|
|||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"body_one": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
|
"body_one": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
|
||||||
"body_other": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđş. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
|
"body_other": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
|
||||||
"button-cancel": "Cäʼnčęľ",
|
"button-cancel": "Cäʼnčęľ",
|
||||||
"button-view-panel1": "Vįęŵ päʼnęľ įʼn {{label}}...",
|
"button-view-panel1": "Vįęŵ päʼnęľ įʼn {{label}}...",
|
||||||
"button-view-panel2": "Vįęŵ päʼnęľ įʼn đäşĥþőäřđ...",
|
"button-view-panel2": "Vįęŵ päʼnęľ įʼn đäşĥþőäřđ...",
|
||||||
|
Loading…
Reference in New Issue
Block a user