diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 0560a7b6547..4251ad2ac74 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -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", diff --git a/packages/grafana-ui/src/components/Table/BackgroundColorCell.tsx b/packages/grafana-ui/src/components/Table/BackgroundColorCell.tsx new file mode 100644 index 00000000000..14bf6bc0577 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/BackgroundColorCell.tsx @@ -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 = 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
{formattedValueToString(displayValue)}
; +}; diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index c8b85d617a2..0a5c0bfa259 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -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 = props => { - const { column, tableStyles, cell } = props; - const { field } = column; +export const BarGaugeCell: FC = props => { + const { field, column, tableStyles, cell } = props; if (!field.display) { return null; @@ -40,10 +39,17 @@ export const BarGaugeCell: FC = props => { barGaugeMode = BarGaugeDisplayMode.Lcd; } + let width; + if (column.width) { + width = (column.width as number) - tableStyles.cellPadding * 2; + } else { + width = tableStyles.cellPadding * 2; + } + return (
= props => { - const { column, cell, tableStyles } = props; +export const DefaultCell: FC = 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
{formattedValueToString(displayValue)}
; }; - -export const BackgroundColoredCell: FC = 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
{formattedValueToString(displayValue)}
; -}; diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 65bc759552b..fcd776a5c83 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -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(); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 8a0c46f1adb..5229c28f387 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -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 (
- {row.cells.map((cell: RenderCellProps) => renderCell(cell, onCellClick))} + {row.cells.map((cell: Cell, index: number) => ( + + ))}
); }, @@ -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 ( -
- {cell.render('Cell')} -
- ); -} - function renderHeaderCell(column: any, className: string) { const headerProps = column.getHeaderProps(column.getSortByToggleProps()); diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx new file mode 100644 index 00000000000..9c574714067 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -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 = ({ 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 ( +
+ {cell.render('Cell', { field, tableStyles })} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index b48f50778a0..0b778b59929 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -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; - // 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 { + tableStyles: TableStyles; + field: Field; } diff --git a/packages/grafana-ui/src/components/Table/utils.test.ts b/packages/grafana-ui/src/components/Table/utils.test.ts index 45b63a420aa..e29d9566813 100644 --- a/packages/grafana-ui/src/components/Table/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/utils.test.ts @@ -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'); }); }); }); diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index d21bb42493f..b8aa15d3dc4 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -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); - - switch (fieldTableOptions.displayMode) { - case TableCellDisplayMode.ColorBackground: - Cell = BackgroundColoredCell; - break; - case TableCellDisplayMode.LcdGauge: - case TableCellDisplayMode.GradientGauge: - Cell = BarGaugeCell; - textAlign = 'center'; - break; - } + const Cell = getCellComponent(fieldTableOptions.displayMode); - 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; + } } diff --git a/yarn.lock b/yarn.lock index eef00df5914..276d2715d24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3778,43 +3778,6 @@ "@types/d3-interpolate" "*" "@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": version "5.7.2" resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.7.2.tgz#52235eb71a1d3ca171d6dca52a58f5ccbe0254cc" @@ -4290,6 +4253,13 @@ dependencies: "@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@*": version "16.9.1" 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-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: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"