VizTooltip: Replace ExemplarHoverView with VizTooltip components (#109369)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
co-authored by
Leon Sorokin
parent
cff39476f3
commit
238961d3ea
@@ -1,9 +1,9 @@
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLayoutEffect, useMemo, useRef, useState, ReactNode } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useMountedState } from 'react-use';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { DataFrame, DataFrameFieldIndex } from '@grafana/data';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
|
||||
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
||||
|
||||
@@ -14,11 +14,8 @@ interface EventsCanvasProps {
|
||||
id: string;
|
||||
config: UPlotConfigBuilder;
|
||||
events: DataFrame[];
|
||||
renderEventMarker: (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => React.ReactNode;
|
||||
mapEventToXYCoords: (
|
||||
dataFrame: DataFrame,
|
||||
dataFrameFieldIndex: DataFrameFieldIndex
|
||||
) => { x: number; y: number } | undefined;
|
||||
renderEventMarker: (dataFrame: DataFrame, rowIndex: number) => ReactNode;
|
||||
mapEventToXYCoords: (dataFrame: DataFrame, rowIndex: number) => { x: number; y: number } | undefined;
|
||||
}
|
||||
|
||||
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) {
|
||||
@@ -50,14 +47,15 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
|
||||
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const frame = events[i];
|
||||
|
||||
for (let j = 0; j < frame.length; j++) {
|
||||
const coords = mapEventToXYCoords(frame, { fieldIndex: j, frameIndex: i });
|
||||
const coords = mapEventToXYCoords(frame, j);
|
||||
if (!coords) {
|
||||
continue;
|
||||
}
|
||||
markers.push(
|
||||
<Marker {...coords} key={`${id}-marker-${i}-${j}`}>
|
||||
{renderEventMarker(frame, { fieldIndex: j, frameIndex: i })}
|
||||
{renderEventMarker(frame, j)}
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { ArrayDataFrame } from '@grafana/data';
|
||||
import { arrayToDataFrame } from '@grafana/data';
|
||||
|
||||
import { DataHoverView } from './DataHoverView';
|
||||
|
||||
describe('DataHoverView component', () => {
|
||||
it('should default to multi mode if mode is null or undefined', () => {
|
||||
const data = new ArrayDataFrame([{ foo: 'bar' }]);
|
||||
const data = arrayToDataFrame([{ foo: 'bar' }]);
|
||||
render(<DataHoverView data={data} rowIndex={0} />);
|
||||
|
||||
expect(screen.queryByText('bar')).toBeInTheDocument();
|
||||
|
||||
@@ -1,116 +1,61 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import {
|
||||
arrayUtils,
|
||||
DataFrame,
|
||||
Field,
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
GrafanaTheme2,
|
||||
LinkModel,
|
||||
} from '@grafana/data';
|
||||
import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
|
||||
import { TextLink, useStyles2 } from '@grafana/ui';
|
||||
import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils';
|
||||
|
||||
import { ExemplarHoverView } from './ExemplarHoverView';
|
||||
import { getDataLinks } from 'app/plugins/panel/status-history/utils';
|
||||
|
||||
export interface Props {
|
||||
data?: DataFrame; // source data
|
||||
rowIndex?: number | null; // the hover row
|
||||
columnIndex?: number | null; // the hover column
|
||||
sortOrder?: SortOrder;
|
||||
mode?: TooltipDisplayMode | null;
|
||||
header?: string;
|
||||
padding?: number;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export interface DisplayValue {
|
||||
name: string;
|
||||
value: unknown;
|
||||
valueString: string;
|
||||
highlight: boolean;
|
||||
}
|
||||
|
||||
export function getDisplayValuesAndLinks(
|
||||
data: DataFrame,
|
||||
rowIndex: number,
|
||||
columnIndex?: number | null,
|
||||
sortOrder?: SortOrder,
|
||||
mode?: TooltipDisplayMode | null
|
||||
) {
|
||||
const fields = data.fields;
|
||||
const hoveredField = columnIndex != null ? fields[columnIndex] : null;
|
||||
export function getDisplayValuesAndLinks(data: DataFrame, rowIndex: number, columnIndex?: number) {
|
||||
const visibleFields = data.fields.filter(
|
||||
(f, i) => !Boolean(f.config.custom?.hideFrom?.tooltip) && (columnIndex == null || i === columnIndex)
|
||||
);
|
||||
|
||||
const visibleFields = fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
|
||||
const traceIDField = visibleFields.find((field) => field.name === 'traceID') || fields[0];
|
||||
const orderedVisibleFields = [];
|
||||
// Only include traceID if it's visible and put it in front.
|
||||
if (visibleFields.filter((field) => traceIDField === field).length > 0) {
|
||||
orderedVisibleFields.push(traceIDField);
|
||||
}
|
||||
orderedVisibleFields.push(...visibleFields.filter((field) => traceIDField !== field));
|
||||
|
||||
if (orderedVisibleFields.length === 0) {
|
||||
if (visibleFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayValues: DisplayValue[] = [];
|
||||
const links: Array<LinkModel<Field>> = [];
|
||||
const linkLookup = new Set<string>();
|
||||
|
||||
for (const field of orderedVisibleFields) {
|
||||
if (mode === TooltipDisplayMode.Single && field !== hoveredField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const field of visibleFields) {
|
||||
const value = field.values[rowIndex];
|
||||
const fieldDisplay = field.display ? field.display(value) : { text: `${value}`, numeric: +value };
|
||||
|
||||
if (field.getLinks) {
|
||||
field.getLinks({ calculatedValue: fieldDisplay, valueRowIndex: rowIndex }).forEach((link) => {
|
||||
const key = `${link.title}/${link.href}`;
|
||||
if (!linkLookup.has(key)) {
|
||||
links.push(link);
|
||||
linkLookup.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
links.push(...getDataLinks(field, rowIndex));
|
||||
|
||||
displayValues.push({
|
||||
name: getFieldDisplayName(field, data),
|
||||
value,
|
||||
valueString: formattedValueToString(fieldDisplay),
|
||||
highlight: field === hoveredField,
|
||||
});
|
||||
}
|
||||
|
||||
if (sortOrder && sortOrder !== SortOrder.None) {
|
||||
displayValues.sort((a, b) => arrayUtils.sortValues(sortOrder)(a.value, b.value));
|
||||
}
|
||||
|
||||
return { displayValues, links };
|
||||
}
|
||||
|
||||
export const DataHoverView = ({
|
||||
data,
|
||||
rowIndex,
|
||||
columnIndex,
|
||||
sortOrder,
|
||||
mode,
|
||||
header,
|
||||
padding = 0,
|
||||
maxHeight,
|
||||
}: Props) => {
|
||||
export const DataHoverView = ({ data, rowIndex, header, padding = 0 }: Props) => {
|
||||
const styles = useStyles2(getStyles, padding);
|
||||
|
||||
if (!data || rowIndex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dispValuesAndLinks = getDisplayValuesAndLinks(data, rowIndex, columnIndex, sortOrder, mode);
|
||||
const dispValuesAndLinks = getDisplayValuesAndLinks(data, rowIndex);
|
||||
|
||||
if (dispValuesAndLinks == null) {
|
||||
return null;
|
||||
@@ -118,10 +63,6 @@ export const DataHoverView = ({
|
||||
|
||||
const { displayValues, links } = dispValuesAndLinks;
|
||||
|
||||
if (header === 'Exemplar') {
|
||||
return <ExemplarHoverView displayValues={displayValues} links={links} header={header} maxHeight={maxHeight} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{header && (
|
||||
@@ -154,6 +95,7 @@ export const DataHoverView = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, padding = 0) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { DataLinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { VizTooltipRow } from '@grafana/ui/internal';
|
||||
import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils';
|
||||
|
||||
import { DisplayValue } from './DataHoverView';
|
||||
|
||||
export interface Props {
|
||||
displayValues: DisplayValue[];
|
||||
links?: LinkModel[];
|
||||
header?: string;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar', maxHeight }: Props) => {
|
||||
const styles = useStyles2(getStyles, 0, maxHeight);
|
||||
|
||||
const time = displayValues.find((val) => val.name === 'Time');
|
||||
displayValues = displayValues.filter((val) => val.name !== 'Time'); // time?
|
||||
|
||||
return (
|
||||
<div className={styles.exemplarWrapper}>
|
||||
<div className={styles.exemplarHeader}>
|
||||
<span className={styles.title}>{header}</span>
|
||||
{time && <span className={styles.time}>{renderValue(time.valueString)}</span>}
|
||||
</div>
|
||||
<div className={styles.exemplarContent}>
|
||||
{displayValues.map((displayValue, i) => {
|
||||
return (
|
||||
<VizTooltipRow
|
||||
key={i}
|
||||
label={displayValue.name}
|
||||
value={renderValue(displayValue.valueString)}
|
||||
justify={'space-between'}
|
||||
isPinned={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{links && links.length > 0 && (
|
||||
<div className={styles.exemplarFooter}>
|
||||
{links.map((link, i) => (
|
||||
<DataLinkButton link={link} key={i} buttonProps={{ size: 'md' }} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, padding = 0, maxHeight?: number) => {
|
||||
return {
|
||||
exemplarWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
whiteSpace: 'pre',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
background: theme.colors.background.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
exemplarHeader: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: theme.spacing(0.5),
|
||||
color: theme.colors.text.secondary,
|
||||
padding: theme.spacing(1),
|
||||
}),
|
||||
time: css({
|
||||
color: theme.colors.text.primary,
|
||||
}),
|
||||
exemplarContent: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
borderTop: `1px solid ${theme.colors.border.medium}`,
|
||||
padding: theme.spacing(1),
|
||||
overflowY: 'auto',
|
||||
maxHeight: maxHeight,
|
||||
}),
|
||||
exemplarFooter: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(1),
|
||||
borderTop: `1px solid ${theme.colors.border.medium}`,
|
||||
gap: 4,
|
||||
}),
|
||||
linkButton: css({
|
||||
width: 'fit-content',
|
||||
}),
|
||||
label: css({
|
||||
color: theme.colors.text.secondary,
|
||||
fontWeight: 400,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
marginRight: theme.spacing(0.5),
|
||||
}),
|
||||
value: css({
|
||||
fontWeight: 500,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
title: css({
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
flexGrow: 1,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { LinkModel } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import {
|
||||
VizTooltipContent,
|
||||
VizTooltipFooter,
|
||||
VizTooltipItem,
|
||||
VizTooltipHeader,
|
||||
VizTooltipWrapper,
|
||||
} from '@grafana/ui/internal';
|
||||
|
||||
export interface Props {
|
||||
items: VizTooltipItem[];
|
||||
links?: LinkModel[];
|
||||
isPinned: boolean;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export const ExemplarTooltip = ({ items, links, isPinned, maxHeight }: Props) => {
|
||||
const timeItem = items.find((val) => val.label === 'Time');
|
||||
|
||||
return (
|
||||
<VizTooltipWrapper>
|
||||
<VizTooltipHeader
|
||||
item={{
|
||||
label: t('exemplar-tooltip-header', 'Exemplar'),
|
||||
value: timeItem?.value ?? '',
|
||||
}}
|
||||
isPinned={isPinned}
|
||||
/>
|
||||
<VizTooltipContent
|
||||
items={items.filter((item) => item !== timeItem)}
|
||||
isPinned={isPinned}
|
||||
maxHeight={maxHeight}
|
||||
scrollable={maxHeight != null}
|
||||
/>
|
||||
<VizTooltipFooter dataLinks={links ?? []} />
|
||||
</VizTooltipWrapper>
|
||||
);
|
||||
};
|
||||
@@ -324,7 +324,13 @@ export const CandlestickPanel = ({
|
||||
/>
|
||||
<OutsideRangePlugin config={uplotConfig} onChangeTimeRange={onChangeTimeRange} />
|
||||
{data.annotations && (
|
||||
<ExemplarsPlugin config={uplotConfig} exemplars={data.annotations} timeZone={timeZone} />
|
||||
<ExemplarsPlugin
|
||||
config={uplotConfig}
|
||||
exemplars={data.annotations}
|
||||
timeZone={timeZone}
|
||||
maxHeight={options.tooltip.maxHeight}
|
||||
maxWidth={options.tooltip.maxWidth}
|
||||
/>
|
||||
)}
|
||||
{((canEditThresholds && onThresholdsChange) || showThresholds) && (
|
||||
<ThresholdControlsPlugin
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
|
||||
export function ExemplarModalHeader(props: { onClick: () => void; style?: React.CSSProperties }) {
|
||||
const defaultStyle: CSSProperties = {
|
||||
position: 'relative',
|
||||
top: 'auto',
|
||||
right: 'auto',
|
||||
marginRight: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
>
|
||||
<CloseButton onClick={props.onClick} style={props.style ?? defaultStyle} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
||||
import { getDisplayValuesAndLinks } from 'app/features/visualization/data-hover/DataHoverView';
|
||||
import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip';
|
||||
|
||||
import { getDataLinks, getFieldActions } from '../status-history/utils';
|
||||
import { isTooltipScrollable } from '../timeseries/utils';
|
||||
@@ -54,13 +55,23 @@ interface HeatmapTooltipProps {
|
||||
|
||||
export const HeatmapTooltip = (props: HeatmapTooltipProps) => {
|
||||
if (props.seriesIdx === 2) {
|
||||
const dispValuesAndLinks = getDisplayValuesAndLinks(props.dataRef.current!.exemplars!, props.dataIdxs[2]!);
|
||||
|
||||
if (dispValuesAndLinks == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { displayValues, links } = dispValuesAndLinks;
|
||||
|
||||
return (
|
||||
<DataHoverView
|
||||
data={props.dataRef.current!.exemplars}
|
||||
rowIndex={props.dataIdxs[2]}
|
||||
header={'Exemplar'}
|
||||
padding={8}
|
||||
<ExemplarTooltip
|
||||
items={displayValues.map((dispVal) => ({
|
||||
label: dispVal.name,
|
||||
value: dispVal.valueString,
|
||||
}))}
|
||||
links={links}
|
||||
maxHeight={props.maxHeight}
|
||||
isPinned={props.isPinned}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ export const TimeSeriesPanel = ({
|
||||
exemplars={data.annotations}
|
||||
timeZone={timeZone}
|
||||
maxHeight={options.tooltip.maxHeight}
|
||||
maxWidth={options.tooltip.maxWidth}
|
||||
/>
|
||||
)}
|
||||
{((canEditThresholds && onThresholdsChange) || showThresholds) && (
|
||||
|
||||
@@ -9,40 +9,45 @@ import {
|
||||
useHover,
|
||||
useInteractions,
|
||||
} from '@floating-ui/react';
|
||||
import { CSSProperties, useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DataFrame, DataFrameFieldIndex, Field, formattedValueToString, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { DataFrame, Field, formattedValueToString, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui';
|
||||
import { DisplayValue } from 'app/features/visualization/data-hover/DataHoverView';
|
||||
import { ExemplarHoverView } from 'app/features/visualization/data-hover/ExemplarHoverView';
|
||||
import { VizTooltipItem } from '@grafana/ui/internal';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip';
|
||||
|
||||
import { ExemplarModalHeader } from '../../heatmap/ExemplarModalHeader';
|
||||
import { getDataLinks } from '../../status-history/utils';
|
||||
|
||||
interface ExemplarMarkerProps {
|
||||
timeZone: TimeZone;
|
||||
dataFrame: DataFrame;
|
||||
dataFrameFieldIndex: DataFrameFieldIndex;
|
||||
frameIndex: number;
|
||||
rowIndex: number;
|
||||
config: UPlotConfigBuilder;
|
||||
exemplarColor?: string;
|
||||
clickedExemplarFieldIndex: DataFrameFieldIndex | undefined;
|
||||
setClickedExemplarFieldIndex: React.Dispatch<DataFrameFieldIndex | undefined>;
|
||||
clickedRowIndex: number | undefined;
|
||||
setClickedRowIndex: React.Dispatch<number | undefined>;
|
||||
maxHeight?: number;
|
||||
maxWidth?: number;
|
||||
}
|
||||
|
||||
export const ExemplarMarker = ({
|
||||
timeZone,
|
||||
dataFrame,
|
||||
dataFrameFieldIndex,
|
||||
frameIndex,
|
||||
rowIndex,
|
||||
config,
|
||||
exemplarColor,
|
||||
clickedExemplarFieldIndex,
|
||||
setClickedExemplarFieldIndex,
|
||||
clickedRowIndex,
|
||||
setClickedRowIndex,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
}: ExemplarMarkerProps) => {
|
||||
const styles = useStyles2(getExemplarMarkerStyles);
|
||||
const styles = useStyles2(getExemplarMarkerStyles, maxWidth);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
|
||||
@@ -69,24 +74,19 @@ export const ExemplarMarker = ({
|
||||
const dismiss = useDismiss(context);
|
||||
const hover = useHover(context, {
|
||||
handleClose: safePolygon(),
|
||||
enabled: clickedExemplarFieldIndex === undefined,
|
||||
enabled: clickedRowIndex === undefined,
|
||||
});
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, hover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!(
|
||||
clickedExemplarFieldIndex?.fieldIndex === dataFrameFieldIndex.fieldIndex &&
|
||||
clickedExemplarFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex
|
||||
)
|
||||
) {
|
||||
if (clickedRowIndex !== rowIndex) {
|
||||
setIsLocked(false);
|
||||
}
|
||||
}, [clickedExemplarFieldIndex, dataFrameFieldIndex]);
|
||||
}, [clickedRowIndex, rowIndex]);
|
||||
|
||||
const getSymbol = () => {
|
||||
const symbols = [
|
||||
return (
|
||||
<rect
|
||||
fill={exemplarColor}
|
||||
key="diamond"
|
||||
@@ -94,27 +94,38 @@ export const ExemplarMarker = ({
|
||||
width="4.78985"
|
||||
height="4.78985"
|
||||
transform="rotate(45 3.38672 0)"
|
||||
/>,
|
||||
<path
|
||||
fill={exemplarColor}
|
||||
key="x"
|
||||
d="M1.94444 3.49988L0 5.44432L1.55552 6.99984L3.49996 5.05539L5.4444 6.99983L6.99992 5.44431L5.05548 3.49988L6.99983 1.55552L5.44431 0L3.49996 1.94436L1.5556 0L8.42584e-05 1.55552L1.94444 3.49988Z"
|
||||
/>,
|
||||
<path fill={exemplarColor} key="triangle" d="M4 0L7.4641 6H0.535898L4 0Z" />,
|
||||
<rect fill={exemplarColor} key="rectangle" width="5" height="5" />,
|
||||
<path
|
||||
fill={exemplarColor}
|
||||
key="pentagon"
|
||||
d="M3 0.5L5.85317 2.57295L4.76336 5.92705H1.23664L0.146831 2.57295L3 0.5Z"
|
||||
/>,
|
||||
<path
|
||||
fill={exemplarColor}
|
||||
key="plus"
|
||||
d="m2.35672,4.2425l0,2.357l1.88558,0l0,-2.357l2.3572,0l0,-1.88558l-2.3572,0l0,-2.35692l-1.88558,0l0,2.35692l-2.35672,0l0,1.88558l2.35672,0z"
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
);
|
||||
|
||||
return symbols[dataFrameFieldIndex.frameIndex % symbols.length];
|
||||
// const symbols = [
|
||||
// <rect
|
||||
// fill={exemplarColor}
|
||||
// key="diamond"
|
||||
// x="3.38672"
|
||||
// width="4.78985"
|
||||
// height="4.78985"
|
||||
// transform="rotate(45 3.38672 0)"
|
||||
// />,
|
||||
// <path
|
||||
// fill={exemplarColor}
|
||||
// key="x"
|
||||
// d="M1.94444 3.49988L0 5.44432L1.55552 6.99984L3.49996 5.05539L5.4444 6.99983L6.99992 5.44431L5.05548 3.49988L6.99983 1.55552L5.44431 0L3.49996 1.94436L1.5556 0L8.42584e-05 1.55552L1.94444 3.49988Z"
|
||||
// />,
|
||||
// <path fill={exemplarColor} key="triangle" d="M4 0L7.4641 6H0.535898L4 0Z" />,
|
||||
// <rect fill={exemplarColor} key="rectangle" width="5" height="5" />,
|
||||
// <path
|
||||
// fill={exemplarColor}
|
||||
// key="pentagon"
|
||||
// d="M3 0.5L5.85317 2.57295L4.76336 5.92705H1.23664L0.146831 2.57295L3 0.5Z"
|
||||
// />,
|
||||
// <path
|
||||
// fill={exemplarColor}
|
||||
// key="plus"
|
||||
// d="m2.35672,4.2425l0,2.357l1.88558,0l0,-2.357l2.3572,0l0,-1.88558l-2.3572,0l0,-2.35692l-1.88558,0l0,2.35692l-2.35672,0l0,1.88558l2.35672,0z"
|
||||
// />,
|
||||
// ];
|
||||
|
||||
// return symbols[dataFrameFieldIndex.frameIndex % symbols.length];
|
||||
};
|
||||
|
||||
const lockExemplarModal = () => {
|
||||
@@ -122,70 +133,56 @@ export const ExemplarMarker = ({
|
||||
};
|
||||
|
||||
const renderMarker = useCallback(() => {
|
||||
//Put fields with links on the top
|
||||
const fieldsWithLinks =
|
||||
dataFrame.fields.filter((field) => field.config.links?.length && field.config.links?.length > 0) || [];
|
||||
const orderedDataFrameFields = [
|
||||
...fieldsWithLinks,
|
||||
...dataFrame.fields.filter((field) => !fieldsWithLinks.includes(field)),
|
||||
];
|
||||
|
||||
const onClose = () => {
|
||||
setIsLocked(false);
|
||||
setIsOpen(false);
|
||||
setClickedExemplarFieldIndex(undefined);
|
||||
setClickedRowIndex(undefined);
|
||||
};
|
||||
|
||||
let displayValues: DisplayValue[] = [];
|
||||
let links: LinkModel[] | undefined = [];
|
||||
orderedDataFrameFields.map((field: Field, i) => {
|
||||
const value = field.values[dataFrameFieldIndex.fieldIndex];
|
||||
let items: VizTooltipItem[] = [];
|
||||
let links: LinkModel[] = [];
|
||||
|
||||
if (field.config.links?.length) {
|
||||
links?.push(...(field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex }) || []));
|
||||
}
|
||||
dataFrame.fields.forEach((field: Field) => {
|
||||
const value = field.values[rowIndex];
|
||||
|
||||
const fieldDisplay = field.display ? field.display(value) : { text: `${value}`, numeric: +value };
|
||||
links.push(...getDataLinks(field, rowIndex));
|
||||
|
||||
displayValues.push({
|
||||
name: field.name,
|
||||
value,
|
||||
valueString: formattedValueToString(fieldDisplay),
|
||||
highlight: false,
|
||||
const fieldDisplay = field.display?.(value) ?? { text: `${value}`, numeric: +value };
|
||||
|
||||
items.push({
|
||||
label: field.state?.displayName ?? field.name,
|
||||
value: formattedValueToString(fieldDisplay),
|
||||
});
|
||||
});
|
||||
|
||||
const exemplarHeaderCustomStyle: CSSProperties = {
|
||||
position: 'relative',
|
||||
top: '35px',
|
||||
right: '5px',
|
||||
marginRight: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.tooltip} ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}>
|
||||
{isLocked && <ExemplarModalHeader onClick={onClose} style={exemplarHeaderCustomStyle} />}
|
||||
<ExemplarHoverView displayValues={displayValues} links={links} maxHeight={maxHeight} />
|
||||
<div
|
||||
className={cx(styles.tooltipWrapper, isLocked && styles.pinned)}
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{isLocked && <CloseButton onClick={onClose} />}
|
||||
<ExemplarTooltip items={items} links={links} isPinned={isLocked} maxHeight={maxHeight} />
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
dataFrame.fields,
|
||||
dataFrameFieldIndex,
|
||||
rowIndex,
|
||||
styles,
|
||||
isLocked,
|
||||
setClickedExemplarFieldIndex,
|
||||
setClickedRowIndex,
|
||||
floatingStyles,
|
||||
getFloatingProps,
|
||||
refs.setFloating,
|
||||
maxHeight,
|
||||
]);
|
||||
|
||||
const seriesColor = config
|
||||
.getSeries()
|
||||
.find((s) => s.props.dataFrameFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex)?.props.lineColor;
|
||||
const seriesColor = config.getSeries().find((s) => s.props.dataFrameFieldIndex?.frameIndex === frameIndex)
|
||||
?.props.lineColor;
|
||||
|
||||
const onExemplarClick = () => {
|
||||
setClickedExemplarFieldIndex(dataFrameFieldIndex);
|
||||
setClickedRowIndex(rowIndex);
|
||||
lockExemplarModal();
|
||||
};
|
||||
|
||||
@@ -220,12 +217,7 @@ export const ExemplarMarker = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getExemplarMarkerStyles = (theme: GrafanaTheme2) => {
|
||||
const bg = theme.isDark ? theme.v1.palette.dark2 : theme.v1.palette.white;
|
||||
const headerBg = theme.isDark ? theme.v1.palette.dark9 : theme.v1.palette.gray5;
|
||||
const shadowColor = theme.isDark ? theme.v1.palette.black : theme.v1.palette.white;
|
||||
const tableBgOdd = theme.isDark ? theme.v1.palette.dark3 : theme.v1.palette.gray6;
|
||||
|
||||
const getExemplarMarkerStyles = (theme: GrafanaTheme2, maxWidth: number | undefined) => {
|
||||
return {
|
||||
markerWrapper: css({
|
||||
padding: '0 4px 4px 4px',
|
||||
@@ -249,66 +241,6 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => {
|
||||
borderBottom: `4px solid ${theme.v1.palette.red}`,
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
wrapper: css({
|
||||
background: bg,
|
||||
border: `1px solid ${headerBg}`,
|
||||
borderRadius: theme.shape.borderRadius(2),
|
||||
boxShadow: `0 0 20px ${shadowColor}`,
|
||||
padding: theme.spacing(1),
|
||||
}),
|
||||
exemplarsTable: css({
|
||||
width: '100%',
|
||||
'tr td': {
|
||||
padding: '5px 10px',
|
||||
whiteSpace: 'nowrap',
|
||||
borderBottom: `4px solid ${theme.components.panel.background}`,
|
||||
},
|
||||
tr: {
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
'&:nth-child(even)': {
|
||||
backgroundColor: tableBgOdd,
|
||||
},
|
||||
},
|
||||
}),
|
||||
valueWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
columnGap: theme.spacing(1),
|
||||
'> span': {
|
||||
flexGrow: 0,
|
||||
},
|
||||
'> *': {
|
||||
flex: '1 1',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
}),
|
||||
tooltip: css({
|
||||
background: 'none',
|
||||
padding: 0,
|
||||
overflowY: 'auto',
|
||||
maxHeight: '95vh',
|
||||
boxShadow: theme.shadows.z2,
|
||||
}),
|
||||
header: css({
|
||||
background: headerBg,
|
||||
padding: '6px 10px',
|
||||
display: 'flex',
|
||||
}),
|
||||
title: css({
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
paddingRight: theme.spacing(2),
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
flexGrow: 1,
|
||||
}),
|
||||
body: css({
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
borderRadius: theme.shape.borderRadius(2),
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
marble: css({
|
||||
display: 'block',
|
||||
opacity: 0.5,
|
||||
@@ -321,5 +253,18 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => {
|
||||
opacity: 1,
|
||||
filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.5))',
|
||||
}),
|
||||
tooltipWrapper: css({
|
||||
background: theme.colors.background.elevated,
|
||||
maxWidth: maxWidth ?? 'none',
|
||||
whiteSpace: 'pre',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
position: 'fixed',
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
boxShadow: theme.shadows.z2,
|
||||
userSelect: 'text',
|
||||
}),
|
||||
pinned: css({
|
||||
boxShadow: theme.shadows.z3,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
DataFrameFieldIndex,
|
||||
Labels,
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
TIME_SERIES_VALUE_FIELD_NAME,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { DataFrame, Labels, TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, TimeZone } from '@grafana/data';
|
||||
import { FIXED_UNIT, EventsCanvas, UPlotConfigBuilder } from '@grafana/ui';
|
||||
|
||||
import { ExemplarMarker } from './ExemplarMarker';
|
||||
@@ -19,12 +12,20 @@ interface ExemplarsPluginProps {
|
||||
timeZone: TimeZone;
|
||||
visibleSeries?: VisibleExemplarLabels;
|
||||
maxHeight?: number;
|
||||
maxWidth?: number;
|
||||
}
|
||||
|
||||
export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, maxHeight }: ExemplarsPluginProps) => {
|
||||
export const ExemplarsPlugin = ({
|
||||
exemplars,
|
||||
timeZone,
|
||||
config,
|
||||
visibleSeries,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
}: ExemplarsPluginProps) => {
|
||||
const plotInstance = useRef<uPlot>();
|
||||
|
||||
const [lockedExemplarFieldIndex, setLockedExemplarFieldIndex] = useState<DataFrameFieldIndex | undefined>();
|
||||
const [lockedExemplarRowIndex, setLockedExemplarRowIndex] = useState<number | undefined>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
config.addHook('init', (u) => {
|
||||
@@ -32,7 +33,7 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, rowIndex: number) => {
|
||||
const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||
|
||||
@@ -47,7 +48,7 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma
|
||||
const yMin = plotInstance.current.scales[yScale].min;
|
||||
const yMax = plotInstance.current.scales[yScale].max;
|
||||
|
||||
let y = value.values[dataFrameFieldIndex.fieldIndex];
|
||||
let y = value.values[rowIndex];
|
||||
// To not to show exemplars outside of the graph we set the y value to min if it is smaller and max if it is bigger than the size of the graph
|
||||
if (yMin != null && y < yMin) {
|
||||
y = yMin;
|
||||
@@ -58,18 +59,17 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma
|
||||
}
|
||||
|
||||
return {
|
||||
x: plotInstance.current.valToPos(time.values[dataFrameFieldIndex.fieldIndex], 'x'),
|
||||
x: plotInstance.current.valToPos(time.values[rowIndex], 'x'),
|
||||
y: plotInstance.current.valToPos(y, yScale),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderMarker = useCallback(
|
||||
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
const showMarker =
|
||||
visibleSeries !== undefined ? showExemplarMarker(visibleSeries, dataFrame, dataFrameFieldIndex) : true;
|
||||
(dataFrame: DataFrame, rowIndex: number): ReactNode => {
|
||||
const showMarker = visibleSeries !== undefined ? showExemplarMarker(visibleSeries, dataFrame, rowIndex) : true;
|
||||
|
||||
const markerColor =
|
||||
visibleSeries !== undefined ? getExemplarColor(dataFrame, dataFrameFieldIndex, visibleSeries) : undefined;
|
||||
visibleSeries !== undefined ? getExemplarColor(dataFrame, rowIndex, visibleSeries) : undefined;
|
||||
|
||||
if (!showMarker) {
|
||||
return <></>;
|
||||
@@ -77,18 +77,20 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma
|
||||
|
||||
return (
|
||||
<ExemplarMarker
|
||||
setClickedExemplarFieldIndex={setLockedExemplarFieldIndex}
|
||||
clickedExemplarFieldIndex={lockedExemplarFieldIndex}
|
||||
setClickedRowIndex={setLockedExemplarRowIndex}
|
||||
clickedRowIndex={lockedExemplarRowIndex}
|
||||
timeZone={timeZone}
|
||||
dataFrame={dataFrame}
|
||||
dataFrameFieldIndex={dataFrameFieldIndex}
|
||||
frameIndex={0}
|
||||
rowIndex={rowIndex}
|
||||
config={config}
|
||||
exemplarColor={markerColor}
|
||||
maxHeight={maxHeight}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[config, timeZone, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex, maxHeight]
|
||||
[visibleSeries, lockedExemplarRowIndex, timeZone, config, maxHeight, maxWidth]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -138,11 +140,7 @@ interface LabelWithExemplarUIData {
|
||||
/**
|
||||
* Get color of active series in legend
|
||||
*/
|
||||
const getExemplarColor = (
|
||||
dataFrame: DataFrame,
|
||||
dataFrameFieldIndex: DataFrameFieldIndex,
|
||||
visibleLabels: VisibleExemplarLabels
|
||||
) => {
|
||||
const getExemplarColor = (dataFrame: DataFrame, rowIndex: number, visibleLabels: VisibleExemplarLabels) => {
|
||||
let exemplarColor;
|
||||
visibleLabels.labels.some((visibleLabel) => {
|
||||
const labelKeys = Object.keys(visibleLabel.labels);
|
||||
@@ -151,7 +149,7 @@ const getExemplarColor = (
|
||||
});
|
||||
if (fields.length) {
|
||||
const hasMatch = fields.every((field, index, fields) => {
|
||||
const value = field.values[dataFrameFieldIndex.fieldIndex];
|
||||
const value = field.values[rowIndex];
|
||||
return visibleLabel.labels[field.name] === value;
|
||||
});
|
||||
|
||||
@@ -168,11 +166,7 @@ const getExemplarColor = (
|
||||
/**
|
||||
* Determine if the current exemplar marker is filtered by what series are selected in the legend UI
|
||||
*/
|
||||
const showExemplarMarker = (
|
||||
visibleSeries: VisibleExemplarLabels,
|
||||
dataFrame: DataFrame,
|
||||
dataFrameFieldIndex: DataFrameFieldIndex
|
||||
) => {
|
||||
const showExemplarMarker = (visibleSeries: VisibleExemplarLabels, dataFrame: DataFrame, rowIndex: number) => {
|
||||
let showMarker = false;
|
||||
// If all series are visible, don't filter any exemplars
|
||||
if (visibleSeries.labels.length === visibleSeries.totalSeriesCount) {
|
||||
@@ -196,7 +190,7 @@ const showExemplarMarker = (
|
||||
showMarker = visibleSeries.labels.some((series) => {
|
||||
return Object.keys(series.labels).every((label) => {
|
||||
const value = series.labels[label];
|
||||
return fields.find((field) => field.values[dataFrameFieldIndex.fieldIndex] === value);
|
||||
return fields.find((field) => field.values[rowIndex] === value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6806,6 +6806,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exemplar-tooltip-header": "Exemplar",
|
||||
"explore": {
|
||||
"accordian-logs": {
|
||||
"events": "Events",
|
||||
|
||||
Reference in New Issue
Block a user