Exemplars: Update UX to match new tooltips (#79916)

This commit is contained in:
Adela Almasan
2024-01-03 06:27:34 -06:00
committed by GitHub
parent 552fa78564
commit 070e41d136
6 changed files with 345 additions and 210 deletions

View File

@@ -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"],

View File

@@ -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 <ExemplarHoverView displayValues={displayValues} links={links} header={header} />;
}
return (
<div className={styles.wrapper}>
{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,
}),
};
};

View File

@@ -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 (
<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) => (
<HorizontalGroup key={i} justify={'space-between'} align={'center'} spacing={'md'}>
<div className={styles.label}>{displayValue.name}</div>
<div className={styles.value}>{renderValue(displayValue.valueString)}</div>
</HorizontalGroup>
))}
</div>
{links && (
<div className={styles.exemplarFooter}>
{links.map((link, i) => (
<LinkButton key={i} href={link.href} className={styles.linkButton}>
{link.title}
</LinkButton>
))}
</div>
)}
</div>
);
};
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,
}),
};
};

View File

@@ -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 (
<div
style={{
@@ -12,15 +19,7 @@ export function ExemplarModalHeader(props: { onClick: () => void }) {
paddingBottom: '6px',
}}
>
<CloseButton
onClick={props.onClick}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
<CloseButton onClick={props.onClick} style={props.style ?? defaultStyle} />
</div>
);
}

View File

@@ -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<number | null>;

View File

@@ -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 && <ExemplarModalHeader onClick={onClose} style={exemplarHeaderCustomStyle} />}
<ExemplarHoverView displayValues={displayValues} links={links} />
</>
);
} else {
return (
<div className={styles.wrapper}>
{isLocked && <ExemplarModalHeader onClick={onClose} />}
<div className={styles.body}>
<div className={styles.header}>
<span className={styles.title}>Exemplars</span>
</div>
<div>
<table className={styles.exemplarsTable}>
<tbody>
{orderedDataFrameFields.map((field: Field, i) => {
const value = field.values[dataFrameFieldIndex.fieldIndex];
const links = field.config.links?.length
? field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex })
: undefined;
return (
<tr key={i}>
<td valign="top">{field.name}</td>
<td>
<div className={styles.valueWrapper}>
<span>{field.type === FieldType.time ? timeFormatter(value) : value}</span>
{links && <FieldLinkList links={links} />}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
}
};
return (
<div
onMouseEnter={onMouseEnter}
@@ -152,37 +228,7 @@ export const ExemplarMarker = ({
style={popperStyles.popper}
{...attributes.popper}
>
<div className={styles.wrapper}>
{isLocked && <ExemplarModalHeader onClick={onClose} />}
<div className={styles.body}>
<div className={styles.header}>
<span className={styles.title}>Exemplars</span>
</div>
<div>
<table className={styles.exemplarsTable}>
<tbody>
{orderedDataFrameFields.map((field: Field, i) => {
const value = field.values[dataFrameFieldIndex.fieldIndex];
const links = field.config.links?.length
? field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex })
: undefined;
return (
<tr key={i}>
<td valign="top">{field.name}</td>
<td>
<div className={styles.valueWrapper}>
<span>{field.type === FieldType.time ? timeFormatter(value) : value}</span>
{links && <FieldLinkList links={links} />}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
{getExemplarMarkerContent()}
</div>
);
}, [
@@ -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))',
}),
};
};