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
11 changed files with 169 additions and 226 deletions
+1
View File
@@ -30,6 +30,7 @@
"@torkelo/react-select": "2.1.1",
"@types/react-color": "2.17.0",
"@types/react-select": "2.0.15",
"@types/react-table": "7.0.2",
"@types/slate": "0.47.1",
"@types/slate-react": "0.22.5",
"bizcharts": "^3.5.5",
@@ -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>;
};
@@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { ReactTableCellProps, TableCellDisplayMode } from './types';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { ThresholdsConfig, ThresholdsMode, VizOrientation } from '@grafana/data';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { TableCellProps, TableCellDisplayMode } from './types';
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
@@ -17,9 +17,8 @@ const defaultScale: ThresholdsConfig = {
],
};
export const BarGaugeCell: FC<ReactTableCellProps> = props => {
const { column, tableStyles, cell } = props;
const { field } = column;
export const BarGaugeCell: FC<TableCellProps> = props => {
const { field, column, tableStyles, cell } = props;
if (!field.display) {
return null;
@@ -40,10 +39,17 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
barGaugeMode = BarGaugeDisplayMode.Lcd;
}
let width;
if (column.width) {
width = (column.width as number) - tableStyles.cellPadding * 2;
} else {
width = tableStyles.cellPadding * 2;
}
return (
<div className={tableStyles.tableCell}>
<BarGauge
width={column.width - tableStyles.cellPadding * 2}
width={width}
height={tableStyles.cellHeightInner}
field={config}
value={displayValue}
@@ -1,41 +1,14 @@
import React, { FC, CSSProperties } from 'react';
import { ReactTableCellProps } from './types';
import React, { FC } from 'react';
import { TableCellProps } from './types';
import { formattedValueToString } from '@grafana/data';
import tinycolor from 'tinycolor2';
export const DefaultCell: FC<ReactTableCellProps> = props => {
const { column, cell, tableStyles } = props;
export const DefaultCell: FC<TableCellProps> = props => {
const { field, cell, tableStyles } = props;
if (!column.field.display) {
if (!field.display) {
return null;
}
const displayValue = column.field.display(cell.value);
const displayValue = field.display(cell.value);
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>;
};
@@ -5,13 +5,15 @@ import { number } from '@storybook/addon-knobs';
import { useTheme } from '../../themes';
import mdx from './Table.mdx';
import {
applyFieldOverrides,
ConfigOverrideRule,
DataFrame,
MutableDataFrame,
FieldMatcherID,
FieldType,
GrafanaTheme,
applyFieldOverrides,
FieldMatcherID,
ConfigOverrideRule,
MutableDataFrame,
ThresholdsConfig,
ThresholdsMode,
} from '@grafana/data';
export default {
@@ -56,7 +58,7 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
config: {
unit: 'percent',
custom: {
width: 50,
width: 100,
},
},
},
@@ -118,16 +120,19 @@ export const BarGaugeCell = () => {
);
};
const defaultThresholds = [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
];
const defaultThresholds: ThresholdsConfig = {
steps: [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
],
mode: ThresholdsMode.Absolute,
};
export const ColoredCells = () => {
const theme = useTheme();
@@ -1,12 +1,12 @@
import React, { useMemo, CSSProperties } from 'react';
import React, { useMemo } from 'react';
import { DataFrame } from '@grafana/data';
// @ts-ignore
import { useSortBy, useTable, useBlockLayout } from 'react-table';
import { useSortBy, useTable, useBlockLayout, Cell } from 'react-table';
import { FixedSizeList } from 'react-window';
import { getTableStyles } from './styles';
import { getColumns, getTableRows } from './utils';
import { TableColumn } from './types';
import { useTheme } from '../../themes';
import { TableFilterActionCallback } from './types';
import { getTableStyles } from './styles';
import { TableCell } from './TableCell';
export interface Props {
data: DataFrame;
@@ -15,17 +15,14 @@ export interface Props {
onCellClick?: TableFilterActionCallback;
}
type TableFilterActionCallback = (key: string, value: string) => void;
export const Table = ({ data, height, onCellClick, width }: Props) => {
const theme = useTheme();
const tableStyles = getTableStyles(theme);
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns: useMemo(() => getColumns(data, width, theme), [data]),
columns: useMemo(() => getColumns(data, width), [data]),
data: useMemo(() => getTableRows(data), [data]),
tableStyles,
},
useSortBy,
useBlockLayout
@@ -37,7 +34,15 @@ export const Table = ({ data, height, onCellClick, width }: Props) => {
prepareRow(row);
return (
<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>
);
},
@@ -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) {
const headerProps = column.getHeaderProps(column.getSortByToggleProps());
@@ -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>
);
};
@@ -1,5 +1,4 @@
import { TextAlignProperty } from 'csstype';
import { ComponentType } from 'react';
import { CellProps } from 'react-table';
import { Field } from '@grafana/data';
import { TableStyles } from './styles';
@@ -19,27 +18,13 @@ export enum TableCellDisplayMode {
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 {
[x: string]: any;
}
export interface ReactTableCellProps {
cell: ReactTableCell;
column: TableColumn;
tableStyles: TableStyles;
}
export type TableFilterActionCallback = (key: string, value: string) => void;
export interface ReactTableCell {
value: any;
export interface TableCellProps extends CellProps<any> {
tableStyles: TableStyles;
field: Field;
}
@@ -1,6 +1,5 @@
import { MutableDataFrame, GrafanaThemeType, FieldType } from '@grafana/data';
import { getColumns } from './utils';
import { getTheme } from '../../themes';
import { MutableDataFrame, FieldType } from '@grafana/data';
import { getColumns, getTextAlign } from './utils';
function getData() {
const data = new MutableDataFrame({
@@ -34,33 +33,31 @@ function getData() {
describe('Table utils', () => {
describe('getColumns', () => {
it('Should build columns from DataFrame', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
const columns = getColumns(getData(), 1000);
expect(columns[0].Header).toBe('Time');
expect(columns[1].Header).toBe('Value');
});
it('Should distribute width and use field config width', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
const columns = getColumns(getData(), 1000);
expect(columns[0].width).toBe(450);
expect(columns[1].width).toBe(100);
});
});
describe('getTextAlign', () => {
it('Should use textAlign from custom', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
const data = getData();
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', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
expect(columns[1].textAlign).toBe('right');
const data = getData();
const textAlign = getTextAlign(data.fields[1]);
expect(textAlign).toBe('right');
});
});
});
@@ -1,8 +1,10 @@
import { TextAlignProperty } from 'csstype';
import { DataFrame, Field, GrafanaTheme, FieldType } from '@grafana/data';
import { TableColumn, TableRow, TableFieldOptions, TableCellDisplayMode } from './types';
import { DataFrame, Field, FieldType } from '@grafana/data';
import { Column } from 'react-table';
import { DefaultCell } from './DefaultCell';
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[] {
const tableData = [];
@@ -19,7 +21,7 @@ export function getTableRows(data: DataFrame): TableRow[] {
return tableData;
}
function getTextAlign(field: Field): TextAlignProperty {
export function getTextAlign(field: Field): TextAlignProperty {
if (field.config.custom) {
const custom = field.config.custom as TableFieldOptions;
@@ -40,36 +42,21 @@ function getTextAlign(field: Field): TextAlignProperty {
return 'left';
}
export function getColumns(data: DataFrame, availableWidth: number, theme: GrafanaTheme): TableColumn[] {
const cols: TableColumn[] = [];
export function getColumns(data: DataFrame, availableWidth: number): Column[] {
const columns: Column[] = [];
let fieldCountWithoutWidth = data.fields.length;
for (const field of data.fields) {
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
if (fieldTableOptions.width) {
availableWidth -= fieldTableOptions.width;
fieldCountWithoutWidth -= 1;
}
let Cell = DefaultCell;
let textAlign = getTextAlign(field);
const Cell = getCellComponent(fieldTableOptions.displayMode);
switch (fieldTableOptions.displayMode) {
case TableCellDisplayMode.ColorBackground:
Cell = BackgroundColoredCell;
break;
case TableCellDisplayMode.LcdGauge:
case TableCellDisplayMode.GradientGauge:
Cell = BarGaugeCell;
textAlign = 'center';
break;
}
cols.push({
field,
columns.push({
Cell,
textAlign,
Header: field.name,
accessor: field.name,
width: fieldTableOptions.width,
@@ -78,11 +65,23 @@ export function getColumns(data: DataFrame, availableWidth: number, theme: Grafa
// divide up the rest of the space
const sharedWidth = availableWidth / fieldCountWithoutWidth;
for (const column of cols) {
for (const column of columns) {
if (!column.width) {
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;
}
}