Table: Introduce sparkline cell type (#63182)

This commit is contained in:
Domas
2023-03-10 14:41:06 +02:00
committed by GitHub
parent c955c20670
commit 548a5054ad
38 changed files with 1714 additions and 714 deletions
+1 -1
View File
@@ -7,4 +7,4 @@ export * from './dimensions';
export * from './ArrayDataFrame';
export * from './DataFrameJSON';
export * from './frameComparisons';
export { anySeriesWithTimeField } from './utils';
export { anySeriesWithTimeField, isTimeSeriesFrame, isTimeSeriesFrames } from './utils';
+3 -3
View File
@@ -2,15 +2,15 @@ import { DataFrame, FieldType } from '../types/dataFrame';
import { getTimeField } from './processDataFrame';
export function isTimeSerie(frame: DataFrame) {
export function isTimeSeriesFrame(frame: DataFrame) {
if (frame.fields.length > 2) {
return false;
}
return Boolean(frame.fields.find((field) => field.type === FieldType.time));
}
export function isTimeSeries(data: DataFrame[]) {
return !data.find((frame) => !isTimeSerie(frame));
export function isTimeSeriesFrames(data: DataFrame[]) {
return !data.find((frame) => !isTimeSeriesFrame(frame));
}
/**
+1 -1
View File
@@ -15,4 +15,4 @@ export { sortThresholds, getActiveThreshold } from './thresholds';
export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides, useFieldOverrides } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldDisplayName, getFrameDisplayName } from './fieldState';
export { getScaleCalculator, getFieldConfigWithMinMax } from './scale';
export { getScaleCalculator, getFieldConfigWithMinMax, getMinMaxAndDelta } from './scale';
@@ -36,4 +36,5 @@ export enum DataTransformerID {
groupingToMatrix = 'groupingToMatrix',
limit = 'limit',
partitionByValues = 'partitionByValues',
timeSeriesTable = 'timeSeriesTable',
}
@@ -2,7 +2,7 @@ import { omit } from 'lodash';
import { map } from 'rxjs/operators';
import { MutableDataFrame, sortDataFrame } from '../../dataframe';
import { isTimeSeries } from '../../dataframe/utils';
import { isTimeSeriesFrames } from '../../dataframe/utils';
import { getFrameDisplayName } from '../../field/fieldState';
import {
Field,
@@ -30,7 +30,7 @@ export const seriesToRowsTransformer: DataTransformerInfo<SeriesToRowsTransforme
return data;
}
if (!isTimeSeries(data)) {
if (!isTimeSeriesFrames(data)) {
return data;
}
@@ -18,6 +18,7 @@ export enum FieldType {
geo = 'geo',
enum = 'enum',
other = 'other', // Object, Array, etc
frame = 'frame', // DataFrame
}
/**
@@ -82,4 +82,5 @@ export interface FeatureToggles {
drawerDataSourcePicker?: boolean;
traceqlSearch?: boolean;
prometheusMetricEncyclopedia?: boolean;
timeSeriesTable?: boolean;
}
@@ -601,6 +601,8 @@ export interface VizTooltipOptions {
sort: SortOrder;
}
export interface Labels {}
/**
* Internally, this is the "type" of cell that's being displayed
* in the table such as colored text, JSON, gauge, etc.
@@ -618,6 +620,7 @@ export enum TableCellDisplayMode {
Image = 'image',
JSONView = 'json-view',
LcdGauge = 'lcd-gauge',
Sparkline = 'sparkline',
}
/**
@@ -696,6 +699,13 @@ export interface TableBarGaugeCellOptions {
type: TableCellDisplayMode.Gauge;
}
/**
* Sparkline cell options
*/
export interface TableSparklineCellOptions extends GraphFieldConfig {
type: TableCellDisplayMode.Sparkline;
}
/**
* Colored background cell options
*/
@@ -708,7 +718,7 @@ export interface TableColoredBackgroundCellOptions {
* Table cell options. Each cell has a display mode
* and other potential options for that display.
*/
export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions);
export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions);
/**
* Use UTC/GMT timezone
@@ -769,7 +779,14 @@ export enum LogsDedupStrategy {
signature = 'signature',
}
export interface Labels {}
/**
* Height of a table cell
*/
export enum TableCellHeight {
Lg = 'lg',
Md = 'md',
Sm = 'sm',
}
/**
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
@@ -251,3 +251,6 @@ VizTooltipOptions: {
Labels: {
[string]: string
} @cuetsy(kind="interface")
// Height of a table cell
TableCellHeight: "sm" | "md" | "lg" @cuetsy(kind="enum")
+8 -2
View File
@@ -4,7 +4,7 @@ package common
// in the table such as colored text, JSON, gauge, etc.
// The color-background-solid, gradient-gauge, and lcd-gauge
// modes are deprecated in favor of new cell subOptions
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge")
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline")
// Display mode to the "Colored Background" display
// mode for table cells. Either displays a solid color (basic mode)
@@ -54,6 +54,12 @@ TableBarGaugeCellOptions: {
mode?: BarGaugeDisplayMode
} @cuetsy(kind="interface")
// Sparkline cell options
TableSparklineCellOptions: {
GraphFieldConfig
type: TableCellDisplayMode & "sparkline"
} @cuetsy(kind="interface")
// Colored background cell options
TableColoredBackgroundCellOptions: {
type: TableCellDisplayMode & "color-background"
@@ -62,7 +68,7 @@ TableColoredBackgroundCellOptions: {
// Table cell options. Each cell has a display mode
// and other potential options for that display.
TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
// Generally defines alignment, filtering capabilties, display options, etc.
@@ -44,6 +44,7 @@ const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
showPoints: VisibilityMode.Auto,
axisPlacement: AxisPlacement.Hidden,
pointSize: 2,
};
/** @internal */
@@ -181,6 +182,8 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
pxAlign: false,
scaleKey,
theme,
colorMode,
thresholds: config.thresholds,
drawStyle: customConfig.drawStyle!,
lineColor: customConfig.lineColor ?? seriesColor,
lineWidth: customConfig.lineWidth,
@@ -188,7 +191,9 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
showPoints: pointsMode,
pointSize: customConfig.pointSize,
fillOpacity: customConfig.fillOpacity,
fillColor: customConfig.fillColor ?? seriesColor,
fillColor: customConfig.fillColor,
lineStyle: customConfig.lineStyle,
gradientMode: customConfig.gradientMode,
});
}
@@ -16,7 +16,7 @@ export interface FooterRowProps {
tableStyles: TableStyles;
}
export const FooterRow = (props: FooterRowProps) => {
export function FooterRow(props: FooterRowProps) {
const { totalColumnsWidth, footerGroups, isPaginationVisible, tableStyles } = props;
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
@@ -38,7 +38,7 @@ export const FooterRow = (props: FooterRowProps) => {
})}
</div>
);
};
}
function renderFooterCell(column: ColumnInstance, tableStyles: TableStyles) {
const footerProps = column.getHeaderProps();
@@ -0,0 +1,114 @@
import { isArray } from 'lodash';
import React, { FC } from 'react';
import {
ArrayVector,
FieldType,
FieldConfig,
getMinMaxAndDelta,
FieldSparkline,
isDataFrame,
Field,
} from '@grafana/data';
import {
BarAlignment,
GraphDrawStyle,
GraphFieldConfig,
GraphGradientMode,
LineInterpolation,
TableSparklineCellOptions,
TableCellDisplayMode,
VisibilityMode,
} from '@grafana/schema';
import { Sparkline } from '../Sparkline/Sparkline';
import { TableCellProps } from './types';
import { getCellOptions } from './utils';
export const defaultSparklineCellConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
lineInterpolation: LineInterpolation.Smooth,
lineWidth: 1,
fillOpacity: 17,
gradientMode: GraphGradientMode.Hue,
pointSize: 2,
barAlignment: BarAlignment.Center,
showPoints: VisibilityMode.Never,
};
export const SparklineCell: FC<TableCellProps> = (props) => {
const { field, innerWidth, tableStyles, cell, cellProps } = props;
const sparkline = getSparkline(cell.value);
if (!sparkline) {
return (
<div {...cellProps} className={tableStyles.cellContainer}>
no data
</div>
);
}
const range = getMinMaxAndDelta(sparkline.y);
sparkline.y.config.min = range.min;
sparkline.y.config.max = range.max;
sparkline.y.state = { range };
const cellOptions = getTableSparklineCellOptions(field);
const config: FieldConfig<GraphFieldConfig> = {
color: field.config.color,
custom: {
...defaultSparklineCellConfig,
...cellOptions,
},
};
return (
<div {...cellProps} className={tableStyles.cellContainer}>
<Sparkline
width={innerWidth}
height={tableStyles.cellHeightInner}
sparkline={sparkline}
config={config}
theme={tableStyles.theme}
/>
</div>
);
};
function getSparkline(value: unknown): FieldSparkline | undefined {
if (isArray(value)) {
return {
y: {
name: 'test',
type: FieldType.number,
values: new ArrayVector(value),
config: {},
},
};
}
if (isDataFrame(value)) {
const timeField = value.fields.find((x) => x.type === FieldType.time);
const numberField = value.fields.find((x) => x.type === FieldType.number);
if (timeField && numberField) {
return { x: timeField, y: numberField };
}
}
return;
}
function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions {
let options = getCellOptions(field);
if (options.type === TableCellDisplayMode.Auto) {
options = { ...options, type: TableCellDisplayMode.Sparkline };
}
if (options.type === TableCellDisplayMode.Sparkline) {
return options;
}
throw new Error(`Excpected options type ${TableCellDisplayMode.Sparkline} but got ${options.type}`);
}
@@ -13,8 +13,9 @@ import {
import { VariableSizeList } from 'react-window';
import { DataFrame, Field, ReducerID } from '@grafana/data';
import { TableCellHeight } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../themes';
import { useTheme2 } from '../../themes';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
import { Pagination } from '../Pagination/Pagination';
@@ -23,7 +24,7 @@ import { HeaderRow } from './HeaderRow';
import { TableCell } from './TableCell';
import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks';
import { getInitialState, useTableStateReducer } from './reducer';
import { getTableStyles } from './styles';
import { useTableStyles } from './styles';
import { FooterItem, GrafanaTableState, Props } from './types';
import {
getColumns,
@@ -56,13 +57,14 @@ export const Table = memo((props: Props) => {
showTypeIcons,
footerValues,
enablePagination,
cellHeight = TableCellHeight.Md,
} = props;
const listRef = useRef<VariableSizeList>(null);
const tableDivRef = useRef<HTMLDivElement>(null);
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
const tableStyles = useStyles2(getTableStyles);
const theme = useTheme2();
const tableStyles = useTableStyles(theme, cellHeight);
const headerHeight = noHeader ? 0 : tableStyles.rowHeight;
const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(footerValues);
@@ -385,6 +387,8 @@ export const Table = memo((props: Props) => {
<div ref={variableSizeListScrollbarRef}>
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}>
<VariableSizeList
// This component needs an unmount/remount when row height changes
key={tableStyles.rowHeight}
height={listHeight}
itemCount={itemCount}
itemSize={getItemSize}
@@ -1,15 +1,15 @@
import { css, CSSObject } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { TableCellHeight } from '@grafana/schema';
export const getTableStyles = (theme: GrafanaTheme2) => {
export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCellHeight) {
const borderColor = theme.colors.border.weak;
const resizerColor = theme.colors.primary.border;
const cellPadding = 6;
const lineHeight = theme.typography.body.lineHeight;
const bodyFontSize = 14;
const cellHeight = cellPadding * 2 + bodyFontSize * lineHeight;
const cellHeight = getCellHeight(theme, cellHeightOption, cellPadding);
const rowHeight = cellHeight + 2;
const headerHeight = 28;
const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03);
const buildCellContainerStyle = (color?: string, background?: string, overflowOnHover?: boolean) => {
@@ -95,7 +95,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
cellHeight,
buildCellContainerStyle,
cellPadding,
cellHeightInner: bodyFontSize * lineHeight,
cellHeightInner: cellHeight - cellPadding * 2,
rowHeight,
table: css`
height: 100%;
@@ -106,14 +106,14 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
`,
thead: css`
label: thead;
height: ${rowHeight}px;
height: ${headerHeight}px;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`,
tfoot: css`
label: tfoot;
height: ${rowHeight}px;
height: ${headerHeight}px;
border-top: 1px solid ${borderColor};
overflow-y: auto;
overflow-x: hidden;
@@ -124,10 +124,12 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
border-bottom: 1px solid ${borderColor};
`,
headerCell: css`
padding: ${cellPadding}px;
height: 100%;
padding: 0 ${cellPadding}px;
overflow: hidden;
white-space: nowrap;
display: flex;
align-items: center;
font-weight: ${theme.typography.fontWeightMedium};
&:last-child {
@@ -285,6 +287,21 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
cursor: pointer;
`,
};
};
}
export type TableStyles = ReturnType<typeof getTableStyles>;
export type TableStyles = ReturnType<typeof useTableStyles>;
function getCellHeight(theme: GrafanaTheme2, cellHeightOption: TableCellHeight, cellPadding: number) {
const bodyFontSize = theme.typography.fontSize;
const lineHeight = theme.typography.body.lineHeight;
switch (cellHeightOption) {
case 'md':
return 42;
case 'lg':
return 48;
case 'sm':
default:
return cellPadding * 2 + bodyFontSize * lineHeight;
}
}
@@ -3,6 +3,7 @@ import { FC } from 'react';
import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table';
import { DataFrame, Field, KeyValue, SelectableValue } from '@grafana/data';
import { TableCellHeight } from '@grafana/schema';
import { TableStyles } from './styles';
@@ -84,6 +85,7 @@ export interface Props {
footerOptions?: TableFooterCalc;
footerValues?: FooterItem[];
enablePagination?: boolean;
cellHeight?: TableCellHeight;
/** @alpha */
subData?: DataFrame[];
}
@@ -15,6 +15,8 @@ import {
reduceField,
GrafanaTheme2,
ArrayVector,
isDataFrame,
isTimeSeriesFrame,
} from '@grafana/data';
import {
BarGaugeDisplayMode,
@@ -30,6 +32,7 @@ import { GeoCell } from './GeoCell';
import { ImageCell } from './ImageCell';
import { JSONViewCell } from './JSONViewCell';
import { RowExpander } from './RowExpander';
import { SparklineCell } from './SparklineCell';
import {
CellComponent,
TableCellDisplayMode,
@@ -190,6 +193,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field
return ImageCell;
case TableCellDisplayMode.Gauge:
return BarGaugeCell;
case TableCellDisplayMode.Sparkline:
return SparklineCell;
case TableCellDisplayMode.JSONView:
return JSONViewCell;
}
@@ -198,10 +203,20 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field
return GeoCell;
}
if (field.type === FieldType.frame) {
const firstValue = field.values.get(0);
if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) {
return SparklineCell;
}
return JSONViewCell;
}
// Default or Auto
if (field.type === FieldType.other) {
return JSONViewCell;
}
return DefaultCell;
}