mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Support for alerting for react panels, and lots of fixes to alert annotations for both react and angular (#33608)
* Alerting: Support for alerting for react panels, and lots of fixes to alert annotations for both react and angular * Fix showing annotations in panel edit
This commit is contained in:
parent
567b852072
commit
f2e4f41f69
@ -44,6 +44,8 @@ export interface AnnotationEvent {
|
||||
type?: string;
|
||||
tags?: string[];
|
||||
color?: string;
|
||||
alertId?: number;
|
||||
newState?: string;
|
||||
|
||||
// Currently used to merge annotations from alerts and dashboard
|
||||
source?: any; // source.type === 'dashboard'
|
||||
|
@ -169,7 +169,7 @@ export enum NullValueMode {
|
||||
*/
|
||||
export interface DataConfigSource {
|
||||
configRev?: number;
|
||||
dataSupport?: PanelPluginDataSupport;
|
||||
getDataSupport: () => PanelPluginDataSupport;
|
||||
getTransformations: () => DataTransformerConfig[] | undefined;
|
||||
getFieldOverrideOptions: () => ApplyFieldOverrideOptions | undefined;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { TestRuleResult } from './TestRuleResult';
|
||||
import { AppNotificationSeverity, StoreState } from 'app/types';
|
||||
import { PanelNotSupported } from '../dashboard/components/PanelEditor/PanelNotSupported';
|
||||
import { AlertState } from '../../plugins/datasource/alertmanager/types';
|
||||
import { EventBusSrv } from '@grafana/data';
|
||||
|
||||
interface AngularPanelController {
|
||||
_enableAlert: () => void;
|
||||
@ -76,24 +77,28 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
async loadAlertTab() {
|
||||
const { panel, angularPanelComponent } = this.props;
|
||||
|
||||
if (!this.element || !angularPanelComponent || this.component) {
|
||||
if (!this.element || this.component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = angularPanelComponent.getScope();
|
||||
if (angularPanelComponent) {
|
||||
const scope = angularPanelComponent.getScope();
|
||||
|
||||
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||
if (!scope.$$childHead) {
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
return;
|
||||
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||
if (!scope.$$childHead) {
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.panelCtrl = scope.$$childHead.ctrl;
|
||||
} else {
|
||||
this.panelCtrl = this.getReactAlertPanelCtrl();
|
||||
}
|
||||
|
||||
this.panelCtrl = scope.$$childHead.ctrl;
|
||||
const loader = getAngularLoader();
|
||||
const template = '<alert-tab />';
|
||||
|
||||
const scopeProps = { ctrl: this.panelCtrl };
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
@ -110,6 +115,16 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
getReactAlertPanelCtrl() {
|
||||
return {
|
||||
panel: this.props.panel,
|
||||
events: new EventBusSrv(),
|
||||
render: () => {
|
||||
this.props.panel.render();
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
onAddAlert = () => {
|
||||
this.panelCtrl?._enableAlert();
|
||||
this.component?.digest();
|
||||
|
@ -34,7 +34,7 @@ export const getPanelEditorTabs = memoizeOne((tab?: string, plugin?: PanelPlugin
|
||||
});
|
||||
}
|
||||
|
||||
if (getConfig().alertingEnabled && plugin.meta.id === 'graph') {
|
||||
if ((getConfig().alertingEnabled && plugin.meta.id === 'graph') || plugin.meta.id === 'timeseries') {
|
||||
tabs.push({
|
||||
id: PanelEditorTabId.Alert,
|
||||
text: 'Alert',
|
||||
|
@ -223,7 +223,7 @@ export class PanelChrome extends Component<Props, State> {
|
||||
panel.getQueryRunner().run({
|
||||
datasource: panel.datasource,
|
||||
queries: panel.targets,
|
||||
panelId: panel.id,
|
||||
panelId: panel.editSourceId || panel.id,
|
||||
dashboardId: this.props.dashboard.id,
|
||||
timezone: this.props.dashboard.getTimezone(),
|
||||
timeRange: timeData.timeRange,
|
||||
@ -364,11 +364,14 @@ export class PanelChrome extends Component<Props, State> {
|
||||
const { errorMessage, data } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
let alertState = data.alertState?.state;
|
||||
|
||||
const containerClassNames = classNames({
|
||||
'panel-container': true,
|
||||
'panel-container--absolute': true,
|
||||
'panel-container--transparent': transparent,
|
||||
'panel-container--no-title': this.hasOverlayHeader(),
|
||||
[`panel-alert-state--${alertState}`]: alertState !== undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -382,6 +385,7 @@ export class PanelChrome extends Component<Props, State> {
|
||||
error={errorMessage}
|
||||
isEditing={isEditing}
|
||||
isViewing={isViewing}
|
||||
alertState={alertState}
|
||||
data={data}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
|
@ -16,7 +16,6 @@ import { StoreState } from 'app/types';
|
||||
import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin } from '@grafana/data';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { RenderEvent } from 'app/types/events';
|
||||
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
@ -42,7 +41,6 @@ export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
export interface State {
|
||||
data: PanelData;
|
||||
errorMessage?: string;
|
||||
alertState?: string;
|
||||
}
|
||||
|
||||
interface AngularScopeProps {
|
||||
@ -84,29 +82,8 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
next: (data: PanelData) => this.onPanelDataUpdate(data),
|
||||
})
|
||||
);
|
||||
|
||||
this.subs.add(panel.events.subscribe(RenderEvent, this.onPanelRenderEvent));
|
||||
}
|
||||
|
||||
onPanelRenderEvent = (event: RenderEvent) => {
|
||||
const { alertState } = this.state;
|
||||
// graph sends these old render events with payloads
|
||||
const payload = event.payload;
|
||||
|
||||
if (payload && payload.alertState && this.props.panel.alert) {
|
||||
this.setState({ alertState: payload.alertState });
|
||||
} else if (payload && payload.alertState && !this.props.panel.alert) {
|
||||
// when user deletes alert in panel editor the source panel needs to refresh as this is in the mutable state and
|
||||
// will not automatically re render
|
||||
this.setState({ alertState: undefined });
|
||||
} else if (payload && alertState) {
|
||||
this.setState({ alertState: undefined });
|
||||
} else {
|
||||
// only needed for detecting title updates right now fix before 7.0
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onPanelDataUpdate(data: PanelData) {
|
||||
let errorMessage: string | undefined;
|
||||
|
||||
@ -213,9 +190,11 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isViewing, isEditing, plugin } = this.props;
|
||||
const { errorMessage, data, alertState } = this.state;
|
||||
const { errorMessage, data } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
let alertState = data.alertState?.state;
|
||||
|
||||
const containerClassNames = classNames({
|
||||
'panel-container': true,
|
||||
'panel-container--absolute': true,
|
||||
|
@ -173,7 +173,6 @@ export class PanelModel implements DataConfigSource {
|
||||
cachedPluginOptions: Record<string, PanelOptionsCache> = {};
|
||||
legend?: { show: boolean; sort?: string; sortDesc?: boolean };
|
||||
plugin?: PanelPlugin;
|
||||
dataSupport?: PanelPluginDataSupport;
|
||||
|
||||
/**
|
||||
* The PanelModel event bus only used for internal and legacy angular support.
|
||||
@ -355,7 +354,6 @@ export class PanelModel implements DataConfigSource {
|
||||
}
|
||||
}
|
||||
|
||||
this.dataSupport = plugin.dataSupport;
|
||||
this.applyPluginOptionDefaults(plugin, false);
|
||||
this.resendLastResult();
|
||||
}
|
||||
@ -484,6 +482,10 @@ export class PanelModel implements DataConfigSource {
|
||||
};
|
||||
}
|
||||
|
||||
getDataSupport(): PanelPluginDataSupport {
|
||||
return this.plugin?.dataSupport ?? { annotations: false, alertStates: false };
|
||||
}
|
||||
|
||||
getQueryRunner(): PanelQueryRunner {
|
||||
if (!this.queryRunner) {
|
||||
this.queryRunner = new PanelQueryRunner(this);
|
||||
|
@ -58,6 +58,21 @@ export function translateQueryResult(annotation: AnnotationQuery, results: Annot
|
||||
item.color = annotation.iconColor;
|
||||
item.type = annotation.name;
|
||||
item.isRegion = Boolean(item.timeEnd && item.time !== item.timeEnd);
|
||||
|
||||
switch (item.newState) {
|
||||
case 'pending':
|
||||
item.color = 'gray';
|
||||
break;
|
||||
case 'alerting':
|
||||
item.color = 'red';
|
||||
break;
|
||||
case 'ok':
|
||||
item.color = 'green';
|
||||
break;
|
||||
case 'no_data':
|
||||
item.color = 'gray';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@ -67,6 +67,7 @@ function describeQueryRunnerScenario(
|
||||
const defaultPanelConfig: grafanaData.DataConfigSource = {
|
||||
getFieldOverrideOptions: () => undefined,
|
||||
getTransformations: () => undefined,
|
||||
getDataSupport: () => ({ annotations: false, alertStates: false }),
|
||||
};
|
||||
const ctx: ScenarioContext = {
|
||||
maxDataPoints: 200,
|
||||
@ -251,6 +252,7 @@ describe('PanelQueryRunner', () => {
|
||||
theme: grafanaData.createTheme(),
|
||||
}),
|
||||
getTransformations: () => undefined,
|
||||
getDataSupport: () => ({ annotations: false, alertStates: false }),
|
||||
}
|
||||
);
|
||||
|
||||
@ -274,6 +276,7 @@ describe('PanelQueryRunner', () => {
|
||||
getFieldOverrideOptions: () => undefined,
|
||||
// @ts-ignore
|
||||
getTransformations: () => [({} as unknown) as grafanaData.DataTransformerConfig],
|
||||
getDataSupport: () => ({ annotations: false, alertStates: false }),
|
||||
}
|
||||
);
|
||||
|
||||
@ -316,6 +319,7 @@ describe('PanelQueryRunner', () => {
|
||||
}),
|
||||
// @ts-ignore
|
||||
getTransformations: () => [({} as unknown) as grafanaData.DataTransformerConfig],
|
||||
getDataSupport: () => ({ annotations: false, alertStates: false }),
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
DataTransformerConfig,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelPluginDataSupport,
|
||||
rangeUtil,
|
||||
ScopedVars,
|
||||
TimeRange,
|
||||
@ -67,12 +66,10 @@ export class PanelQueryRunner {
|
||||
private subscription?: Unsubscribable;
|
||||
private lastResult?: PanelData;
|
||||
private dataConfigSource: DataConfigSource;
|
||||
private dataSupport?: PanelPluginDataSupport;
|
||||
|
||||
constructor(dataConfigSource: DataConfigSource) {
|
||||
this.subject = new ReplaySubject(1);
|
||||
this.dataConfigSource = dataConfigSource;
|
||||
this.dataSupport = this.dataConfigSource.dataSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -249,7 +246,9 @@ export class PanelQueryRunner {
|
||||
}
|
||||
|
||||
let panelData = observable;
|
||||
if (this.dataSupport?.alertStates || this.dataSupport?.annotations) {
|
||||
const dataSupport = this.dataConfigSource.getDataSupport();
|
||||
|
||||
if (dataSupport.alertStates || dataSupport.annotations) {
|
||||
panelData = mergePanelAndDashData(observable, getDashboardQueryRunner().getResult(panelId));
|
||||
}
|
||||
|
||||
|
@ -109,6 +109,7 @@ export function getDefaultState(): State {
|
||||
const dataConfig = {
|
||||
getTransformations: () => [] as DataTransformerConfig[],
|
||||
getFieldOverrideOptions: () => options,
|
||||
getDataSupport: () => ({ annotations: false, alertStates: false }),
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -26,4 +26,4 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(TimeSeriesPanel
|
||||
|
||||
addLegendOptions(builder);
|
||||
})
|
||||
.setDataSupport({ annotations: true });
|
||||
.setDataSupport({ annotations: true, alertStates: true });
|
||||
|
@ -1,16 +1,17 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { HorizontalGroup, Portal, Tag, VizTooltipContainer, useStyles } from '@grafana/ui';
|
||||
import React, { CSSProperties, useCallback, useRef, useState } from 'react';
|
||||
import { GrafanaThemeV2, dateTimeFormat, systemDateFormats, TimeZone, textUtil, getColorForTheme } from '@grafana/data';
|
||||
import { HorizontalGroup, Portal, Tag, VizTooltipContainer, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import alertDef from 'app/features/alerting/state/alertDef';
|
||||
|
||||
interface AnnotationMarkerProps {
|
||||
time: string;
|
||||
text: string;
|
||||
tags: string[];
|
||||
interface Props {
|
||||
timeZone: TimeZone;
|
||||
annotation: AnnotationsDataFrameViewDTO;
|
||||
}
|
||||
|
||||
export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text, tags }) => {
|
||||
const styles = useStyles(getAnnotationMarkerStyles);
|
||||
export function AnnotationMarker({ annotation, timeZone }: Props) {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getAnnotationMarkerStyles);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const markerRef = useRef<HTMLDivElement>(null);
|
||||
const annotationPopoverRef = useRef<HTMLDivElement>(null);
|
||||
@ -29,6 +30,25 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
|
||||
}, 100);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const timeFormatter = useCallback(
|
||||
(value: number) => {
|
||||
return dateTimeFormat(value, {
|
||||
format: systemDateFormats.fullDate,
|
||||
timeZone,
|
||||
});
|
||||
},
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
const markerStyles: CSSProperties = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '4px solid transparent',
|
||||
borderRight: '4px solid transparent',
|
||||
borderBottom: `4px solid ${getColorForTheme(annotation.color, theme.v1)}`,
|
||||
pointerEvents: 'none',
|
||||
};
|
||||
|
||||
const renderMarker = useCallback(() => {
|
||||
if (!markerRef?.current) {
|
||||
return null;
|
||||
@ -36,6 +56,24 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
|
||||
|
||||
const el = markerRef.current;
|
||||
const elBBox = el.getBoundingClientRect();
|
||||
const time = timeFormatter(annotation.time);
|
||||
let text = annotation.text;
|
||||
const tags = annotation.tags;
|
||||
let alertText = '';
|
||||
let state: React.ReactNode | null = null;
|
||||
|
||||
if (annotation.alertId) {
|
||||
const stateModel = alertDef.getStateDisplayModel(annotation.newState!);
|
||||
state = (
|
||||
<div className={styles.alertState}>
|
||||
<i className={stateModel.stateClass}>{stateModel.text}</i>
|
||||
</div>
|
||||
);
|
||||
|
||||
alertText = alertDef.getAlertAnnotationInfo(annotation);
|
||||
} else if (annotation.title) {
|
||||
text = annotation.title + '<br />' + (typeof text === 'string' ? text : '');
|
||||
}
|
||||
|
||||
return (
|
||||
<VizTooltipContainer
|
||||
@ -47,11 +85,12 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
|
||||
>
|
||||
<div ref={annotationPopoverRef} className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
{/*<span className={styles.title}>{annotationEvent.title}</span>*/}
|
||||
{state}
|
||||
{time && <span className={styles.time}>{time}</span>}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{text && <div dangerouslySetInnerHTML={{ __html: text }} />}
|
||||
{text && <div dangerouslySetInnerHTML={{ __html: textUtil.sanitize(text) }} />}
|
||||
{alertText}
|
||||
<>
|
||||
<HorizontalGroup spacing="xs" wrap>
|
||||
{tags?.map((t, i) => (
|
||||
@ -63,62 +102,40 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
|
||||
</div>
|
||||
</VizTooltipContainer>
|
||||
);
|
||||
}, [onMouseEnter, onMouseLeave, styles, time, text, tags]);
|
||||
}, [onMouseEnter, onMouseLeave, timeFormatter, styles, annotation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={markerRef} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={styles.markerWrapper}>
|
||||
<div className={styles.marker} />
|
||||
<div style={markerStyles} />
|
||||
</div>
|
||||
{isOpen && <Portal>{renderMarker()}</Portal>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getAnnotationMarkerStyles = (theme: GrafanaTheme) => {
|
||||
const bg = theme.isDark ? theme.palette.dark2 : theme.palette.white;
|
||||
const headerBg = theme.isDark ? theme.palette.dark9 : theme.palette.gray5;
|
||||
const shadowColor = theme.isDark ? theme.palette.black : theme.palette.white;
|
||||
}
|
||||
|
||||
const getAnnotationMarkerStyles = (theme: GrafanaThemeV2) => {
|
||||
return {
|
||||
markerWrapper: css`
|
||||
padding: 0 4px 4px 4px;
|
||||
`,
|
||||
marker: css`
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid ${theme.palette.red};
|
||||
pointer-events: none;
|
||||
`,
|
||||
wrapper: css`
|
||||
background: ${bg};
|
||||
border: 1px solid ${headerBg};
|
||||
border-radius: ${theme.border.radius.md};
|
||||
max-width: 400px;
|
||||
box-shadow: 0 0 20px ${shadowColor};
|
||||
`,
|
||||
tooltip: css`
|
||||
background: none;
|
||||
padding: 0;
|
||||
`,
|
||||
header: css`
|
||||
background: ${headerBg};
|
||||
padding: 6px 10px;
|
||||
padding: ${theme.spacing(0.5, 1)};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
display: flex;
|
||||
`,
|
||||
title: css`
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
padding-right: ${theme.spacing.md};
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
alertState: css`
|
||||
padding-right: ${theme.spacing(1)};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
`,
|
||||
time: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
color: ${theme.colors.text.secondary};
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
@ -126,8 +143,7 @@ const getAnnotationMarkerStyles = (theme: GrafanaTheme) => {
|
||||
top: 1px;
|
||||
`,
|
||||
body: css`
|
||||
padding: ${theme.spacing.sm};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataFrame, DataFrameView, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
|
||||
import { DataFrame, DataFrameView, getColorForTheme, TimeZone } from '@grafana/data';
|
||||
import { EventsCanvas, UPlotConfigBuilder, usePlotContext, useTheme } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { AnnotationMarker } from './AnnotationMarker';
|
||||
@ -9,28 +9,12 @@ interface AnnotationsPluginProps {
|
||||
timeZone: TimeZone;
|
||||
}
|
||||
|
||||
interface AnnotationsDataFrameViewDTO {
|
||||
time: number;
|
||||
text: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone, config }) => {
|
||||
const theme = useTheme();
|
||||
const plotCtx = usePlotContext();
|
||||
|
||||
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
|
||||
|
||||
const timeFormatter = useCallback(
|
||||
(value: number) => {
|
||||
return dateTimeFormat(value, {
|
||||
format: systemDateFormats.fullDate,
|
||||
timeZone,
|
||||
});
|
||||
},
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
// Update annotations views when new annotations came
|
||||
useEffect(() => {
|
||||
const views: Array<DataFrameView<AnnotationsDataFrameViewDTO>> = [];
|
||||
@ -68,7 +52,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
const xpos = u.valToPos(annotation.time, 'x', true);
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = theme.palette.red;
|
||||
ctx.strokeStyle = getColorForTheme(annotation.color, theme);
|
||||
ctx.setLineDash([5, 5]);
|
||||
ctx.moveTo(xpos, u.bbox.top);
|
||||
ctx.lineTo(xpos, u.bbox.top + u.bbox.height);
|
||||
@ -101,9 +85,9 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
(frame: DataFrame, index: number) => {
|
||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||
const annotation = view.get(index);
|
||||
return <AnnotationMarker time={timeFormatter(annotation.time)} text={annotation.text} tags={annotation.tags} />;
|
||||
return <AnnotationMarker annotation={annotation} timeZone={timeZone} />;
|
||||
},
|
||||
[timeFormatter]
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
return (
|
||||
|
9
public/app/plugins/panel/timeseries/plugins/types.ts
Normal file
9
public/app/plugins/panel/timeseries/plugins/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
interface AnnotationsDataFrameViewDTO {
|
||||
time: number;
|
||||
text: string;
|
||||
tags: string[];
|
||||
alertId?: number;
|
||||
newState?: string;
|
||||
title?: string;
|
||||
color: string;
|
||||
}
|
@ -273,8 +273,9 @@
|
||||
|
||||
.graph-annotation__header {
|
||||
background: $popover-header-bg;
|
||||
padding: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.graph-annotation__title {
|
||||
|
Loading…
Reference in New Issue
Block a user