From 070e41d1368ef1fc41459fa34f8b3158757aa3ee Mon Sep 17 00:00:00 2001
From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com>
Date: Wed, 3 Jan 2024 06:27:34 -0600
Subject: [PATCH] Exemplars: Update UX to match new tooltips (#79916)
---
.betterer.results | 21 --
.../data-hover/DataHoverView.tsx | 84 ++---
.../data-hover/ExemplarHoverView.tsx | 111 +++++++
.../panel/heatmap/ExemplarModalHeader.tsx | 23 +-
.../panel/heatmap/HeatmapHoverView.tsx | 18 +-
.../timeseries/plugins/ExemplarMarker.tsx | 298 ++++++++++--------
6 files changed, 345 insertions(+), 210 deletions(-)
create mode 100644 public/app/features/visualization/data-hover/ExemplarHoverView.tsx
diff --git a/.betterer.results b/.betterer.results
index 4f8d38ab988..caec6e2ab30 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -4791,14 +4791,6 @@ exports[`better eslint`] = {
"public/app/features/visualization/data-hover/DataHoverRows.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
- "public/app/features/visualization/data-hover/DataHoverView.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"],
- [0, 0, 0, "Styles should be written using objects.", "5"]
- ],
"public/app/plugins/datasource/alertmanager/DataSource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -6514,19 +6506,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
],
- "public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"],
- [0, 0, 0, "Styles should be written using objects.", "5"],
- [0, 0, 0, "Styles should be written using objects.", "6"],
- [0, 0, 0, "Styles should be written using objects.", "7"],
- [0, 0, 0, "Styles should be written using objects.", "8"],
- [0, 0, 0, "Styles should be written using objects.", "9"],
- [0, 0, 0, "Styles should be written using objects.", "10"]
- ],
"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"],
diff --git a/public/app/features/visualization/data-hover/DataHoverView.tsx b/public/app/features/visualization/data-hover/DataHoverView.tsx
index 7a212631f1e..c1a346a4bee 100644
--- a/public/app/features/visualization/data-hover/DataHoverView.tsx
+++ b/public/app/features/visualization/data-hover/DataHoverView.tsx
@@ -10,10 +10,13 @@ import {
GrafanaTheme2,
LinkModel,
} from '@grafana/data';
+import { config } from '@grafana/runtime';
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';
+
export interface Props {
data?: DataFrame; // source data
rowIndex?: number | null; // the hover row
@@ -110,6 +113,10 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he
const { displayValues, links } = dispValuesAndLinks;
+ if (config.featureToggles.newVizTooltips && header === 'Exemplar') {
+ return ;
+ }
+
return (
{header && (
@@ -142,45 +149,44 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he
};
const getStyles = (theme: GrafanaTheme2, padding = 0) => {
return {
- wrapper: css`
- padding: ${padding}px;
- background: ${theme.components.tooltip.background};
- border-radius: ${theme.shape.borderRadius(2)};
- `,
- header: css`
- background: ${theme.colors.background.secondary};
- align-items: center;
- align-content: center;
- display: flex;
- padding-bottom: ${theme.spacing(1)};
- `,
- title: css`
- font-weight: ${theme.typography.fontWeightMedium};
- overflow: hidden;
- display: inline-block;
- white-space: nowrap;
- text-overflow: ellipsis;
- flex-grow: 1;
- `,
- infoWrap: css`
- padding: ${theme.spacing(1)};
- background: transparent;
- border: none;
- th {
- font-weight: ${theme.typography.fontWeightMedium};
- padding: ${theme.spacing(0.25, 2, 0.25, 0)};
- }
+ wrapper: css({
+ padding: `${padding}px`,
+ background: theme.components.tooltip.background,
+ borderRadius: theme.shape.borderRadius(2),
+ }),
+ header: css({
+ background: theme.colors.background.secondary,
+ alignItems: 'center',
+ alignContent: 'center',
+ display: 'flex',
+ paddingBottom: theme.spacing(1),
+ }),
+ title: css({
+ fontWeight: theme.typography.fontWeightMedium,
+ overflow: 'hidden',
+ display: 'inline-block',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ flexGrow: 1,
+ }),
+ infoWrap: css({
+ padding: theme.spacing(1),
+ background: 'transparent',
+ border: 'none',
+ th: {
+ fontWeight: theme.typography.fontWeightMedium,
+ padding: theme.spacing(0.25, 2, 0.25, 0),
+ },
- tr {
- border-bottom: 1px solid ${theme.colors.border.weak};
- &:last-child {
- border-bottom: none;
- }
- }
- `,
- highlight: css``,
- link: css`
- color: ${theme.colors.text.link};
- `,
+ tr: {
+ borderBottom: `1px solid ${theme.colors.border.weak}`,
+ '&:last-child': {
+ borderBottom: 'none',
+ },
+ },
+ }),
+ link: css({
+ color: theme.colors.text.link,
+ }),
};
};
diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx
new file mode 100644
index 00000000000..87a36444c9c
--- /dev/null
+++ b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx
@@ -0,0 +1,111 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import { GrafanaTheme2, LinkModel } from '@grafana/data';
+import { HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
+import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils';
+
+import { DisplayValue } from './DataHoverView';
+
+export interface Props {
+ displayValues: DisplayValue[];
+ links?: LinkModel[];
+ header?: string;
+}
+
+export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: Props) => {
+ const styles = useStyles2(getStyles);
+
+ const time = displayValues.find((val) => val.name === 'Time');
+ displayValues = displayValues.filter((val) => val.name !== 'Time'); // time?
+
+ return (
+
+
+ {header}
+ {time && {renderValue(time.valueString)}}
+
+
+ {displayValues.map((displayValue, i) => (
+
+ {displayValue.name}
+ {renderValue(displayValue.valueString)}
+
+ ))}
+
+ {links && (
+
+ {links.map((link, i) => (
+
+ {link.title}
+
+ ))}
+
+ )}
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2, padding = 0) => {
+ return {
+ exemplarWrapper: css({
+ display: 'flex',
+ flexDirection: 'column',
+ whiteSpace: 'pre',
+ borderRadius: theme.shape.radius.default,
+ background: theme.colors.background.primary,
+ border: `1px solid ${theme.colors.border.weak}`,
+ boxShadow: `0 4px 8px ${theme.colors.background.primary}`,
+ userSelect: 'text',
+ }),
+ 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),
+ }),
+ exemplarFooter: css({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ borderTop: `1px solid ${theme.colors.border.medium}`,
+ padding: theme.spacing(1),
+ }),
+ 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,
+ }),
+ };
+};
diff --git a/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx b/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx
index 12bcaaa8656..e2110122ab3 100644
--- a/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx
+++ b/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx
@@ -1,8 +1,15 @@
-import React from 'react';
+import React, { CSSProperties } from 'react';
-import { CloseButton } from '../../../core/components/CloseButton/CloseButton';
+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,
+ };
-export function ExemplarModalHeader(props: { onClick: () => void }) {
return (
void }) {
paddingBottom: '6px',
}}
>
-
+
);
}
diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx
index 13bec6d02ef..f5ea9eee339 100644
--- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx
+++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx
@@ -4,16 +4,16 @@ import uPlot from 'uplot';
import {
DataFrameType,
- formattedValueToString,
- getFieldDisplayName,
- GrafanaTheme2,
- getLinksSupplier,
- InterpolateFunction,
- ScopedVars,
- PanelData,
- LinkModel,
Field,
FieldType,
+ formattedValueToString,
+ getFieldDisplayName,
+ getLinksSupplier,
+ GrafanaTheme2,
+ InterpolateFunction,
+ LinkModel,
+ PanelData,
+ ScopedVars,
} from '@grafana/data';
import { HeatmapCellLayout } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
@@ -28,7 +28,7 @@ import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverVi
import { HeatmapData } from './fields';
import { renderHistogram } from './renderHistogram';
-import { getSparseCellMinMax, getFieldFromData, getHoverCellColor, formatMilliseconds } from './tooltip/utils';
+import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils';
interface Props {
dataIdxs: Array
;
diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx
index 5a816578769..6c59f0ad1e2 100644
--- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx
+++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx
@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import {
@@ -8,12 +8,17 @@ import {
dateTimeFormat,
Field,
FieldType,
+ formattedValueToString,
GrafanaTheme2,
+ LinkModel,
systemDateFormats,
TimeZone,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
+import { config as runtimeConfig } from '@grafana/runtime';
import { FieldLinkList, 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 { ExemplarModalHeader } from '../../heatmap/ExemplarModalHeader';
@@ -143,6 +148,77 @@ export const ExemplarMarker = ({
setClickedExemplarFieldIndex(undefined);
};
+ let displayValues: DisplayValue[] = [];
+ let links: LinkModel[] | undefined = [];
+ orderedDataFrameFields.map((field: Field, i) => {
+ const value = field.values[dataFrameFieldIndex.fieldIndex];
+
+ if (field.config.links?.length) {
+ links?.push(...(field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex }) || []));
+ }
+
+ const fieldDisplay = field.display ? field.display(value) : { text: `${value}`, numeric: +value };
+
+ displayValues.push({
+ name: field.name,
+ value,
+ valueString: formattedValueToString(fieldDisplay),
+ highlight: false,
+ });
+ });
+
+ const exemplarHeaderCustomStyle: CSSProperties = {
+ position: 'relative',
+ top: '35px',
+ right: '5px',
+ marginRight: 0,
+ };
+
+ const getExemplarMarkerContent = () => {
+ if (runtimeConfig.featureToggles.newVizTooltips) {
+ return (
+ <>
+ {isLocked && }
+
+ >
+ );
+ } else {
+ return (
+
+ {isLocked &&
}
+
+
+ Exemplars
+
+
+
+
+ {orderedDataFrameFields.map((field: Field, i) => {
+ const value = field.values[dataFrameFieldIndex.fieldIndex];
+ const links = field.config.links?.length
+ ? field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex })
+ : undefined;
+ return (
+
+ | {field.name} |
+
+
+ {field.type === FieldType.time ? timeFormatter(value) : value}
+ {links && }
+
+ |
+
+ );
+ })}
+
+
+
+
+
+ );
+ }
+ };
+
return (
-
- {isLocked &&
}
-
-
- Exemplars
-
-
-
-
- {orderedDataFrameFields.map((field: Field, i) => {
- const value = field.values[dataFrameFieldIndex.fieldIndex];
- const links = field.config.links?.length
- ? field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex })
- : undefined;
- return (
-
- | {field.name} |
-
-
- {field.type === FieldType.time ? timeFormatter(value) : value}
- {links && }
-
- |
-
- );
- })}
-
-
-
-
-
+ {getExemplarMarkerContent()}
);
}, [
@@ -246,102 +292,96 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => {
const tableBgOdd = theme.isDark ? theme.v1.palette.dark3 : theme.v1.palette.gray6;
return {
- markerWrapper: css`
- padding: 0 4px 4px 4px;
- width: 8px;
- height: 8px;
- box-sizing: content-box;
- transform: translate3d(-50%, 0, 0);
-
- &:hover {
- > svg {
- transform: scale(1.3);
- opacity: 1;
- filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
- }
- }
- `,
- marker: css`
- width: 0;
- height: 0;
- border-left: 4px solid transparent;
- border-right: 4px solid transparent;
- border-bottom: 4px solid ${theme.v1.palette.red};
- pointer-events: none;
- `,
- wrapper: css`
- background: ${bg};
- border: 1px solid ${headerBg};
- border-radius: ${theme.shape.borderRadius(2)};
- box-shadow: 0 0 20px ${shadowColor};
- padding: ${theme.spacing(1)};
- `,
- exemplarsTable: css`
- width: 100%;
-
- tr td {
- padding: 5px 10px;
- white-space: nowrap;
- border-bottom: 4px solid ${theme.components.panel.background};
- }
-
- tr {
- background-color: ${theme.colors.background.primary};
-
- &:nth-child(even) {
- background-color: ${tableBgOdd};
- }
- }
- `,
- valueWrapper: css`
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- column-gap: ${theme.spacing(1)};
-
- > span {
- flex-grow: 0;
- }
-
- > * {
- flex: 1 1;
- align-self: center;
- }
- `,
- tooltip: css`
- background: none;
- padding: 0;
- overflow-y: auto;
- max-height: 95vh;
- `,
- header: css`
- background: ${headerBg};
- padding: 6px 10px;
- display: flex;
- `,
- title: css`
- font-weight: ${theme.typography.fontWeightMedium};
- padding-right: ${theme.spacing(2)};
- overflow: hidden;
- display: inline-block;
- white-space: nowrap;
- text-overflow: ellipsis;
- flex-grow: 1;
- `,
- body: css`
- font-weight: ${theme.typography.fontWeightMedium};
- border-radius: ${theme.shape.borderRadius(2)};
- overflow: hidden;
- `,
- marble: css`
- display: block;
- opacity: 0.5;
- transition: transform 0.15s ease-out;
- `,
- activeMarble: css`
- transform: scale(1.3);
- opacity: 1;
- filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
- `,
+ markerWrapper: css({
+ padding: '0 4px 4px 4px',
+ width: '8px',
+ height: '8px',
+ boxSizing: 'content-box',
+ transform: 'translate3d(-50%, 0, 0)',
+ '&:hover': {
+ '> svg': {
+ transform: 'scale(1.3)',
+ opacity: 1,
+ filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.5))',
+ },
+ },
+ }),
+ marker: css({
+ width: 0,
+ height: 0,
+ borderLeft: '4px solid transparent',
+ borderRight: '4px solid transparent',
+ 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',
+ }),
+ 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,
+ transition: 'transform 0.15s ease-out',
+ }),
+ activeMarble: css({
+ transform: 'scale(1.3)',
+ opacity: 1,
+ filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.5))',
+ }),
};
};