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))', + }), }; };