mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panels: AnnotationsPlugin2 (#79531)
Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
@@ -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,10 +254,22 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
|
||||
|
||||
// this handles pinning
|
||||
u.over.addEventListener('click', (e) => {
|
||||
// only pinnable tooltip is visible *and* is within proximity to series/point
|
||||
if (_isHovering && closestSeriesIdx != null && !_isPinned && e.target === u.over) {
|
||||
_isPinned = true;
|
||||
scheduleRender(true);
|
||||
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
|
||||
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 && (
|
||||
<AnnotationsPlugin annotations={data.annotations} config={uplotConfig} timeZone={timeZone} />
|
||||
{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,69 +228,89 @@ 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 && (
|
||||
<TooltipPlugin2
|
||||
config={builder}
|
||||
hoverMode={TooltipHoverMode.xyOne}
|
||||
queryZoom={onChangeTimeRange}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
|
||||
return (
|
||||
<HeatmapHoverView
|
||||
mode={options.tooltip.mode}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
dataRef={dataRef}
|
||||
isPinned={isPinned}
|
||||
dismiss={dismiss}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
showColorScale={options.tooltip.showColorScale}
|
||||
canAnnotate={enableAnnotationCreation}
|
||||
panelData={data}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{data.annotations && (
|
||||
<AnnotationsPlugin
|
||||
annotations={data.annotations}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
disableCanvasRendering={true}
|
||||
/>
|
||||
{showNewVizTooltips && (
|
||||
<>
|
||||
{options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<TooltipPlugin2
|
||||
config={builder}
|
||||
hoverMode={TooltipHoverMode.xyOne}
|
||||
queryZoom={onChangeTimeRange}
|
||||
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}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
dataRef={dataRef}
|
||||
isPinned={isPinned}
|
||||
dismiss={dismiss}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
showColorScale={options.tooltip.showColorScale}
|
||||
panelData={data}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<AnnotationsPlugin2
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
newRange={newAnnotationRange}
|
||||
setNewRange={setNewAnnotationRange}
|
||||
canvasRegionRendering={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</VizLayout>
|
||||
{!showNewVizTooltips && (
|
||||
<Portal>
|
||||
{hover && options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<VizTooltipContainer
|
||||
position={{ x: hover.pageX, y: hover.pageY }}
|
||||
offset={{ x: 10, y: 10 }}
|
||||
allowPointerEvents={isToolTipOpen.current}
|
||||
>
|
||||
{shouldDisplayCloseButton && <ExemplarModalHeader onClick={onCloseToolTip} />}
|
||||
<HeatmapHoverViewOld
|
||||
timeRange={timeRange}
|
||||
data={info}
|
||||
hover={hover}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
/>
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</Portal>
|
||||
<>
|
||||
<Portal>
|
||||
{hover && options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<VizTooltipContainer
|
||||
position={{ x: hover.pageX, y: hover.pageY }}
|
||||
offset={{ x: 10, y: 10 }}
|
||||
allowPointerEvents={isToolTipOpen.current}
|
||||
>
|
||||
{shouldDisplayCloseButton && <ExemplarModalHeader onClick={onCloseToolTip} />}
|
||||
<HeatmapHoverViewOld
|
||||
timeRange={timeRange}
|
||||
data={info}
|
||||
hover={hover}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
/>
|
||||
</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 && (
|
||||
<AnnotationsPlugin annotations={data.annotations} config={uplotConfig} timeZone={timeZone} />
|
||||
{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;
|
||||
|
||||
Reference in New Issue
Block a user