grafana/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx
2022-12-08 14:53:07 +01:00

266 lines
7.8 KiB
TypeScript

import { css, cx } from '@emotion/css';
import React, { useCallback, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import {
DataFrame,
DataFrameFieldIndex,
dateTimeFormat,
Field,
FieldType,
GrafanaTheme2,
LinkModel,
systemDateFormats,
TimeZone,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { FieldLinkList, Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui';
interface ExemplarMarkerProps {
timeZone: TimeZone;
dataFrame: DataFrame;
dataFrameFieldIndex: DataFrameFieldIndex;
config: UPlotConfigBuilder;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
}
export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({
timeZone,
dataFrame,
dataFrameFieldIndex,
config,
getFieldLinks,
}) => {
const styles = useStyles2(getExemplarMarkerStyles);
const [isOpen, setIsOpen] = useState(false);
const [markerElement, setMarkerElement] = React.useState<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement);
const popoverRenderTimeout = useRef<NodeJS.Timer>();
const getSymbol = () => {
const symbols = [
<rect key="diamond" x="3.38672" width="4.78985" height="4.78985" transform="rotate(45 3.38672 0)" />,
<path
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 key="triangle" d="M4 0L7.4641 6H0.535898L4 0Z" />,
<rect key="rectangle" width="5" height="5" />,
<path key="pentagon" d="M3 0.5L5.85317 2.57295L4.76336 5.92705H1.23664L0.146831 2.57295L3 0.5Z" />,
<path
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 onMouseEnter = useCallback(() => {
if (popoverRenderTimeout.current) {
clearTimeout(popoverRenderTimeout.current);
}
setIsOpen(true);
}, [setIsOpen]);
const onMouseLeave = useCallback(() => {
popoverRenderTimeout.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}, [setIsOpen]);
const renderMarker = useCallback(() => {
// Put the traceID field in front.
const traceIDField = dataFrame.fields.find((field) => field.name === 'traceID') || dataFrame.fields[0];
const orderedDataFrameFields = [traceIDField, ...dataFrame.fields.filter((field) => traceIDField !== field)];
const timeFormatter = (value: number) => {
return dateTimeFormat(value, {
format: systemDateFormats.fullDate,
timeZone,
});
};
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={styles.tooltip}
ref={setPopperElement}
style={popperStyles.popper}
{...attributes.popper}
>
<div className={styles.wrapper}>
<div className={styles.header}>
<span className={styles.title}>Exemplar</span>
</div>
<div className={styles.body}>
<div>
<table className={styles.exemplarsTable}>
<tbody>
{orderedDataFrameFields.map((field, i) => {
const value = field.values.get(dataFrameFieldIndex.fieldIndex);
const links = field.config.links?.length
? getFieldLinks(field, 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>
</div>
);
}, [
attributes.popper,
dataFrame.fields,
getFieldLinks,
dataFrameFieldIndex,
onMouseEnter,
onMouseLeave,
popperStyles.popper,
styles,
timeZone,
]);
const seriesColor = config
.getSeries()
.find((s) => s.props.dataFrameFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex)?.props.lineColor;
return (
<>
<div
ref={setMarkerElement}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={styles.markerWrapper}
aria-label={selectors.components.DataSource.Prometheus.exemplarMarker}
>
<svg
viewBox="0 0 7 7"
width="7"
height="7"
style={{ fill: seriesColor }}
className={cx(styles.marble, isOpen && styles.activeMarble)}
>
{getSymbol()}
</svg>
</div>
{isOpen && <Portal>{renderMarker()}</Portal>}
</>
);
};
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;
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};
`,
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;
`,
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`
padding: ${theme.spacing(1)};
font-weight: ${theme.typography.fontWeightMedium};
`,
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));
`,
};
};