Chore: Add react-table typings to Table (#21418)

* add typings

* introduce tyings and refactor accordingly

* extract setting celltype

* update tests to reflect changes

* removing unused things

* renaming getCellType -> getCellDisplayType

* fix width type error

* remove caret

* move cell back to utils, fix story

* remove unused import

* rename type
This commit is contained in:
Peter Holmberg 2020-01-13 11:12:19 +01:00 committed by GitHub
parent b6c75b10d1
commit 30eef76162
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 169 additions and 226 deletions

View File

@ -30,6 +30,7 @@
"@torkelo/react-select": "2.1.1", "@torkelo/react-select": "2.1.1",
"@types/react-color": "2.17.0", "@types/react-color": "2.17.0",
"@types/react-select": "2.0.15", "@types/react-select": "2.0.15",
"@types/react-table": "7.0.2",
"@types/slate": "0.47.1", "@types/slate": "0.47.1",
"@types/slate-react": "0.22.5", "@types/slate-react": "0.22.5",
"bizcharts": "^3.5.5", "bizcharts": "^3.5.5",

View File

@ -0,0 +1,30 @@
import React, { CSSProperties, FC } from 'react';
import { TableCellProps } from './types';
import tinycolor from 'tinycolor2';
import { formattedValueToString } from '@grafana/data';
export const BackgroundColoredCell: FC<TableCellProps> = props => {
const { cell, tableStyles, field } = props;
if (!field.display) {
return null;
}
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
const displayValue = field.display(cell.value);
const bgColor2 = tinycolor(displayValue.color)
.darken(10 * themeFactor)
.spin(5)
.toRgbString();
const styles: CSSProperties = {
background: `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`,
borderRadius: '0px',
color: 'white',
height: tableStyles.cellHeight,
padding: tableStyles.cellPadding,
};
return <div style={styles}>{formattedValueToString(displayValue)}</div>;
};

View File

@ -1,7 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { ReactTableCellProps, TableCellDisplayMode } from './types';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { ThresholdsConfig, ThresholdsMode, VizOrientation } from '@grafana/data'; import { ThresholdsConfig, ThresholdsMode, VizOrientation } from '@grafana/data';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { TableCellProps, TableCellDisplayMode } from './types';
const defaultScale: ThresholdsConfig = { const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute, mode: ThresholdsMode.Absolute,
@ -17,9 +17,8 @@ const defaultScale: ThresholdsConfig = {
], ],
}; };
export const BarGaugeCell: FC<ReactTableCellProps> = props => { export const BarGaugeCell: FC<TableCellProps> = props => {
const { column, tableStyles, cell } = props; const { field, column, tableStyles, cell } = props;
const { field } = column;
if (!field.display) { if (!field.display) {
return null; return null;
@ -40,10 +39,17 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
barGaugeMode = BarGaugeDisplayMode.Lcd; barGaugeMode = BarGaugeDisplayMode.Lcd;
} }
let width;
if (column.width) {
width = (column.width as number) - tableStyles.cellPadding * 2;
} else {
width = tableStyles.cellPadding * 2;
}
return ( return (
<div className={tableStyles.tableCell}> <div className={tableStyles.tableCell}>
<BarGauge <BarGauge
width={column.width - tableStyles.cellPadding * 2} width={width}
height={tableStyles.cellHeightInner} height={tableStyles.cellHeightInner}
field={config} field={config}
value={displayValue} value={displayValue}

View File

@ -1,41 +1,14 @@
import React, { FC, CSSProperties } from 'react'; import React, { FC } from 'react';
import { ReactTableCellProps } from './types'; import { TableCellProps } from './types';
import { formattedValueToString } from '@grafana/data'; import { formattedValueToString } from '@grafana/data';
import tinycolor from 'tinycolor2';
export const DefaultCell: FC<ReactTableCellProps> = props => { export const DefaultCell: FC<TableCellProps> = props => {
const { column, cell, tableStyles } = props; const { field, cell, tableStyles } = props;
if (!column.field.display) { if (!field.display) {
return null; return null;
} }
const displayValue = column.field.display(cell.value); const displayValue = field.display(cell.value);
return <div className={tableStyles.tableCell}>{formattedValueToString(displayValue)}</div>; return <div className={tableStyles.tableCell}>{formattedValueToString(displayValue)}</div>;
}; };
export const BackgroundColoredCell: FC<ReactTableCellProps> = props => {
const { column, cell, tableStyles } = props;
if (!column.field.display) {
return null;
}
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
const displayValue = column.field.display(cell.value);
const bgColor2 = tinycolor(displayValue.color)
.darken(10 * themeFactor)
.spin(5)
.toRgbString();
const styles: CSSProperties = {
background: `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`,
borderRadius: '0px',
color: 'white',
height: tableStyles.cellHeight,
padding: tableStyles.cellPadding,
};
return <div style={styles}>{formattedValueToString(displayValue)}</div>;
};

View File

@ -5,13 +5,15 @@ import { number } from '@storybook/addon-knobs';
import { useTheme } from '../../themes'; import { useTheme } from '../../themes';
import mdx from './Table.mdx'; import mdx from './Table.mdx';
import { import {
applyFieldOverrides,
ConfigOverrideRule,
DataFrame, DataFrame,
MutableDataFrame, FieldMatcherID,
FieldType, FieldType,
GrafanaTheme, GrafanaTheme,
applyFieldOverrides, MutableDataFrame,
FieldMatcherID, ThresholdsConfig,
ConfigOverrideRule, ThresholdsMode,
} from '@grafana/data'; } from '@grafana/data';
export default { export default {
@ -56,7 +58,7 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
config: { config: {
unit: 'percent', unit: 'percent',
custom: { custom: {
width: 50, width: 100,
}, },
}, },
}, },
@ -118,16 +120,19 @@ export const BarGaugeCell = () => {
); );
}; };
const defaultThresholds = [ const defaultThresholds: ThresholdsConfig = {
{ steps: [
color: 'blue', {
value: -Infinity, color: 'blue',
}, value: -Infinity,
{ },
color: 'green', {
value: 20, color: 'green',
}, value: 20,
]; },
],
mode: ThresholdsMode.Absolute,
};
export const ColoredCells = () => { export const ColoredCells = () => {
const theme = useTheme(); const theme = useTheme();

View File

@ -1,12 +1,12 @@
import React, { useMemo, CSSProperties } from 'react'; import React, { useMemo } from 'react';
import { DataFrame } from '@grafana/data'; import { DataFrame } from '@grafana/data';
// @ts-ignore import { useSortBy, useTable, useBlockLayout, Cell } from 'react-table';
import { useSortBy, useTable, useBlockLayout } from 'react-table';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { getTableStyles } from './styles';
import { getColumns, getTableRows } from './utils'; import { getColumns, getTableRows } from './utils';
import { TableColumn } from './types';
import { useTheme } from '../../themes'; import { useTheme } from '../../themes';
import { TableFilterActionCallback } from './types';
import { getTableStyles } from './styles';
import { TableCell } from './TableCell';
export interface Props { export interface Props {
data: DataFrame; data: DataFrame;
@ -15,17 +15,14 @@ export interface Props {
onCellClick?: TableFilterActionCallback; onCellClick?: TableFilterActionCallback;
} }
type TableFilterActionCallback = (key: string, value: string) => void;
export const Table = ({ data, height, onCellClick, width }: Props) => { export const Table = ({ data, height, onCellClick, width }: Props) => {
const theme = useTheme(); const theme = useTheme();
const tableStyles = getTableStyles(theme); const tableStyles = getTableStyles(theme);
const { getTableProps, headerGroups, rows, prepareRow } = useTable( const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{ {
columns: useMemo(() => getColumns(data, width, theme), [data]), columns: useMemo(() => getColumns(data, width), [data]),
data: useMemo(() => getTableRows(data), [data]), data: useMemo(() => getTableRows(data), [data]),
tableStyles,
}, },
useSortBy, useSortBy,
useBlockLayout useBlockLayout
@ -37,7 +34,15 @@ export const Table = ({ data, height, onCellClick, width }: Props) => {
prepareRow(row); prepareRow(row);
return ( return (
<div {...row.getRowProps({ style })} className={tableStyles.row}> <div {...row.getRowProps({ style })} className={tableStyles.row}>
{row.cells.map((cell: RenderCellProps) => renderCell(cell, onCellClick))} {row.cells.map((cell: Cell, index: number) => (
<TableCell
key={index}
field={data.fields[cell.column.index]}
tableStyles={tableStyles}
cell={cell}
onCellClick={onCellClick}
/>
))}
</div> </div>
); );
}, },
@ -60,34 +65,6 @@ export const Table = ({ data, height, onCellClick, width }: Props) => {
); );
}; };
interface RenderCellProps {
column: TableColumn;
value: any;
getCellProps: () => { style: CSSProperties };
render: (component: string) => React.ReactNode;
}
function renderCell(cell: RenderCellProps, onCellClick?: TableFilterActionCallback) {
const filterable = cell.column.field.config.filterable;
const cellProps = cell.getCellProps();
let onClick: ((event: React.SyntheticEvent) => void) | undefined = undefined;
if (filterable && onCellClick) {
cellProps.style.cursor = 'pointer';
onClick = () => onCellClick(cell.column.Header, cell.value);
}
if (cell.column.textAlign) {
cellProps.style.textAlign = cell.column.textAlign;
}
return (
<div {...cellProps} onClick={onClick}>
{cell.render('Cell')}
</div>
);
}
function renderHeaderCell(column: any, className: string) { function renderHeaderCell(column: any, className: string) {
const headerProps = column.getHeaderProps(column.getSortByToggleProps()); const headerProps = column.getHeaderProps(column.getSortByToggleProps());

View File

@ -0,0 +1,37 @@
import React, { FC } from 'react';
import { Cell } from 'react-table';
import { Field } from '@grafana/data';
import { getTextAlign } from './utils';
import { TableFilterActionCallback } from './types';
import { TableStyles } from './styles';
interface Props {
cell: Cell;
field: Field;
tableStyles: TableStyles;
onCellClick?: TableFilterActionCallback;
}
export const TableCell: FC<Props> = ({ cell, field, tableStyles, onCellClick }) => {
const filterable = field.config.filterable;
const cellProps = cell.getCellProps();
let onClick: ((event: React.SyntheticEvent) => void) | undefined = undefined;
if (filterable && onCellClick) {
if (cellProps.style) {
cellProps.style.cursor = 'pointer';
}
onClick = () => onCellClick(cell.column.Header as string, cell.value);
}
const fieldTextAlign = getTextAlign(field);
if (fieldTextAlign && cellProps.style) {
cellProps.style.textAlign = fieldTextAlign;
}
return (
<div {...cellProps} onClick={onClick}>
{cell.render('Cell', { field, tableStyles })}
</div>
);
};

View File

@ -1,5 +1,4 @@
import { TextAlignProperty } from 'csstype'; import { CellProps } from 'react-table';
import { ComponentType } from 'react';
import { Field } from '@grafana/data'; import { Field } from '@grafana/data';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
@ -19,27 +18,13 @@ export enum TableCellDisplayMode {
export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center'; export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center';
export interface TableColumn {
// React table props
Header: string;
accessor: string | Function;
Cell: ComponentType<ReactTableCellProps>;
// Grafana additions
field: Field;
width: number;
textAlign: TextAlignProperty;
}
export interface TableRow { export interface TableRow {
[x: string]: any; [x: string]: any;
} }
export interface ReactTableCellProps { export type TableFilterActionCallback = (key: string, value: string) => void;
cell: ReactTableCell;
column: TableColumn;
tableStyles: TableStyles;
}
export interface ReactTableCell { export interface TableCellProps extends CellProps<any> {
value: any; tableStyles: TableStyles;
field: Field;
} }

View File

@ -1,6 +1,5 @@
import { MutableDataFrame, GrafanaThemeType, FieldType } from '@grafana/data'; import { MutableDataFrame, FieldType } from '@grafana/data';
import { getColumns } from './utils'; import { getColumns, getTextAlign } from './utils';
import { getTheme } from '../../themes';
function getData() { function getData() {
const data = new MutableDataFrame({ const data = new MutableDataFrame({
@ -34,33 +33,31 @@ function getData() {
describe('Table utils', () => { describe('Table utils', () => {
describe('getColumns', () => { describe('getColumns', () => {
it('Should build columns from DataFrame', () => { it('Should build columns from DataFrame', () => {
const theme = getTheme(GrafanaThemeType.Dark); const columns = getColumns(getData(), 1000);
const columns = getColumns(getData(), 1000, theme);
expect(columns[0].Header).toBe('Time'); expect(columns[0].Header).toBe('Time');
expect(columns[1].Header).toBe('Value'); expect(columns[1].Header).toBe('Value');
}); });
it('Should distribute width and use field config width', () => { it('Should distribute width and use field config width', () => {
const theme = getTheme(GrafanaThemeType.Dark); const columns = getColumns(getData(), 1000);
const columns = getColumns(getData(), 1000, theme);
expect(columns[0].width).toBe(450); expect(columns[0].width).toBe(450);
expect(columns[1].width).toBe(100); expect(columns[1].width).toBe(100);
}); });
});
describe('getTextAlign', () => {
it('Should use textAlign from custom', () => { it('Should use textAlign from custom', () => {
const theme = getTheme(GrafanaThemeType.Dark); const data = getData();
const columns = getColumns(getData(), 1000, theme); const textAlign = getTextAlign(data.fields[2]);
expect(columns[2].textAlign).toBe('center'); expect(textAlign).toBe('center');
}); });
it('Should set textAlign to right for number values', () => { it('Should set textAlign to right for number values', () => {
const theme = getTheme(GrafanaThemeType.Dark); const data = getData();
const columns = getColumns(getData(), 1000, theme); const textAlign = getTextAlign(data.fields[1]);
expect(textAlign).toBe('right');
expect(columns[1].textAlign).toBe('right');
}); });
}); });
}); });

View File

@ -1,8 +1,10 @@
import { TextAlignProperty } from 'csstype'; import { TextAlignProperty } from 'csstype';
import { DataFrame, Field, GrafanaTheme, FieldType } from '@grafana/data'; import { DataFrame, Field, FieldType } from '@grafana/data';
import { TableColumn, TableRow, TableFieldOptions, TableCellDisplayMode } from './types'; import { Column } from 'react-table';
import { DefaultCell } from './DefaultCell';
import { BarGaugeCell } from './BarGaugeCell'; import { BarGaugeCell } from './BarGaugeCell';
import { DefaultCell, BackgroundColoredCell } from './DefaultCell'; import { BackgroundColoredCell } from './BackgroundColorCell';
import { TableRow, TableFieldOptions, TableCellDisplayMode } from './types';
export function getTableRows(data: DataFrame): TableRow[] { export function getTableRows(data: DataFrame): TableRow[] {
const tableData = []; const tableData = [];
@ -19,7 +21,7 @@ export function getTableRows(data: DataFrame): TableRow[] {
return tableData; return tableData;
} }
function getTextAlign(field: Field): TextAlignProperty { export function getTextAlign(field: Field): TextAlignProperty {
if (field.config.custom) { if (field.config.custom) {
const custom = field.config.custom as TableFieldOptions; const custom = field.config.custom as TableFieldOptions;
@ -40,36 +42,21 @@ function getTextAlign(field: Field): TextAlignProperty {
return 'left'; return 'left';
} }
export function getColumns(data: DataFrame, availableWidth: number, theme: GrafanaTheme): TableColumn[] { export function getColumns(data: DataFrame, availableWidth: number): Column[] {
const cols: TableColumn[] = []; const columns: Column[] = [];
let fieldCountWithoutWidth = data.fields.length; let fieldCountWithoutWidth = data.fields.length;
for (const field of data.fields) { for (const field of data.fields) {
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions; const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
if (fieldTableOptions.width) { if (fieldTableOptions.width) {
availableWidth -= fieldTableOptions.width; availableWidth -= fieldTableOptions.width;
fieldCountWithoutWidth -= 1; fieldCountWithoutWidth -= 1;
} }
let Cell = DefaultCell; const Cell = getCellComponent(fieldTableOptions.displayMode);
let textAlign = getTextAlign(field);
switch (fieldTableOptions.displayMode) { columns.push({
case TableCellDisplayMode.ColorBackground:
Cell = BackgroundColoredCell;
break;
case TableCellDisplayMode.LcdGauge:
case TableCellDisplayMode.GradientGauge:
Cell = BarGaugeCell;
textAlign = 'center';
break;
}
cols.push({
field,
Cell, Cell,
textAlign,
Header: field.name, Header: field.name,
accessor: field.name, accessor: field.name,
width: fieldTableOptions.width, width: fieldTableOptions.width,
@ -78,11 +65,23 @@ export function getColumns(data: DataFrame, availableWidth: number, theme: Grafa
// divide up the rest of the space // divide up the rest of the space
const sharedWidth = availableWidth / fieldCountWithoutWidth; const sharedWidth = availableWidth / fieldCountWithoutWidth;
for (const column of cols) { for (const column of columns) {
if (!column.width) { if (!column.width) {
column.width = sharedWidth; column.width = sharedWidth;
} }
} }
return cols; return columns;
}
function getCellComponent(displayMode: TableCellDisplayMode) {
switch (displayMode) {
case TableCellDisplayMode.ColorBackground:
return BackgroundColoredCell;
case TableCellDisplayMode.LcdGauge:
case TableCellDisplayMode.GradientGauge:
return BarGaugeCell;
default:
return DefaultCell;
}
} }

View File

@ -3778,43 +3778,6 @@
"@types/d3-interpolate" "*" "@types/d3-interpolate" "*"
"@types/d3-selection" "*" "@types/d3-selection" "*"
"@types/d3@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.7.1.tgz#99e6b3a558816264a674947822600d3aba8b84b0"
integrity sha512-1TNamlKYTdpRzFjDIcgiRqpNqYz9VVvNOisVqCqvYsxXyysgbfTKxdOurrVcW+SxyURTPwAD68KV04BL5RKNcQ==
dependencies:
"@types/d3-array" "*"
"@types/d3-axis" "*"
"@types/d3-brush" "*"
"@types/d3-chord" "*"
"@types/d3-collection" "*"
"@types/d3-color" "*"
"@types/d3-contour" "*"
"@types/d3-dispatch" "*"
"@types/d3-drag" "*"
"@types/d3-dsv" "*"
"@types/d3-ease" "*"
"@types/d3-fetch" "*"
"@types/d3-force" "*"
"@types/d3-format" "*"
"@types/d3-geo" "*"
"@types/d3-hierarchy" "*"
"@types/d3-interpolate" "*"
"@types/d3-path" "*"
"@types/d3-polygon" "*"
"@types/d3-quadtree" "*"
"@types/d3-random" "*"
"@types/d3-scale" "*"
"@types/d3-scale-chromatic" "*"
"@types/d3-selection" "*"
"@types/d3-shape" "*"
"@types/d3-time" "*"
"@types/d3-time-format" "*"
"@types/d3-timer" "*"
"@types/d3-transition" "*"
"@types/d3-voronoi" "*"
"@types/d3-zoom" "*"
"@types/d3@5.7.2": "@types/d3@5.7.2":
version "5.7.2" version "5.7.2"
resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.7.2.tgz#52235eb71a1d3ca171d6dca52a58f5ccbe0254cc" resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.7.2.tgz#52235eb71a1d3ca171d6dca52a58f5ccbe0254cc"
@ -4290,6 +4253,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-table@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.2.tgz#184de5ad5a7c5aced08b49812002a4d2e8918cc0"
integrity sha512-sxvjV0JCk/ijCzENejXth99cFMnmucATaC31gz1bMk8iQwUDE2VYaw2QQTcDrzBxzastBQGdcLpcFIN61RvgIA==
dependencies:
"@types/react" "*"
"@types/react-test-renderer@*": "@types/react-test-renderer@*":
version "16.9.1" version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4"
@ -8245,43 +8215,6 @@ d3@5.15.0:
d3-voronoi "1" d3-voronoi "1"
d3-zoom "1" d3-zoom "1"
d3@5.9.1:
version "5.9.1"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.1.tgz#fde73fa9af7281d2ff0d2a32aa8f306e93a6d1cd"
integrity sha512-JceuBn5VVWySPQc9EA0gfq0xQVgEQXGokHhe+359bmgGeUITLK2r2b9idMzquQne9DKxb7JDCE1gDRXe9OIF2Q==
dependencies:
d3-array "1"
d3-axis "1"
d3-brush "1"
d3-chord "1"
d3-collection "1"
d3-color "1"
d3-contour "1"
d3-dispatch "1"
d3-drag "1"
d3-dsv "1"
d3-ease "1"
d3-fetch "1"
d3-force "1"
d3-format "1"
d3-geo "1"
d3-hierarchy "1"
d3-interpolate "1"
d3-path "1"
d3-polygon "1"
d3-quadtree "1"
d3-random "1"
d3-scale "2"
d3-scale-chromatic "1"
d3-selection "1"
d3-shape "1"
d3-time "1"
d3-time-format "2"
d3-timer "1"
d3-transition "1"
d3-voronoi "1"
d3-zoom "1"
d@1, d@^1.0.1: d@1, d@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"