Tooltips: Support long labels (#77735)

* feat(tooltips): support long labels

Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
Ihor Yeromin 2023-11-16 15:53:23 +02:00 committed by GitHub
parent 2659409191
commit 6f0c5395ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 178 deletions

View File

@ -1,95 +1,16 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup, Tooltip } from '..';
import { useStyles2 } from '../../themes';
import { VizTooltipRow } from './VizTooltipRow';
import { LabelValue } from './types';
import { getColorIndicatorClass } from './utils';
interface Props {
headerLabel: LabelValue;
}
export const HeaderLabel = ({ headerLabel }: Props) => {
const styles = useStyles2(getStyles);
const { label, value, color, colorIndicator } = headerLabel;
return (
<HorizontalGroup justify-content="space-between" spacing="lg" wrap>
<div className={styles.wrapper}>
<span className={styles.label}>{label}</span>
{color && (
<span
style={{ backgroundColor: color }}
className={cx(styles.colorIndicator, getColorIndicatorClass(colorIndicator!, styles))}
/>
)}
<Tooltip content={value ? value.toString() : ''}>
<span className={styles.labelValue}>{value}</span>
</Tooltip>
</div>
</HorizontalGroup>
<VizTooltipRow label={label} value={value} color={color} colorIndicator={colorIndicator} marginRight={'22px'} />
);
};
const getStyles = (theme: GrafanaTheme2) => ({
hgContainer: css({
flexGrow: 1,
}),
colorIndicator: css({
marginRight: theme.spacing(0.5),
}),
label: css({
color: theme.colors.text.secondary,
paddingRight: theme.spacing(0.5),
fontWeight: 400,
}),
value: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.default,
}),
series: css({
width: '14px',
height: '4px',
borderRadius: theme.shape.radius.pill,
}),
labelValue: css({
fontWeight: 500,
lineHeight: '18px',
alignSelf: 'center',
}),
wrapper: css({
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
width: '250px',
maskImage: 'linear-gradient(90deg, rgba(0, 0, 0, 1) 80%, transparent)',
}),
hexagon: css({}),
pie_1_4: css({}),
pie_2_4: css({}),
pie_3_4: css({}),
marker_sm: css({
width: '4px',
height: '4px',
borderRadius: theme.shape.radius.circle,
}),
marker_md: css({
width: '8px',
height: '8px',
borderRadius: theme.shape.radius.circle,
}),
marker_lg: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.circle,
}),
});

View File

@ -1,12 +1,8 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, GraphSeriesValue } from '@grafana/data';
import { GraphSeriesValue } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { HorizontalGroup } from '../Layout/Layout';
import { VizTooltipColorIndicator } from './VizTooltipColorIndicator';
import { VizTooltipRow } from './VizTooltipRow';
import { ColorIndicator } from './types';
export interface SeriesListProps {
@ -18,13 +14,19 @@ export const SeriesList = ({ series }: SeriesListProps) => {
return (
<>
{series.map((series, index) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const label = series.label as string;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const value = series.value as string;
return (
<SingleSeries
isActive={series.isActive}
label={series.label}
color={series.color}
value={series.value}
<VizTooltipRow
key={`${series.label}-${index}`}
label={label}
value={value}
color={series.color}
colorIndicator={ColorIndicator.series}
isActive={series.isActive}
justify={'space-between'}
/>
);
})}
@ -39,31 +41,3 @@ export interface SingleSeriesProps {
isActive?: boolean;
colorIndicator?: ColorIndicator;
}
const SingleSeries = ({ label, value, color, colorIndicator = ColorIndicator.series, isActive }: SingleSeriesProps) => {
const styles = useStyles2(getStyles);
return (
<HorizontalGroup justify="space-between" spacing="md" className={styles.hgContainer}>
<>
{color && <VizTooltipColorIndicator color={color} colorIndicator={colorIndicator} />}
{label && <div className={cx(styles.label, isActive && styles.activeSeries)}>{label}</div>}
</>
{value && <div className={cx(isActive && styles.activeSeries)}>{value}</div>}
</HorizontalGroup>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
hgContainer: css({
flexGrow: 1,
}),
activeSeries: css({
fontWeight: theme.typography.fontWeightBold,
color: theme.colors.text.maxContrast,
}),
label: css({
color: theme.colors.text.secondary,
fontWeight: 400,
}),
});

View File

@ -35,12 +35,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
width: '14px',
height: '4px',
borderRadius: theme.shape.radius.pill,
minWidth: '14px',
}),
value: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.default,
fontWeight: 500,
minWidth: '12px',
}),
hexagon: css({}),
pie_1_4: css({}),
@ -50,15 +52,18 @@ const getStyles = (theme: GrafanaTheme2) => ({
width: '4px',
height: '4px',
borderRadius: theme.shape.radius.circle,
minWidth: '4px',
}),
marker_md: css({
width: '8px',
height: '8px',
borderRadius: theme.shape.radius.circle,
minWidth: '8px',
}),
marker_lg: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.circle,
minWidth: '12px',
}),
});

View File

@ -3,15 +3,16 @@ import React, { ReactElement } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup } from '..';
import { useStyles2 } from '../../themes';
import { VizTooltipRow } from './VizTooltipRow';
import { LabelValue } from './types';
interface Props {
contentLabelValue: LabelValue[];
customContent?: ReactElement | null;
}
export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) => {
const styles = useStyles2(getStyles);
@ -19,11 +20,17 @@ export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) =
<div className={styles.wrapper}>
<div>
{contentLabelValue?.map((labelValue, i) => {
const { label, value, color, colorIndicator } = labelValue;
return (
<HorizontalGroup justify="space-between" spacing="lg" key={i}>
<div className={styles.label}>{labelValue.label}</div>
<div className={styles.value}>{labelValue.value}</div>
</HorizontalGroup>
<VizTooltipRow
key={i}
label={label}
value={value}
color={color}
colorIndicator={colorIndicator}
colorFirst={false}
justify={'space-between'}
/>
);
})}
</div>
@ -44,11 +51,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
customContentPadding: css({
padding: `${theme.spacing(1)} 0`,
}),
label: css({
color: theme.colors.text.secondary,
fontWeight: 400,
}),
value: css({
fontWeight: 500,
}),
});

View File

@ -17,17 +17,10 @@ interface Props {
export const VizTooltipHeader = ({ headerLabel, keyValuePairs, customValueDisplay }: Props) => {
const styles = useStyles2(getStyles);
const renderValue = () => {
if (customValueDisplay) {
return customValueDisplay;
}
return <VizTooltipHeaderLabelValue keyValuePairs={keyValuePairs} />;
};
return (
<div className={styles.wrapper}>
<HeaderLabel headerLabel={headerLabel} />
{renderValue()}
{customValueDisplay || <VizTooltipHeaderLabelValue keyValuePairs={keyValuePairs} />}
</div>
);
};

View File

@ -1,46 +1,24 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup } from '..';
import { useStyles2 } from '../../themes';
import { VizTooltipColorIndicator } from './VizTooltipColorIndicator';
import { VizTooltipRow } from './VizTooltipRow';
import { LabelValue } from './types';
interface Props {
keyValuePairs?: LabelValue[];
}
export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => {
const styles = useStyles2(getStyles);
return (
<>
{keyValuePairs?.map((keyValuePair, i) => {
return (
<HorizontalGroup justify="space-between" spacing="md" className={styles.hgContainer} key={i}>
<div className={styles.label}>{keyValuePair.label}</div>
<>
{keyValuePair.color && (
<VizTooltipColorIndicator color={keyValuePair.color} colorIndicator={keyValuePair.colorIndicator!} />
)}
{keyValuePair.value}
</>
</HorizontalGroup>
);
})}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
hgContainer: css({
flexGrow: 1,
}),
label: css({
color: theme.colors.text.secondary,
fontWeight: 400,
}),
});
export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => (
<>
{keyValuePairs?.map((keyValuePair, i) => (
<VizTooltipRow
key={i}
label={keyValuePair.label}
value={keyValuePair.value}
color={keyValuePair.color}
colorIndicator={keyValuePair.colorIndicator!}
colorFirst={false}
justify={'space-between'}
/>
))}
</>
);

View File

@ -0,0 +1,119 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { Tooltip } from '../Tooltip';
import { VizTooltipColorIndicator } from './VizTooltipColorIndicator';
import { LabelValue } from './types';
interface Props extends LabelValue {
justify?: string;
colorFirst?: boolean;
isActive?: boolean; // for series list
marginRight?: string;
}
export const VizTooltipRow = ({
label,
value,
color,
colorIndicator,
justify = 'flex-start',
colorFirst = true,
isActive = false,
marginRight = '0px',
}: Props) => {
const styles = useStyles2(getStyles, justify, marginRight);
const [showLabelTooltip, setShowLabelTooltip] = useState(false);
const [showValueTooltip, setShowValueTooltip] = useState(false);
const onMouseEnterLabel = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.currentTarget.offsetWidth < event.currentTarget.scrollWidth) {
setShowLabelTooltip(true);
}
};
const onMouseLeaveLabel = () => setShowLabelTooltip(false);
const onMouseEnterValue = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.currentTarget.offsetWidth < event.currentTarget.scrollWidth) {
setShowValueTooltip(true);
}
};
const onMouseLeaveValue = () => setShowValueTooltip(false);
return (
<div className={styles.contentWrapper}>
{(color || label) && (
<div className={styles.valueWrapper}>
{color && colorFirst && <VizTooltipColorIndicator color={color} colorIndicator={colorIndicator!} />}
<Tooltip content={label} interactive={false} show={showLabelTooltip}>
<div
className={cx(styles.label, isActive && styles.activeSeries)}
onMouseEnter={onMouseEnterLabel}
onMouseLeave={onMouseLeaveLabel}
>
{label}
</div>
</Tooltip>
</div>
)}
<div className={styles.valueWrapper}>
{color && !colorFirst && <VizTooltipColorIndicator color={color} colorIndicator={colorIndicator!} />}
<Tooltip content={value ? value.toString() : ''} interactive={false} show={showValueTooltip}>
<div className={cx(styles.value, isActive)} onMouseEnter={onMouseEnterValue} onMouseLeave={onMouseLeaveValue}>
{value}
</div>
</Tooltip>
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme2, justify: string, marginRight: string) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
flex: 1,
gap: 4,
borderTop: `1px solid ${theme.colors.border.medium}`,
padding: theme.spacing(1),
}),
contentWrapper: css({
display: 'flex',
alignItems: 'center',
justifyContent: justify,
flexWrap: 'wrap',
marginRight: marginRight,
}),
customContentPadding: css({
padding: `${theme.spacing(1)} 0`,
}),
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',
}),
valueWrapper: css({
display: 'flex',
alignItems: 'center',
minWidth: 0,
}),
activeSeries: css({
fontWeight: theme.typography.fontWeightBold,
color: theme.colors.text.maxContrast,
}),
});