mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -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": [
|
||||
[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 />", "2"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [
|
||||
[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"})
|
||||
}
|
||||
|
||||
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 {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
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')
|
||||
),
|
||||
},
|
||||
{
|
||||
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?',
|
||||
pageClass: 'page-alerting',
|
||||
|
@ -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 },
|
||||
|
@ -5,6 +5,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, getTagColorsFromName, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { isPrivateLabel } from '../utils/labels';
|
||||
|
||||
@ -28,6 +29,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
|
||||
|
||||
const commonLabelsCount = Object.keys(commonLabels).length;
|
||||
const hasCommonLabels = commonLabelsCount > 0;
|
||||
const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} role="list" aria-label="Labels">
|
||||
@ -39,7 +41,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
onClick={() => setShowCommonLabels(true)}
|
||||
tooltip="Show common labels"
|
||||
tooltip={tooltip}
|
||||
tooltipPlacement="top"
|
||||
size="sm"
|
||||
>
|
||||
@ -54,7 +56,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
|
||||
tooltipPlacement="top"
|
||||
size="sm"
|
||||
>
|
||||
Hide common labels
|
||||
<Trans i18nKey="alert-labels.button.hide">Hide common labels</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</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 { 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';
|
||||
|
@ -7,6 +7,7 @@ export interface Line {
|
||||
current: GrafanaAlertStateWithReason;
|
||||
values?: Record<string, number>;
|
||||
labels?: Record<string, string>;
|
||||
ruleUID?: string;
|
||||
}
|
||||
|
||||
export interface LogRecord {
|
||||
|
@ -240,7 +240,7 @@ export function hashRule(rule: Rule): string {
|
||||
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])));
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -42,6 +42,11 @@ export function isAlertStateWithReason(
|
||||
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(
|
||||
state: GrafanaAlertStateWithReason | PromAlertingRuleState
|
||||
): GrafanaAlertState | PromAlertingRuleState {
|
||||
|
@ -25,6 +25,14 @@
|
||||
"user": "User"
|
||||
}
|
||||
},
|
||||
"alert-labels": {
|
||||
"button": {
|
||||
"hide": "Hide common labels",
|
||||
"show": {
|
||||
"tooltip": "Show common labels"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alert-rule-form": {
|
||||
"evaluation-behaviour": {
|
||||
"description": {
|
||||
@ -84,15 +92,15 @@
|
||||
},
|
||||
"counts": {
|
||||
"alertRule_one": "{{count}} alert rule",
|
||||
"alertRule_other": "{{count}} alert rules",
|
||||
"alertRule_other": "{{count}} alert rule",
|
||||
"dashboard_one": "{{count}} dashboard",
|
||||
"dashboard_other": "{{count}} dashboards",
|
||||
"dashboard_other": "{{count}} dashboard",
|
||||
"folder_one": "{{count}} folder",
|
||||
"folder_other": "{{count}} folders",
|
||||
"folder_other": "{{count}} folder",
|
||||
"libraryPanel_one": "{{count}} library panel",
|
||||
"libraryPanel_other": "{{count}} library panels",
|
||||
"libraryPanel_other": "{{count}} library panel",
|
||||
"total_one": "{{count}} item",
|
||||
"total_other": "{{count}} items"
|
||||
"total_other": "{{count}} item"
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "Collapse folder {{title}}",
|
||||
@ -138,6 +146,16 @@
|
||||
"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": {
|
||||
"inline-toast": {
|
||||
"success": "Copied"
|
||||
@ -758,7 +776,7 @@
|
||||
},
|
||||
"modal": {
|
||||
"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-view-panel1": "View panel in {{label}}...",
|
||||
"button-view-panel2": "View panel in dashboard...",
|
||||
|
@ -25,6 +25,14 @@
|
||||
"user": "Ůşęř"
|
||||
}
|
||||
},
|
||||
"alert-labels": {
|
||||
"button": {
|
||||
"hide": "Ħįđę čőmmőʼn ľäþęľş",
|
||||
"show": {
|
||||
"tooltip": "Ŝĥőŵ čőmmőʼn ľäþęľş"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alert-rule-form": {
|
||||
"evaluation-behaviour": {
|
||||
"description": {
|
||||
@ -84,15 +92,15 @@
|
||||
},
|
||||
"counts": {
|
||||
"alertRule_one": "{{count}} äľęřŧ řūľę",
|
||||
"alertRule_other": "{{count}} äľęřŧ řūľęş",
|
||||
"alertRule_other": "{{count}} äľęřŧ řūľę",
|
||||
"dashboard_one": "{{count}} đäşĥþőäřđ",
|
||||
"dashboard_other": "{{count}} đäşĥþőäřđş",
|
||||
"dashboard_other": "{{count}} đäşĥþőäřđ",
|
||||
"folder_one": "{{count}} ƒőľđęř",
|
||||
"folder_other": "{{count}} ƒőľđęřş",
|
||||
"folder_other": "{{count}} ƒőľđęř",
|
||||
"libraryPanel_one": "{{count}} ľįþřäřy päʼnęľ",
|
||||
"libraryPanel_other": "{{count}} ľįþřäřy päʼnęľş",
|
||||
"libraryPanel_other": "{{count}} ľįþřäřy päʼnęľ",
|
||||
"total_one": "{{count}} įŧęm",
|
||||
"total_other": "{{count}} įŧęmş"
|
||||
"total_other": "{{count}} įŧęm"
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}",
|
||||
@ -138,6 +146,16 @@
|
||||
"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": {
|
||||
"inline-toast": {
|
||||
"success": "Cőpįęđ"
|
||||
@ -758,7 +776,7 @@
|
||||
},
|
||||
"modal": {
|
||||
"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-view-panel1": "Vįęŵ päʼnęľ įʼn {{label}}...",
|
||||
"button-view-panel2": "Vįęŵ päʼnęľ įʼn đäşĥþőäřđ...",
|
||||
|
Loading…
Reference in New Issue
Block a user