Panels: AnnotationsPlugin2 (#79531)

Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
pull/80412/head
Leon Sorokin 1 year ago committed by GitHub
parent d4f76c3391
commit b53e0521d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      .betterer.results
  2. 8
      packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx
  3. 53
      packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx
  4. 39
      public/app/plugins/panel/candlestick/CandlestickPanel.tsx
  5. 9
      public/app/plugins/panel/heatmap/HeatmapHoverView.tsx
  6. 46
      public/app/plugins/panel/heatmap/HeatmapPanel.tsx
  7. 34
      public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
  8. 4
      public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx
  9. 52
      public/app/plugins/panel/status-history/StatusHistoryPanel.tsx
  10. 5
      public/app/plugins/panel/status-history/StatusHistoryTooltip2.tsx
  11. 52
      public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx
  12. 5
      public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx
  13. 249
      public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx
  14. 160
      public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx
  15. 144
      public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx
  16. 157
      public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx
  17. 4
      public/app/plugins/panel/trend/TrendPanel.tsx
  18. 4
      public/app/plugins/panel/xychart/XYChartPanel.tsx
  19. 2
      public/app/plugins/panel/xychart/XYChartTooltip.tsx
  20. 4
      public/app/plugins/panel/xychart/scatter.ts

@ -6442,6 +6442,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
],
"public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/timeseries/plugins/ThresholdDragHandle.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
@ -6484,6 +6487,15 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "7"],
[0, 0, 0, "Styles should be written using objects.", "8"]
],
"public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/timeseries/plugins/styles.ts:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]

@ -8,12 +8,12 @@ import { useStyles2 } from '../../themes';
interface Props {
dataLinks: Array<LinkModel<Field>>;
canAnnotate: boolean;
annotate?: () => void;
}
export const ADD_ANNOTATION_ID = 'add-annotation-button';
export const VizTooltipFooter = ({ dataLinks, canAnnotate }: Props) => {
export const VizTooltipFooter = ({ dataLinks, annotate }: Props) => {
const styles = useStyles2(getStyles);
const renderDataLinks = () => {
@ -33,9 +33,9 @@ export const VizTooltipFooter = ({ dataLinks, canAnnotate }: Props) => {
return (
<div className={styles.wrapper}>
{dataLinks.length > 0 && <div className={styles.dataLinks}>{renderDataLinks()}</div>}
{canAnnotate && (
{annotate && (
<div className={styles.addAnnotations}>
<Button icon="comment-alt" variant="secondary" size="sm" id={ADD_ANNOTATION_ID}>
<Button icon="comment-alt" variant="secondary" size="sm" id={ADD_ANNOTATION_ID} onClick={annotate}>
Add annotation
</Button>
</div>

@ -1,11 +1,12 @@
import { css, cx } from '@emotion/css';
import React, { useLayoutEffect, useRef, useReducer, CSSProperties } from 'react';
import React, { useLayoutEffect, useRef, useReducer, CSSProperties, useContext, useEffect } from 'react';
import { createPortal } from 'react-dom';
import uPlot from 'uplot';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { LayoutItemContext } from '../../Layout/LayoutItemContext';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
import { CloseButton } from './CloseButton';
@ -36,7 +37,9 @@ interface TooltipPlugin2Props {
dataIdxs: Array<number | null>,
seriesIdx: number | null,
isPinned: boolean,
dismiss: () => void
dismiss: () => void,
// selected time range (for annotation triggering)
timeRange: TimeRange2 | null
) => React.ReactNode;
}
@ -55,6 +58,11 @@ interface TooltipContainerSize {
height: number;
}
export interface TimeRange2 {
from: number;
to: number;
}
function mergeState(prevState: TooltipContainerState, nextState: Partial<TooltipContainerState>) {
return {
...prevState,
@ -88,6 +96,9 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, INITIAL_STATE);
const { boostZIndex } = useContext(LayoutItemContext);
useEffect(() => (isPinned ? boostZIndex() : undefined), [isPinned]);
const sizeRef = useRef<TooltipContainerSize>();
const styles = useStyles2(getStyles);
@ -134,6 +145,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
winHeight = htmlEl.clientHeight - 5;
});
let selectedRange: TimeRange2 | null = null;
let seriesIdxs: Array<number | null> = plot?.cursor.idxs!.slice()!;
let closestSeriesIdx: number | null = null;
@ -160,9 +172,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
// in some ways this is similar to ClickOutsideWrapper.tsx
const downEventOutside = (e: Event) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
let isOutside = (e.target as HTMLDivElement).closest(`.${styles.tooltipWrapper}`) !== domRef.current;
if (isOutside) {
if (!domRef.current!.contains(e.target as Node)) {
dismiss();
}
};
@ -173,8 +183,6 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
if (pendingPinned) {
_style = { pointerEvents: _isPinned ? 'all' : 'none' };
domRef.current?.closest<HTMLDivElement>('.react-grid-item')?.classList.toggle('context-menu-open', _isPinned);
// @ts-ignore
_plot!.cursor._lock = _isPinned;
@ -193,18 +201,24 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
style: _style,
isPinned: _isPinned,
isHovering: _isHovering,
contents: _isHovering ? renderRef.current(_plot!, seriesIdxs, closestSeriesIdx, _isPinned, dismiss) : null,
contents:
_isHovering || selectedRange != null
? renderRef.current(_plot!, seriesIdxs, closestSeriesIdx, _isPinned, dismiss, selectedRange)
: null,
dismiss,
};
setState(state);
selectedRange = null;
};
const dismiss = () => {
let prevIsPinned = _isPinned;
_isPinned = false;
_isHovering = false;
_plot!.setCursor({ left: -10, top: -10 });
scheduleRender(true);
scheduleRender(prevIsPinned);
};
config.addHook('init', (u) => {
@ -240,11 +254,23 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
// this handles pinning
u.over.addEventListener('click', (e) => {
if (e.target === u.over) {
if (e.ctrlKey || e.metaKey) {
let xVal = u.posToVal(u.cursor.left!, 'x');
selectedRange = {
from: xVal,
to: xVal,
};
scheduleRender(false);
}
// only pinnable tooltip is visible *and* is within proximity to series/point
if (_isHovering && closestSeriesIdx != null && !_isPinned && e.target === u.over) {
else if (_isHovering && closestSeriesIdx != null && !_isPinned) {
_isPinned = true;
scheduleRender(true);
}
}
});
});
@ -276,6 +302,13 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
yZoomed = false;
}
}
} else {
selectedRange = {
from: u.posToVal(u.select.left!, 'x'),
to: u.posToVal(u.select.left! + u.select.width, 'x'),
};
scheduleRender(true);
}
}

@ -1,7 +1,7 @@
// this file is pretty much a copy-paste of TimeSeriesPanel.tsx :(
// with some extra renderers passed to the <TimeSeries> component
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import uPlot from 'uplot';
import { DashboardCursorSync, Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data';
@ -10,13 +10,14 @@ import { TooltipDisplayMode } from '@grafana/schema';
import { TooltipPlugin, TooltipPlugin2, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip';
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
@ -48,6 +49,9 @@ export const CandlestickPanel = ({
return prepareCandlestickFields(data.series, options, theme, timeRange);
}, [data.series, options, theme, timeRange]);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { renderers, tweakScale, tweakAxis, shouldRenderPrice } = useMemo(() => {
let tweakScale = (opts: ScaleProps, forField: Field) => opts;
let tweakAxis = (opts: AxisProps, forField: Field) => opts;
@ -229,7 +233,8 @@ export const CandlestickPanel = ({
}
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
const showNewVizTooltips = config.featureToggles.newVizTooltips && sync && sync() === DashboardCursorSync.Off;
const showNewVizTooltips =
config.featureToggles.newVizTooltips && (sync == null || sync() === DashboardCursorSync.Off);
return (
<TimeSeries
@ -264,7 +269,20 @@ export const CandlestickPanel = ({
hoverMode={TooltipHoverMode.xAll}
queryZoom={onChangeTimeRange}
clientZoom={true}
render={(u, dataIdxs, seriesIdx, isPinned = false) => {
render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2) => {
if (timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<TimeSeriesTooltip
frames={[info.frame]}
@ -273,6 +291,7 @@ export const CandlestickPanel = ({
seriesIdx={seriesIdx}
mode={TooltipDisplayMode.Multi}
isPinned={isPinned}
annotate={enableAnnotationCreation ? annotate : undefined}
/>
);
}}
@ -290,8 +309,18 @@ export const CandlestickPanel = ({
</>
)}
{/* Renders annotation markers*/}
{data.annotations && (
{showNewVizTooltips ? (
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={uplotConfig}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
/>
) : (
data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={uplotConfig} timeZone={timeZone} />
)
)}
{/* Enables annotations creation*/}
{!showNewVizTooltips ? (

@ -39,10 +39,10 @@ interface Props {
showColorScale?: boolean;
isPinned: boolean;
dismiss: () => void;
canAnnotate: boolean;
panelData: PanelData;
replaceVars: InterpolateFunction;
scopedVars: ScopedVars[];
annotate?: () => void;
}
export const HeatmapHoverView = (props: Props) => {
@ -65,11 +65,11 @@ const HeatmapHoverCell = ({
dataRef,
showHistogram,
isPinned,
canAnnotate,
showColorScale = false,
scopedVars,
replaceVars,
mode,
annotate,
}: Props) => {
const index = dataIdxs[1]!;
const data = dataRef.current;
@ -375,9 +375,6 @@ const HeatmapHoverCell = ({
return content;
};
// @TODO remove this when adding annotations support
canAnnotate = false;
const styles = useStyles2(getStyles);
return (
@ -388,7 +385,7 @@ const HeatmapHoverCell = ({
customContent={getCustomContent()}
isPinned={isPinned}
/>
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={canAnnotate} />}
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
</div>
);
};

@ -27,11 +27,11 @@ import {
VizLayout,
VizTooltipContainer,
} from '@grafana/ui';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { ExemplarModalHeader } from './ExemplarModalHeader';
import { HeatmapHoverView } from './HeatmapHoverView';
@ -60,7 +60,8 @@ export const HeatmapPanel = ({
const styles = useStyles2(getStyles);
const { sync, canAddAnnotations } = usePanelContext();
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
// necessary for enabling datalinks in hover view
let scopedVarsFromRawData: ScopedVars[] = [];
@ -158,7 +159,8 @@ export const HeatmapPanel = ({
// ugh
const dataRef = useRef(info);
dataRef.current = info;
const showNewVizTooltips = config.featureToggles.newVizTooltips && sync && sync() === DashboardCursorSync.Off;
const showNewVizTooltips =
config.featureToggles.newVizTooltips && (sync == null || sync() === DashboardCursorSync.Off);
const builder = useMemo(() => {
const scaleConfig: ScaleDistributionConfig = dataRef.current?.heatmap?.fields[1].config?.custom?.scaleDistribution;
@ -226,19 +228,35 @@ export const HeatmapPanel = ({
);
}
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
return (
<>
<VizLayout width={width} height={height} legend={renderLegend()}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart config={builder} data={facets as any} width={vizWidth} height={vizHeight}>
{/*children ? children(config, alignedFrame) : null*/}
{!showNewVizTooltips && <ZoomPlugin config={builder} onZoom={onChangeTimeRange} />}
{showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None && (
{showNewVizTooltips && (
<>
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={TooltipHoverMode.xyOne}
queryZoom={onChangeTimeRange}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2) => {
if (timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<HeatmapHoverView
mode={options.tooltip.mode}
@ -249,27 +267,30 @@ export const HeatmapPanel = ({
dismiss={dismiss}
showHistogram={options.tooltip.yHistogram}
showColorScale={options.tooltip.showColorScale}
canAnnotate={enableAnnotationCreation}
panelData={data}
replaceVars={replaceVariables}
scopedVars={scopedVarsFromRawData}
annotate={enableAnnotationCreation ? annotate : undefined}
/>
);
}}
/>
)}
{data.annotations && (
<AnnotationsPlugin
annotations={data.annotations}
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={builder}
timeZone={timeZone}
disableCanvasRendering={true}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
canvasRegionRendering={false}
/>
</>
)}
</UPlotChart>
)}
</VizLayout>
{!showNewVizTooltips && (
<>
<Portal>
{hover && options.tooltip.mode !== TooltipDisplayMode.None && (
<VizTooltipContainer
@ -289,6 +310,7 @@ export const HeatmapPanel = ({
</VizTooltipContainer>
)}
</Portal>
</>
)}
</>
);

@ -14,7 +14,7 @@ import {
ZoomPlugin,
} from '@grafana/ui';
import { addTooltipSupport, HoverEvent } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
import {
@ -25,6 +25,7 @@ import {
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
@ -60,6 +61,8 @@ export const StateTimelinePanel = ({
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [isActive, setIsActive] = useState<boolean>(false);
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { sync, canAddAnnotations } = usePanelContext();
const onCloseToolTip = () => {
@ -163,7 +166,8 @@ export const StateTimelinePanel = ({
}
}
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
const showNewVizTooltips = config.featureToggles.newVizTooltips && sync && sync() === DashboardCursorSync.Off;
const showNewVizTooltips =
config.featureToggles.newVizTooltips && (sync == null || sync() === DashboardCursorSync.Off);
return (
<TimelineChart
@ -203,7 +207,20 @@ export const StateTimelinePanel = ({
config={builder}
hoverMode={TooltipHoverMode.xOne}
queryZoom={onChangeTimeRange}
render={(u, dataIdxs, seriesIdx, isPinned) => {
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2) => {
if (timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<StateTimelineTooltip2
data={frames ?? []}
@ -215,16 +232,27 @@ export const StateTimelinePanel = ({
sortOrder={options.tooltip.sort}
isPinned={isPinned}
timeRange={timeRange}
annotate={enableAnnotationCreation ? annotate : undefined}
/>
);
}}
/>
)}
{/* Renders annotations */}
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={builder}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
canvasRegionRendering={false}
/>
</>
) : (
<>
<ZoomPlugin config={builder} onZoom={onChangeTimeRange} />
<OutsideRangePlugin config={builder} onChangeTimeRange={onChangeTimeRange} />
{/* Renders annotation markers*/}
{data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={builder} timeZone={timeZone} />
)}

@ -34,6 +34,7 @@ interface StateTimelineTooltip2Props {
timeRange: TimeRange;
mode?: TooltipDisplayMode;
sortOrder?: SortOrder;
annotate?: () => void;
}
export const StateTimelineTooltip2 = ({
@ -46,6 +47,7 @@ export const StateTimelineTooltip2 = ({
mode = TooltipDisplayMode.Single,
sortOrder = SortOrder.None,
isPinned,
annotate,
}: StateTimelineTooltip2Props) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
@ -182,7 +184,7 @@ export const StateTimelineTooltip2 = ({
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
</div>
);
};

@ -13,7 +13,7 @@ import {
ZoomPlugin,
} from '@grafana/ui';
import { addTooltipSupport, HoverEvent } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
import {
@ -23,6 +23,7 @@ import {
} from 'app/core/components/TimelineChart/utils';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
@ -57,7 +58,11 @@ export const StatusHistoryPanel = ({
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [isActive, setIsActive] = useState<boolean>(false);
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
const { sync } = usePanelContext();
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { sync, canAddAnnotations } = usePanelContext();
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
const onCloseToolTip = () => {
isToolTipOpen.current = false;
@ -190,7 +195,8 @@ export const StatusHistoryPanel = ({
);
}
const showNewVizTooltips = config.featureToggles.newVizTooltips && sync && sync() === DashboardCursorSync.Off;
const showNewVizTooltips =
config.featureToggles.newVizTooltips && (sync == null || sync() === DashboardCursorSync.Off);
return (
<TimelineChart
@ -222,14 +228,6 @@ export const StatusHistoryPanel = ({
return (
<>
{data.annotations && (
<AnnotationsPlugin
annotations={data.annotations}
config={builder}
timeZone={timeZone}
disableCanvasRendering={true}
/>
)}
{showNewVizTooltips ? (
<>
{options.tooltip.mode !== TooltipDisplayMode.None && (
@ -237,7 +235,20 @@ export const StatusHistoryPanel = ({
config={builder}
hoverMode={TooltipHoverMode.xyOne}
queryZoom={onChangeTimeRange}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2) => {
if (timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<StatusHistoryTooltip2
data={frames ?? []}
@ -248,17 +259,34 @@ export const StatusHistoryPanel = ({
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
annotate={enableAnnotationCreation ? annotate : undefined}
/>
);
}}
/>
)}
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={builder}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
canvasRegionRendering={false}
/>
</>
) : (
<>
<ZoomPlugin config={builder} onZoom={onChangeTimeRange} />
{renderTooltip(alignedFrame)}
<OutsideRangePlugin config={builder} onChangeTimeRange={onChangeTimeRange} />
{data.annotations && (
<AnnotationsPlugin
annotations={data.annotations}
config={builder}
timeZone={timeZone}
disableCanvasRendering={true}
/>
)}
</>
)}
</>

@ -31,6 +31,7 @@ interface StatusHistoryTooltipProps {
isPinned: boolean;
mode?: TooltipDisplayMode;
sortOrder?: SortOrder;
annotate?: () => void;
}
function fmt(field: Field, val: number): string {
@ -42,13 +43,13 @@ function fmt(field: Field, val: number): string {
}
export const StatusHistoryTooltip2 = ({
data,
dataIdxs,
alignedData,
seriesIdx,
mode = TooltipDisplayMode.Single,
sortOrder = SortOrder.None,
isPinned,
annotate,
}: StatusHistoryTooltipProps) => {
const styles = useStyles2(getStyles);
@ -144,7 +145,7 @@ export const StatusHistoryTooltip2 = ({
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
</div>
);
};

@ -1,10 +1,10 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { PanelProps, DataFrameType, DashboardCursorSync } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import { KeyboardPlugin, TooltipPlugin, TooltipPlugin2, usePanelContext, ZoomPlugin } from '@grafana/ui';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
@ -12,6 +12,7 @@ import { TimeSeriesTooltip } from './TimeSeriesTooltip';
import { Options } from './panelcfg.gen';
import { AnnotationEditorPlugin } from './plugins/AnnotationEditorPlugin';
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
import { AnnotationsPlugin2 } from './plugins/AnnotationsPlugin2';
import { ContextMenuPlugin } from './plugins/ContextMenuPlugin';
import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin';
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
@ -49,6 +50,12 @@ export const TimeSeriesPanel = ({
return undefined;
}, [frames, id]);
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
const showNewVizTooltips =
config.featureToggles.newVizTooltips && (sync == null || sync() === DashboardCursorSync.Off);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
if (!frames || suggestions) {
return (
<PanelDataErrorView
@ -63,8 +70,13 @@ export const TimeSeriesPanel = ({
);
}
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
const showNewVizTooltips = config.featureToggles.newVizTooltips && sync && sync() === DashboardCursorSync.Off;
// which annotation are we editing?
// are we adding a new annotation? is annotating?
// console.log(data.annotations);
// annotations plugin includes the editor and the renderer
// its annotation state is managed here for now
// tooltipplugin2 receives render with annotate range, callback should setstate here that gets passed to annotationsplugin as newAnnotaton or editAnnotation
return (
<TimeSeries
@ -90,7 +102,7 @@ export const TimeSeriesPanel = ({
return (
<>
<KeyboardPlugin config={uplotConfig} />
{!showNewVizTooltips && <KeyboardPlugin config={uplotConfig} />}
{options.tooltip.mode === TooltipDisplayMode.None || (
<>
{showNewVizTooltips ? (
@ -101,8 +113,22 @@ export const TimeSeriesPanel = ({
}
queryZoom={onChangeTimeRange}
clientZoom={true}
render={(u, dataIdxs, seriesIdx, isPinned = false) => {
render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2) => {
if (timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
// not sure it header time here works for annotations, since it's taken from nearest datapoint index
<TimeSeriesTooltip
frames={frames}
seriesFrame={alignedDataFrame}
@ -111,6 +137,7 @@ export const TimeSeriesPanel = ({
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
annotate={enableAnnotationCreation ? annotate : undefined}
/>
);
}}
@ -132,9 +159,20 @@ export const TimeSeriesPanel = ({
</>
)}
{/* Renders annotation markers*/}
{data.annotations && (
{showNewVizTooltips ? (
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={uplotConfig}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
/>
) : (
data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={uplotConfig} timeZone={timeZone} />
)
)}
{/*Enables annotations creation*/}
{!showNewVizTooltips ? (
enableAnnotationCreation ? (

@ -38,6 +38,8 @@ interface TimeSeriesTooltipProps {
sortOrder?: SortOrder;
isPinned: boolean;
annotate?: () => void;
}
export const TimeSeriesTooltip = ({
@ -48,6 +50,7 @@ export const TimeSeriesTooltip = ({
mode = TooltipDisplayMode.Single,
sortOrder = SortOrder.None,
isPinned,
annotate,
}: TimeSeriesTooltipProps) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
@ -147,7 +150,7 @@ export const TimeSeriesTooltip = ({
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
</div>
</div>
);

@ -0,0 +1,249 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import tinycolor from 'tinycolor2';
import uPlot from 'uplot';
import { arrayToDataFrame, colorManipulator, DataFrame, DataTopic, GrafanaTheme2 } from '@grafana/data';
import { TimeZone } from '@grafana/schema';
import { DEFAULT_ANNOTATION_COLOR, UPlotConfigBuilder, useStyles2, useTheme2 } from '@grafana/ui';
import { AnnotationMarker2 } from './annotations2/AnnotationMarker2';
// (copied from TooltipPlugin2)
interface TimeRange2 {
from: number;
to: number;
}
interface AnnotationsPluginProps {
config: UPlotConfigBuilder;
annotations: DataFrame[];
timeZone: TimeZone;
newRange: TimeRange2 | null;
setNewRange: (newRage: TimeRange2 | null) => void;
canvasRegionRendering?: boolean;
}
// TODO: batch by color, use Path2D objects
const renderLine = (ctx: CanvasRenderingContext2D, y0: number, y1: number, x: number, color: string) => {
ctx.beginPath();
ctx.moveTo(x, y0);
ctx.lineTo(x, y1);
ctx.strokeStyle = color;
ctx.stroke();
};
// const renderUpTriangle = (ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, color: string) => {
// ctx.beginPath();
// ctx.moveTo(x - w/2, y + h/2);
// ctx.lineTo(x + w/2, y + h/2);
// ctx.lineTo(x, y);
// ctx.closePath();
// ctx.fillStyle = color;
// ctx.fill();
// }
const DEFAULT_ANNOTATION_COLOR_HEX8 = tinycolor(DEFAULT_ANNOTATION_COLOR).toHex8String();
function getVals(frame: DataFrame) {
let vals: Record<string, any[]> = {};
frame.fields.forEach((f) => {
vals[f.name] = f.values;
});
return vals;
}
export const AnnotationsPlugin2 = ({
annotations,
timeZone,
config,
newRange,
setNewRange,
canvasRegionRendering = true,
}: AnnotationsPluginProps) => {
const [plot, setPlot] = useState<uPlot>();
const styles = useStyles2(getStyles);
const getColorByName = useTheme2().visualization.getColorByName;
const annos = useMemo(() => {
let annos = annotations.slice();
if (newRange) {
let isRegion = newRange.to > newRange.from;
const wipAnnoFrame = arrayToDataFrame([
{
time: newRange.from,
timeEnd: isRegion ? newRange.to : null,
isRegion: isRegion,
color: DEFAULT_ANNOTATION_COLOR_HEX8,
},
]);
wipAnnoFrame.meta = {
dataTopic: DataTopic.Annotations,
custom: {
isWip: true,
},
};
annos.push(wipAnnoFrame);
}
return annos;
}, [annotations, newRange]);
const exitWipEdit = useCallback(() => {
setNewRange(null);
}, [setNewRange]);
const annoRef = useRef(annos);
annoRef.current = annos;
const newRangeRef = useRef(newRange);
newRangeRef.current = newRange;
const xAxisRef = useRef<HTMLDivElement>();
useLayoutEffect(() => {
config.addHook('ready', (u) => {
let xAxisEl = u.root.querySelector<HTMLDivElement>('.u-axis')!;
xAxisRef.current = xAxisEl;
setPlot(u);
});
config.addHook('draw', (u) => {
let annos = annoRef.current;
const ctx = u.ctx;
let y0 = u.bbox.top;
let y1 = y0 + u.bbox.height;
ctx.save();
ctx.beginPath();
ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
ctx.clip();
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
annos.forEach((frame) => {
let vals = getVals(frame);
for (let i = 0; i < vals.time.length; i++) {
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
let x0 = u.valToPos(vals.time[i], 'x', true);
if (!vals.isRegion?.[i]) {
renderLine(ctx, y0, y1, x0, color);
// renderUpTriangle(ctx, x0, y1, 8 * uPlot.pxRatio, 5 * uPlot.pxRatio, color);
} else if (canvasRegionRendering) {
renderLine(ctx, y0, y1, x0, color);
let x1 = u.valToPos(vals.timeEnd[i], 'x', true);
renderLine(ctx, y0, y1, x1, color);
ctx.fillStyle = colorManipulator.alpha(color, 0.1);
ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
}
}
});
ctx.restore();
});
}, [config, canvasRegionRendering, getColorByName]);
// ensure annos are re-drawn whenever they change
useEffect(() => {
if (plot) {
plot.redraw();
}
}, [annos, plot]);
if (plot) {
let markers = annos.flatMap((frame, frameIdx) => {
let vals = getVals(frame);
let markers: React.ReactNode[] = [];
for (let i = 0; i < vals.time.length; i++) {
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR);
let left = plot.valToPos(vals.time[i], 'x');
let style: React.CSSProperties | null = null;
let className = '';
let isVisible = true;
if (vals.isRegion?.[i]) {
let right = plot.valToPos(vals.timeEnd?.[i], 'x');
isVisible = left < plot.rect.width && right > 0;
if (isVisible) {
let clampedLeft = Math.max(0, left);
let clampedRight = Math.min(plot.rect.width, right);
style = { left: clampedLeft, background: color, width: clampedRight - clampedLeft };
className = styles.annoRegion;
}
} else {
isVisible = left > 0 && left <= plot.rect.width;
if (isVisible) {
style = { left, borderBottomColor: color };
className = styles.annoMarker;
}
}
// @TODO: Reset newRange after annotation is saved
if (isVisible) {
let isWip = frame.meta?.custom?.isWip;
markers.push(
<AnnotationMarker2
annoIdx={i}
annoVals={vals}
className={className}
style={style}
timezone={timeZone}
key={`${frameIdx}:${i}`}
exitWipEdit={isWip ? exitWipEdit : null}
/>
);
}
}
return markers;
});
return createPortal(markers, xAxisRef.current!);
}
return null;
};
const getStyles = (theme: GrafanaTheme2) => ({
annoMarker: css({
position: 'absolute',
width: 0,
height: 0,
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderBottomWidth: '6px',
borderBottomStyle: 'solid',
transform: 'translateX(-50%)',
cursor: 'pointer',
zIndex: 1,
}),
annoRegion: css({
position: 'absolute',
height: '5px',
cursor: 'pointer',
zIndex: 1,
}),
});

@ -0,0 +1,160 @@
import { css } from '@emotion/css';
import React, { useContext, useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { AnnotationEventUIModel, GrafanaTheme2 } from '@grafana/data';
import {
Button,
Field,
Form,
HorizontalGroup,
InputControl,
LayoutItemContext,
TextArea,
usePanelContext,
useStyles2,
} from '@grafana/ui';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
import { getAnnotationTags } from 'app/features/annotations/api';
interface Props {
annoVals: Record<string, any[]>;
annoIdx: number;
timeFormatter: (v: number) => string;
dismiss: () => void;
}
interface AnnotationEditFormDTO {
description: string;
tags: string[];
}
export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeFormatter, ...otherProps }: Props) => {
const styles = useStyles2(getStyles);
const panelContext = usePanelContext();
const layoutCtx = useContext(LayoutItemContext);
useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]);
const [createAnnotationState, createAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => {
const result = await panelContext.onAnnotationCreate!(event);
dismiss();
return result;
});
const [updateAnnotationState, updateAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => {
const result = await panelContext.onAnnotationUpdate!(event);
dismiss();
return result;
});
const isUpdatingAnnotation = annoVals.id?.[annoIdx] != null;
const isRegionAnnotation = annoVals.isRegion?.[annoIdx];
const operation = isUpdatingAnnotation ? updateAnnotation : createAnnotation;
const stateIndicator = isUpdatingAnnotation ? updateAnnotationState : createAnnotationState;
const time = isRegionAnnotation
? `${timeFormatter(annoVals.time[annoIdx])} - ${timeFormatter(annoVals.timeEnd[annoIdx])}`
: timeFormatter(annoVals.time[annoIdx]);
const onSubmit = ({ tags, description }: AnnotationEditFormDTO) => {
operation({
id: annoVals.id?.[annoIdx] ?? undefined,
tags,
description,
from: Math.round(annoVals.time[annoIdx]!),
to: Math.round(annoVals.timeEnd?.[annoIdx] ?? annoVals.time[annoIdx]!),
});
};
// Annotation editor
return (
<div className={styles.editor} {...otherProps}>
<div className={styles.header}>
<HorizontalGroup justify={'space-between'} align={'center'}>
<div>{isUpdatingAnnotation ? 'Edit annotation' : 'Add annotation'}</div>
<div>{time}</div>
</HorizontalGroup>
</div>
<Form<AnnotationEditFormDTO>
onSubmit={onSubmit}
defaultValues={{ description: annoVals.text?.[annoIdx], tags: annoVals.tags?.[annoIdx] || [] }}
>
{({ register, errors, control }) => {
return (
<>
<div className={styles.content}>
<Field label={'Description'} invalid={!!errors.description} error={errors?.description?.message}>
<TextArea
className={styles.textarea}
{...register('description', {
required: 'Annotation description is required',
})}
/>
</Field>
<Field label={'Tags'}>
<InputControl
control={control}
name="tags"
render={({ field: { ref, onChange, ...field } }) => {
return (
<TagFilter
allowCustomValue
placeholder="Add tags"
onChange={onChange}
tagOptions={getAnnotationTags}
tags={field.value}
/>
);
}}
/>
</Field>
</div>
<div className={styles.footer}>
<HorizontalGroup justify={'flex-end'}>
<Button size={'sm'} variant="secondary" onClick={dismiss} fill="outline">
Cancel
</Button>
<Button size={'sm'} type={'submit'} disabled={stateIndicator?.loading}>
{stateIndicator?.loading ? 'Saving' : 'Save'}
</Button>
</HorizontalGroup>
</div>
</>
);
}}
</Form>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
editor: css({
// zIndex: theme.zIndex.tooltip,
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,
userSelect: 'text',
width: '460px',
}),
content: css({
padding: theme.spacing(1),
}),
header: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
padding: theme.spacing(0.5, 1),
fontWeight: theme.typography.fontWeightBold,
fontSize: theme.typography.fontSize,
color: theme.colors.text.primary,
}),
footer: css({
borderTop: `1px solid ${theme.colors.border.weak}`,
padding: theme.spacing(1, 1),
}),
textarea: css({
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
}),
};
};

@ -0,0 +1,144 @@
import { css } from '@emotion/css';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import useClickAway from 'react-use/lib/useClickAway';
import { dateTimeFormat, GrafanaTheme2, systemDateFormats } from '@grafana/data';
import { TimeZone } from '@grafana/schema';
import { usePanelContext, useStyles2 } from '@grafana/ui';
import { AnnotationEditor2 } from './AnnotationEditor2';
import { AnnotationTooltip2 } from './AnnotationTooltip2';
interface AnnoBoxProps {
annoVals: Record<string, any[]>;
annoIdx: number;
style: React.CSSProperties | null;
className: string;
timezone: TimeZone;
exitWipEdit?: null | (() => void);
}
const STATE_DEFAULT = 0;
const STATE_EDITING = 1;
const STATE_HOVERED = 2;
export const AnnotationMarker2 = ({ annoVals, annoIdx, className, style, exitWipEdit, timezone }: AnnoBoxProps) => {
const { canEditAnnotations, canDeleteAnnotations, ...panelCtx } = usePanelContext();
const styles = useStyles2(getStyles);
const [state, setState] = useState(STATE_DEFAULT);
const clickAwayRef = useRef(null);
useClickAway(clickAwayRef, () => {
if (state === STATE_EDITING) {
setIsEditingWrap(false);
}
});
const domRef = React.createRef<HTMLDivElement>();
// similar to TooltipPlugin2, when editing annotation (pinned), it should boost z-index
const setIsEditingWrap = useCallback(
(isEditing: boolean) => {
setState(isEditing ? STATE_EDITING : STATE_DEFAULT);
if (!isEditing && exitWipEdit != null) {
exitWipEdit();
}
},
[exitWipEdit]
);
const onAnnotationEdit = useCallback(() => {
setIsEditingWrap(true);
}, [setIsEditingWrap]);
const onAnnotationDelete = useCallback(() => {
if (panelCtx.onAnnotationDelete) {
panelCtx.onAnnotationDelete(annoVals.id?.[annoIdx]);
}
}, [annoIdx, annoVals.id, panelCtx]);
const timeFormatter = useCallback(
(value: number) => {
return dateTimeFormat(value, {
format: systemDateFormats.fullDate,
timeZone: timezone,
});
},
[timezone]
);
useLayoutEffect(
() => {
if (exitWipEdit != null) {
setIsEditingWrap(true);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const renderAnnotationTooltip = useCallback(() => {
let dashboardUID = annoVals.dashboardUID?.[annoIdx];
return (
<AnnotationTooltip2
timeFormatter={timeFormatter}
onEdit={onAnnotationEdit}
onDelete={onAnnotationDelete}
canEdit={canEditAnnotations ? canEditAnnotations(dashboardUID) : false}
canDelete={canDeleteAnnotations ? canDeleteAnnotations(dashboardUID) : false}
annoIdx={annoIdx}
annoVals={annoVals}
/>
);
}, [
timeFormatter,
onAnnotationEdit,
onAnnotationDelete,
canEditAnnotations,
annoVals,
annoIdx,
canDeleteAnnotations,
]);
const renderAnnotationEditor = useCallback(() => {
return (
<AnnotationEditor2
dismiss={() => setIsEditingWrap(false)}
timeFormatter={timeFormatter}
annoIdx={annoIdx}
annoVals={annoVals}
/>
);
}, [annoIdx, annoVals, timeFormatter, setIsEditingWrap]);
return (
<div
ref={domRef}
className={className}
style={style!}
onMouseEnter={() => state !== STATE_EDITING && setState(STATE_HOVERED)}
onMouseLeave={() => state !== STATE_EDITING && setState(STATE_DEFAULT)}
>
<div className={styles.annoInfo} ref={clickAwayRef}>
{state === STATE_HOVERED && renderAnnotationTooltip()}
{state === STATE_EDITING && renderAnnotationEditor()}
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
annoInfo: css({
background: theme.colors.background.secondary,
minWidth: '300px',
// maxWidth: '400px',
position: 'absolute',
top: '5px',
left: '50%',
transform: 'translateX(-50%)',
}),
});

@ -0,0 +1,157 @@
import { css } from '@emotion/css';
import React, { useContext, useEffect } from 'react';
import { GrafanaTheme2, textUtil } from '@grafana/data';
import { HorizontalGroup, IconButton, LayoutItemContext, Tag, useStyles2 } from '@grafana/ui';
import alertDef from 'app/features/alerting/state/alertDef';
interface Props {
annoVals: Record<string, any[]>;
annoIdx: number;
timeFormatter: (v: number) => string;
canEdit: boolean;
canDelete: boolean;
onEdit: () => void;
onDelete: () => void;
}
export const AnnotationTooltip2 = ({
annoVals,
annoIdx,
timeFormatter,
canEdit,
canDelete,
onEdit,
onDelete,
}: Props) => {
const styles = useStyles2(getStyles);
const layoutCtx = useContext(LayoutItemContext);
useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]);
let time = timeFormatter(annoVals.time[annoIdx]);
let text = annoVals.text[annoIdx];
if (annoVals.isRegion?.[annoIdx]) {
time += ' - ' + timeFormatter(annoVals.timeEnd[annoIdx]);
}
let avatar;
if (annoVals.login?.[annoIdx] && annoVals.avatarUrl?.[annoIdx]) {
avatar = <img className={styles.avatar} alt="Annotation avatar" src={annoVals.avatarUrl[annoIdx]} />;
}
let state: React.ReactNode | null = null;
let alertText = '';
if (annoVals.alertId?.[annoIdx] !== undefined && annoVals.newState?.[annoIdx]) {
const stateModel = alertDef.getStateDisplayModel(annoVals.newState[annoIdx]);
state = (
<div className={styles.alertState}>
<i className={stateModel.stateClass}>{stateModel.text}</i>
</div>
);
// alertText = alertDef.getAlertAnnotationInfo(annotation); // @TODO ??
} else if (annoVals.title?.[annoIdx]) {
text = annoVals.title[annoIdx] + '<br />' + (typeof text === 'string' ? text : '');
}
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<HorizontalGroup justify={'space-between'} align={'center'} spacing={'md'}>
<div className={styles.meta}>
<span>
{avatar}
{state}
</span>
{time}
</div>
{(canEdit || canDelete) && (
<div className={styles.editControls}>
{canEdit && <IconButton name={'pen'} size={'sm'} onClick={onEdit} tooltip="Edit" />}
{canDelete && (
<IconButton
name={'trash-alt'}
size={'sm'}
onClick={onDelete}
tooltip="Delete"
disabled={!annoVals.id?.[annoIdx]}
/>
)}
</div>
)}
</HorizontalGroup>
</div>
<div className={styles.body}>
{text && <div className={styles.text} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(text) }} />}
{alertText}
<div>
<HorizontalGroup spacing="xs" wrap>
{annoVals.tags?.[annoIdx]?.map((t: string, i: number) => <Tag name={t} key={`${t}-${i}`} />)}
</HorizontalGroup>
</div>
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
zIndex: theme.zIndex.tooltip,
whiteSpace: 'initial',
borderRadius: theme.shape.radius.default,
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z2,
userSelect: 'text',
}),
header: css({
padding: theme.spacing(0.5, 1),
borderBottom: `1px solid ${theme.colors.border.weak}`,
fontWeight: theme.typography.fontWeightBold,
fontSize: theme.typography.fontSize,
color: theme.colors.text.primary,
display: 'flex',
}),
meta: css({
display: 'flex',
justifyContent: 'space-between',
color: theme.colors.text.primary,
fontWeight: 400,
}),
editControls: css({
display: 'flex',
alignItems: 'center',
'> :last-child': {
marginLeft: 0,
},
}),
body: css({
padding: theme.spacing(1),
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
fontWeight: 400,
a: {
color: theme.colors.text.link,
'&:hover': {
textDecoration: 'underline',
},
},
}),
text: css({
paddingBottom: theme.spacing(1),
}),
avatar: css({
borderRadius: theme.shape.radius.circle,
width: 16,
height: 16,
marginRight: theme.spacing(1),
}),
alertState: css({
paddingRight: theme.spacing(1),
fontWeight: theme.typography.fontWeightMedium,
}),
});

@ -26,6 +26,8 @@ export const TrendPanel = ({
replaceVariables,
id,
}: PanelProps<Options>) => {
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
const { dataLinkPostProcessor } = usePanelContext();
// Need to fallback to first number field if no xField is set in options otherwise panel crashes 😬
const trendXFieldName =
@ -124,7 +126,7 @@ export const TrendPanel = ({
<KeyboardPlugin config={uPlotConfig} />
{options.tooltip.mode !== TooltipDisplayMode.None && (
<>
{config.featureToggles.newVizTooltips ? (
{showNewVizTooltips ? (
<TooltipPlugin2
config={uPlotConfig}
hoverMode={

@ -223,7 +223,7 @@ export const XYChartPanel = (props: Props) => {
<VizLayout width={props.width} height={props.height} legend={renderLegend()}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart config={builder} data={facets} width={vizWidth} height={vizHeight}>
{config.featureToggles.newVizTooltips && props.options.tooltip.mode !== TooltipDisplayMode.None && (
{showNewVizTooltips && props.options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={TooltipHoverMode.xyOne}
@ -245,7 +245,7 @@ export const XYChartPanel = (props: Props) => {
</UPlotChart>
)}
</VizLayout>
{!config.featureToggles.newVizTooltips && (
{!showNewVizTooltips && (
<Portal>
{hover && props.options.tooltip.mode !== TooltipDisplayMode.None && (
<VizTooltipContainer

@ -122,7 +122,7 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss,
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabel()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={getLinks()} canAnnotate={false} />}
{isPinned && <VizTooltipFooter dataLinks={getLinks()} />}
</div>
);
};

@ -535,7 +535,9 @@ const prepConfig = (
// clip hover points/bubbles to plotting area
builder.addHook('init', (u, r) => {
if (!config.featureToggles.newVizTooltips) {
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
if (!showNewVizTooltips) {
u.over.style.overflow = 'hidden';
}
ref_parent = u.root.parentElement;

Loading…
Cancel
Save