From fe22d14e933f19cb3a18252507d8d79541a9b3b2 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 9 Mar 2019 20:47:02 -0800 Subject: [PATCH] cell builder cleanup --- .../src/components/Table/Table.story.tsx | 26 +- .../src/components/Table/Table.test.ts | 240 --------- .../grafana-ui/src/components/Table/Table.tsx | 147 +++--- .../src/components/Table/TableCellBuilder.tsx | 292 +++++++++++ .../src/components/Table/TableXXXX.tsx | 456 ------------------ .../src/components/Table/_Table.scss | 1 + .../src/components/Table/examples.ts | 2 +- .../src/utils/storybook/withFullSizeStory.tsx | 1 + public/app/plugins/panel/table/renderer.ts | 2 +- public/app/plugins/panel/table2/types.ts | 2 +- 10 files changed, 375 insertions(+), 794 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Table/Table.test.ts create mode 100644 packages/grafana-ui/src/components/Table/TableCellBuilder.tsx delete mode 100644 packages/grafana-ui/src/components/Table/TableXXXX.tsx diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 8cd97dcefa9..1aa618d73d7 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -5,24 +5,32 @@ import { Table } from './Table'; import { migratedTestTable, migratedTestStyles, simpleTable } from './examples'; import { ScopedVars, TableData } from '../../types/index'; import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory'; +import { number, boolean } from '@storybook/addon-knobs'; -const replaceVariables = (value: any, scopedVars: ScopedVars | undefined) => { - // if (scopedVars) { - // // For testing variables replacement in link - // _.each(scopedVars, (val, key) => { - // value = value.replace('$' + key, val.value); - // }); - // } +const replaceVariables = (value: string, scopedVars?: ScopedVars) => { + if (scopedVars) { + // For testing variables replacement in link + for (const key in scopedVars) { + const val = scopedVars[key]; + value = value.replace('$' + key, val.value); + } + } return value; }; -storiesOf('UI - Alpha/Table', module) +storiesOf('UI/Table', module) .add('basic', () => { + const showHeader = boolean('Show Header', true); + const fixedRowCount = number('Fixed Rows', 1); + const fixedColumnCount = number('Fixed Columns', 1); + return withFullSizeStory(Table, { styles: [], data: simpleTable, replaceVariables, - showHeader: true, + fixedRowCount, + fixedColumnCount, + showHeader, }); }) .add('Test Configuration', () => { diff --git a/packages/grafana-ui/src/components/Table/Table.test.ts b/packages/grafana-ui/src/components/Table/Table.test.ts deleted file mode 100644 index e53df39750e..00000000000 --- a/packages/grafana-ui/src/components/Table/Table.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import _ from 'lodash'; - -import { getColorDefinitionByName } from '@grafana/ui'; -import { ScopedVars } from '@grafana/ui/src/types'; -import { getTheme } from '../../themes'; - -import { migratedTestTable, migratedTestStyles } from './examples'; -import TableXXXX from './TableXXXX'; - -// TODO: this is commented out with *x* describe! -// Essentially all the elements need to replace the with
-xdescribe('when rendering table', () => { - const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange'); - - describe('given 13 columns', () => { - // const sanitize = value => { - // return 'sanitized'; - // }; - - const replaceVariables = (value: any, scopedVars: ScopedVars | undefined) => { - if (scopedVars) { - // For testing variables replacement in link - _.each(scopedVars, (val, key) => { - value = value.replace('$' + key, val.value); - }); - } - return value; - }; - - const table = migratedTestTable; - const renderer = new TableXXXX({ - styles: migratedTestStyles, - data: migratedTestTable, - replaceVariables, - showHeader: true, - width: 100, - height: 100, - theme: getTheme(), - }); - - it('time column should be formated', () => { - const html = renderer.renderCell(0, 0, 1388556366666); - expect(html).toBe('2014-01-01T06:06:06Z'); - }); - - it('time column with epoch as string should be formatted', () => { - const html = renderer.renderCell(0, 0, '1388556366666'); - expect(html).toBe('2014-01-01T06:06:06Z'); - }); - - it('time column with RFC2822 date as string should be formatted', () => { - const html = renderer.renderCell(0, 0, 'Sat, 01 Dec 2018 01:00:00 GMT'); - expect(html).toBe('2018-12-01T01:00:00Z'); - }); - - it('time column with ISO date as string should be formatted', () => { - const html = renderer.renderCell(0, 0, '2018-12-01T01:00:00Z'); - expect(html).toBe('2018-12-01T01:00:00Z'); - }); - - it('undefined time column should be rendered as -', () => { - const html = renderer.renderCell(0, 0, undefined); - expect(html).toBe('-'); - }); - - it('null time column should be rendered as -', () => { - const html = renderer.renderCell(0, 0, null); - expect(html).toBe('-'); - }); - - it('number column with unit specified should ignore style unit', () => { - const html = renderer.renderCell(5, 0, 1230); - expect(html).toBe('1.23 kbps'); - }); - - it('number column should be formated', () => { - const html = renderer.renderCell(1, 0, 1230); - expect(html).toBe('1.230 s'); - }); - - it('number style should ignore string values', () => { - const html = renderer.renderCell(1, 0, 'asd'); - expect(html).toBe('asd'); - }); - - it('colored cell should have style (handles HEX color values)', () => { - const html = renderer.renderCell(2, 0, 40); - expect(html).toBe('40.0'); - }); - - it('colored cell should have style (handles named color values', () => { - const html = renderer.renderCell(2, 0, 55); - expect(html).toBe(`55.0`); - }); - - it('colored cell should have style handles(rgb color values)', () => { - const html = renderer.renderCell(2, 0, 85); - expect(html).toBe('85.0'); - }); - - it('unformated undefined should be rendered as string', () => { - const html = renderer.renderCell(3, 0, 'value'); - expect(html).toBe('value'); - }); - - it('string style with escape html should return escaped html', () => { - const html = renderer.renderCell(4, 0, '&breaking
the
row'); - expect(html).toBe('&breaking <br /> the <br /> row'); - }); - - it('undefined formater should return escaped html', () => { - const html = renderer.renderCell(3, 0, '&breaking
the
row'); - expect(html).toBe('&breaking <br /> the <br /> row'); - }); - - it('undefined value should render as -', () => { - const html = renderer.renderCell(3, 0, undefined); - expect(html).toBe(''); - }); - - it('sanitized value should render as', () => { - const html = renderer.renderCell(6, 0, 'text link'); - expect(html).toBe('sanitized'); - }); - - it('Time column title should be Timestamp', () => { - expect(table.columns[0].title).toBe('Timestamp'); - }); - - it('Value column title should be Val', () => { - expect(table.columns[1].title).toBe('Val'); - }); - - it('Colored column title should be Colored', () => { - expect(table.columns[2].title).toBe('Colored'); - }); - - it('link should render as', () => { - const html = renderer.renderCell(7, 0, 'host1'); - const expectedHtml = ` - - - host1 - - - `; - expect(normalize(html + '')).toBe(normalize(expectedHtml)); - }); - - it('Array column should not use number as formatter', () => { - const html = renderer.renderCell(8, 0, ['value1', 'value2']); - expect(html).toBe('value1, value2'); - }); - - it('numeric value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, 1); - expect(html).toBe('on'); - }); - - it('string numeric value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, '0'); - expect(html).toBe('off'); - }); - - it('string value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, 'HELLO WORLD'); - expect(html).toBe('HELLO GRAFANA'); - }); - - it('array column value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, ['value1', 'value2']); - expect(html).toBe('value3, value4'); - }); - - it('value should be mapped to text (range)', () => { - const html = renderer.renderCell(10, 0, 2); - expect(html).toBe('on'); - }); - - it('value should be mapped to text (range)', () => { - const html = renderer.renderCell(10, 0, 5); - expect(html).toBe('off'); - }); - - it('array column value should not be mapped to text', () => { - const html = renderer.renderCell(10, 0, ['value1', 'value2']); - expect(html).toBe('value1, value2'); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, 1); - expect(html).toBe(`on`); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, '1'); - expect(html).toBe(`on`); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, 0); - expect(html).toBe('off'); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, '0'); - expect(html).toBe('off'); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, '2.1'); - expect(html).toBe('2.1'); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, 0); - expect(html).toBe('0'); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, 1); - expect(html).toBe('on'); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, 4); - expect(html).toBe(`off`); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, '7.1'); - expect(html).toBe('7.1'); - }); - }); -}); - -function normalize(str: string) { - return str.replace(/\s+/gm, ' ').trim(); -} diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index cbc0b50ebc0..2a75c33224e 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -1,6 +1,6 @@ // Libraries import _ from 'lodash'; -import React, { Component, ReactNode } from 'react'; +import React, { Component, ReactElement } from 'react'; import { SortDirectionType, SortIndicator, @@ -14,49 +14,16 @@ import { Themeable } from '../../types/theme'; import { sortTableData } from '../../utils/processTimeSeries'; import { TableData, InterpolateFunction } from '@grafana/ui'; -import { ColumnStyle } from './Table'; - -// APP Imports!!! -// import kbn from 'app/core/utils/kbn'; - -// Made to match the existing (untyped) settings in the angular table -export interface ColumnStyle { - pattern?: string; - - alias?: string; - colorMode?: 'cell' | 'value'; - colors?: any[]; - decimals?: number; - thresholds?: any[]; - type?: 'date' | 'number' | 'string' | 'hidden'; - unit?: string; - dateFormat?: string; - sanitize?: boolean; // not used in react - mappingType?: any; - valueMaps?: any; - rangeMaps?: any; - - link?: any; - linkUrl?: any; - linkTooltip?: any; - linkTargetBlank?: boolean; - - preserveFormat?: boolean; -} - -type CellFormatter = (v: any, style?: ColumnStyle) => ReactNode; +import { TableCellBuilder, ColumnStyle, getCellBuilder, TableCellBuilderOptions } from './TableCellBuilder'; interface ColumnInfo { + index: number; header: string; - accessor: string; // the field name - style?: ColumnStyle; - hidden?: boolean; - formatter: CellFormatter; - filterable?: boolean; + builder: TableCellBuilder; } -interface Props extends Themeable { - data?: TableData; +export interface Props extends Themeable { + data: TableData; showHeader: boolean; fixedColumnCount: number; fixedRowCount: number; @@ -70,14 +37,12 @@ interface Props extends Themeable { interface State { sortBy?: number; sortDirection?: SortDirectionType; - data?: TableData; + data: TableData; } export class Table extends Component { - columns: ColumnInfo[] = []; - colorState: any; - - _cache: CellMeasurerCache; + columns: ColumnInfo[]; + measurer: CellMeasurerCache; static defaultProps = { showHeader: true, @@ -92,12 +57,11 @@ export class Table extends Component { data: props.data, }; - this._cache = new CellMeasurerCache({ + this.columns = this.initColumns(props); + this.measurer = new CellMeasurerCache({ defaultHeight: 30, defaultWidth: 150, }); - - this.initRenderer(); } componentDidUpdate(prevProps: Props, prevState: State) { @@ -105,9 +69,14 @@ export class Table extends Component { const { sortBy, sortDirection } = this.state; const dataChanged = data !== prevProps.data; + // Reset the size cache + if (dataChanged) { + this.measurer.clearAll(); + } + // Update the renderer if options change if (dataChanged || styles !== prevProps.styles) { - this.initRenderer(); + this.columns = this.initColumns(this.props); } // Update the data when data or sort changes @@ -117,7 +86,32 @@ export class Table extends Component { } } - initRenderer() {} + initColumns(props: Props): ColumnInfo[] { + const { styles, data } = props; + return data.columns.map((col, index) => { + let title = col.text; + let style: ColumnStyle | null = null; // ColumnStyle + + // Find the style based on the text + for (let i = 0; i < styles.length; i++) { + const s = styles[i]; + const regex = 'XXX'; //kbn.stringToJsRegex(s.pattern); + if (title.match(regex)) { + style = s; + if (s.alias) { + title = title.replace(regex, s.alias); + } + break; + } + } + + return { + index, + header: title, + builder: getCellBuilder(col, style, this.props), + }; + }); + } //---------------------------------------------------------------------- //---------------------------------------------------------------------- @@ -136,7 +130,7 @@ export class Table extends Component { this.setState({ sortBy: sort, sortDirection: dir }); }; - handelClick = (rowIndex: number, columnIndex: number) => { + handleCellClick = (rowIndex: number, columnIndex: number) => { const { showHeader } = this.props; const { data } = this.state; const realRowIndex = rowIndex - (showHeader ? 1 : 0); @@ -149,14 +143,16 @@ export class Table extends Component { } }; - headerRenderer = (columnIndex: number): ReactNode => { + headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => { const { data, sortBy, sortDirection } = this.state; + const { columnIndex, rowIndex, style } = cell.props; + const col = data!.columns[columnIndex]; const sorting = sortBy === columnIndex; return ( -
- {col.text}{' '} +
this.handleCellClick(rowIndex, columnIndex)}> + {col.text} {sorting && ( {sortDirection} @@ -168,43 +164,22 @@ export class Table extends Component { }; cellRenderer = (props: GridCellProps): React.ReactNode => { - const { rowIndex, columnIndex, key, parent, style } = props; + const { rowIndex, columnIndex, key, parent } = props; const { showHeader } = this.props; const { data } = this.state; if (!data) { - return
?
; + return
??
; } const realRowIndex = rowIndex - (showHeader ? 1 : 0); - - let classNames = 'gf-table-cell'; - let content = null; - - if (realRowIndex < 0) { - content = this.headerRenderer(columnIndex); - classNames = 'gf-table-header'; - } else { - const row = data.rows[realRowIndex]; - const value = row[columnIndex]; - content = ( -
- {rowIndex}/{columnIndex}: {value} -
- ); - } + const isHeader = realRowIndex < 0; + const row = isHeader ? (data.columns as any[]) : data.rows[realRowIndex]; + const value = row[columnIndex]; + const builder = isHeader ? this.headerBuilder : this.columns[columnIndex].builder; return ( - -
this.handelClick(rowIndex, columnIndex)} - className={classNames} - style={{ - ...style, - whiteSpace: 'nowrap', - }} - > - {content} -
+ + {builder({ value, row, table: this, props })} ); }; @@ -218,16 +193,16 @@ export class Table extends Component { return ( ReactElement<'div'>; + +/** Simplest cell that just spits out the value */ +export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => { + const { props, value, className } = cell; + const { style } = props; + + return ( +
+ {value} +
+ ); +}; + +// *************************************************************************** +// HERE BE DRAGONS!!! +// *************************************************************************** +// +// The following code has been migrated blindy two times from the angular +// table panel. I don't understand all the options nor do I know if they +// are correct! +// +// *************************************************************************** + +// APP Imports!!! +// import kbn from 'app/core/utils/kbn'; + +// Made to match the existing (untyped) settings in the angular table +export interface ColumnStyle { + pattern?: string; + + alias?: string; + colorMode?: 'cell' | 'value'; + colors?: any[]; + decimals?: number; + thresholds?: any[]; + type?: 'date' | 'number' | 'string' | 'hidden'; + unit?: string; + dateFormat?: string; + sanitize?: boolean; // not used in react + mappingType?: any; + valueMaps?: any; + rangeMaps?: any; + + link?: any; + linkUrl?: any; + linkTooltip?: any; + linkTargetBlank?: boolean; + + preserveFormat?: boolean; +} + +// private mapper:ValueMapper, +// private style:ColumnStyle, +// private theme:GrafanaTheme, +// private column:Column, +// private replaceVariables: InterpolateFunction, +// private fmt?:ValueFormatter) { + +export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: Props): TableCellBuilder { + if (!style) { + return simpleCellBuilder; + } + + if (style.type === 'hidden') { + // TODO -- for hidden, we either need to: + // 1. process the Table and remove hidden fields + // 2. do special math to pick the right column skipping hidden fields + throw new Error('hidden not supported!'); + } + + if (style.type === 'date') { + return new CellBuilderWithStyle( + (v: any) => { + if (v === undefined || v === null) { + return '-'; + } + + if (_.isArray(v)) { + v = v[0]; + } + let date = moment(v); + if (false) { + // TODO?????? this.props.isUTC) { + date = date.utc(); + } + return date.format(style.dateFormat); + }, + style, + props.theme, + schema, + props.replaceVariables + ).build; + } + + if (style.type === 'string') { + return new CellBuilderWithStyle( + (v: any) => { + if (_.isArray(v)) { + v = v.join(', '); + } + return v; + }, + style, + props.theme, + schema, + props.replaceVariables + ).build; + // TODO!!!! all the mapping stuff!!!! + } + + if (style.type === 'number') { + const valueFormatter = getValueFormat(style.unit || schema.unit || 'none'); + return new CellBuilderWithStyle( + (v: any) => { + if (v === null || v === void 0) { + return '-'; + } + return v; + }, + style, + props.theme, + schema, + props.replaceVariables, + valueFormatter + ).build; + } + + return simpleCellBuilder; +} + +type ValueMapper = (value: any) => any; + +// Runs the value through a formatter and adds colors to the cell properties +class CellBuilderWithStyle { + constructor( + private mapper: ValueMapper, + private style: ColumnStyle, + private theme: GrafanaTheme, + private column: Column, + private replaceVariables: InterpolateFunction, + private fmt?: ValueFormatter + ) { + // + } + + getColorForValue = (value: any): string | null => { + const { thresholds, colors } = this.style; + if (!thresholds || !colors) { + return null; + } + + for (let i = thresholds.length; i > 0; i--) { + if (value >= thresholds[i - 1]) { + return getColorFromHexRgbOrName(colors[i], this.theme.type); + } + } + return getColorFromHexRgbOrName(_.first(colors), this.theme.type); + }; + + build = (cell: TableCellBuilderOptions) => { + let { props } = cell; + let value = this.mapper(cell.value); + + if (_.isNumber(value)) { + if (this.fmt) { + value = this.fmt(value, this.style.decimals); + } + + // For numeric values set the color + const { colorMode } = this.style; + if (colorMode) { + const color = this.getColorForValue(Number(value)); + if (color) { + if (colorMode === 'cell') { + props = { + ...props, + style: { + ...props.style, + backgroundColor: color, + color: 'white', + }, + }; + } else if (colorMode === 'value') { + props = { + ...props, + style: { + ...props.style, + color: color, + }, + }; + } + } + } + } + + const cellClasses = []; + if (this.style.preserveFormat) { + cellClasses.push('table-panel-cell-pre'); + } + + if (this.style.link) { + // Render cell as link + const { row } = cell; + + const scopedVars: any = {}; + if (row) { + for (let i = 0; i < row.length; i++) { + scopedVars[`__cell_${i}`] = { value: row[i] }; + } + } + scopedVars['__cell'] = { value: value }; + + const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent); + const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars); + const cellTarget = this.style.linkTargetBlank ? '_blank' : ''; + + cellClasses.push('table-panel-cell-link'); + value = ( + + {value} + + ); + } + + // ??? I don't think this will still work! + if (this.column.filterable) { + cellClasses.push('table-panel-cell-filterable'); + value = ( + <> + {value} + + + + + + + + + + ); + } + + let className; + if (cellClasses.length) { + className = cellClasses.join(' '); + } + + return simpleCellBuilder({ value, props, className }); + }; +} diff --git a/packages/grafana-ui/src/components/Table/TableXXXX.tsx b/packages/grafana-ui/src/components/Table/TableXXXX.tsx deleted file mode 100644 index 4c78c3b336b..00000000000 --- a/packages/grafana-ui/src/components/Table/TableXXXX.tsx +++ /dev/null @@ -1,456 +0,0 @@ -// Libraries -import _ from 'lodash'; -import React, { Component, CSSProperties, ReactNode } from 'react'; -import { - Table as RVTable, - SortDirectionType, - SortIndicator, - Column as RVColumn, - TableHeaderProps, - TableCellProps, -} from 'react-virtualized'; -import { Themeable } from '../../types/theme'; - -import { sortTableData } from '../../utils/processTimeSeries'; - -import moment from 'moment'; - -import { getValueFormat, TableData, getColorFromHexRgbOrName, InterpolateFunction, Column } from '@grafana/ui'; -import { Index } from 'react-virtualized'; -import { ColumnStyle } from './Table'; - -type CellFormatter = (v: any, style?: ColumnStyle) => ReactNode; - -interface ColumnInfo { - header: string; - accessor: string; // the field name - style?: ColumnStyle; - hidden?: boolean; - formatter: CellFormatter; - filterable?: boolean; -} - -interface Props extends Themeable { - data?: TableData; - showHeader: boolean; - styles: ColumnStyle[]; - replaceVariables: InterpolateFunction; - width: number; - height: number; - isUTC?: boolean; -} - -interface State { - sortBy?: number; - sortDirection?: SortDirectionType; - data?: TableData; -} - -export class TableXXXX extends Component { - columns: ColumnInfo[] = []; - colorState: any; - - static defaultProps = { - showHeader: true, - }; - - constructor(props: Props) { - super(props); - - this.state = { - data: props.data, - }; - - this.initRenderer(); - } - - componentDidUpdate(prevProps: Props, prevState: State) { - const { data, styles } = this.props; - const { sortBy, sortDirection } = this.state; - const dataChanged = data !== prevProps.data; - - // Update the renderer if options change - if (dataChanged || styles !== prevProps.styles) { - this.initRenderer(); - } - - // Update the data when data or sort changes - if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { - const sorted = data ? sortTableData(data, sortBy, sortDirection === 'DESC') : data; - this.setState({ data: sorted }); - } - } - - initRenderer() { - const { styles } = this.props; - const { data } = this.state; - this.colorState = {}; - if (!data || !data.columns) { - this.columns = []; - return; - } - this.columns = data.columns.map((col, index) => { - let title = col.text; - let style; // ColumnStyle - - // Find the style based on the text - for (let i = 0; i < styles.length; i++) { - const s = styles[i]; - const regex = 'XXX'; //kbn.stringToJsRegex(s.pattern); - if (title.match(regex)) { - style = s; - if (s.alias) { - title = title.replace(regex, s.alias); - } - break; - } - } - - return { - header: title, - accessor: col.text, // unique? - style: style, - formatter: this.createColumnFormatter(col, style), - }; - }); - } - - //---------------------------------------------------------------------- - // renderer.ts copy (taken from angular version!!!) - //---------------------------------------------------------------------- - - getColorForValue(value: any, style: ColumnStyle) { - if (!style.thresholds || !style.colors) { - return null; - } - const { theme } = this.props; - - for (let i = style.thresholds.length; i > 0; i--) { - if (value >= style.thresholds[i - 1]) { - return getColorFromHexRgbOrName(style.colors[i], theme.type); - } - } - return getColorFromHexRgbOrName(_.first(style.colors), theme.type); - } - - defaultCellFormatter(v: any, style?: ColumnStyle): string { - if (v === null || v === void 0 || v === undefined) { - return ''; - } - - if (_.isArray(v)) { - v = v.join(', '); - } - - return v; // react will sanitize - } - - createColumnFormatter(schema: Column, style?: ColumnStyle): CellFormatter { - if (!style) { - return this.defaultCellFormatter; - } - - if (style.type === 'hidden') { - return v => { - return undefined; - }; - } - - if (style.type === 'date') { - return v => { - if (v === undefined || v === null) { - return '-'; - } - - if (_.isArray(v)) { - v = v[0]; - } - let date = moment(v); - if (this.props.isUTC) { - date = date.utc(); - } - return date.format(style.dateFormat); - }; - } - - if (style.type === 'string') { - return v => { - if (_.isArray(v)) { - v = v.join(', '); - } - - const mappingType = style.mappingType || 0; - - if (mappingType === 1 && style.valueMaps) { - for (let i = 0; i < style.valueMaps.length; i++) { - const map = style.valueMaps[i]; - - if (v === null) { - if (map.value === 'null') { - return map.text; - } - continue; - } - - // Allow both numeric and string values to be mapped - if ((!_.isString(v) && Number(map.value) === Number(v)) || map.value === v) { - this.setColorState(v, style); - return this.defaultCellFormatter(map.text, style); - } - } - } - - if (mappingType === 2 && style.rangeMaps) { - for (let i = 0; i < style.rangeMaps.length; i++) { - const map = style.rangeMaps[i]; - - if (v === null) { - if (map.from === 'null' && map.to === 'null') { - return map.text; - } - continue; - } - - if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) { - this.setColorState(v, style); - return this.defaultCellFormatter(map.text, style); - } - } - } - - if (v === null || v === void 0) { - return '-'; - } - - this.setColorState(v, style); - return this.defaultCellFormatter(v, style); - }; - } - - if (style.type === 'number') { - const valueFormatter = getValueFormat(style.unit || schema.unit || 'none'); - - return v => { - if (v === null || v === void 0) { - return '-'; - } - - if (_.isString(v) || _.isArray(v)) { - return this.defaultCellFormatter(v, style); - } - - this.setColorState(v, style); - return valueFormatter(v, style.decimals, null); - }; - } - - return value => { - return this.defaultCellFormatter(value, style); - }; - } - - setColorState(value: any, style: ColumnStyle) { - if (!style.colorMode) { - return; - } - - if (value === null || value === void 0 || _.isArray(value)) { - return; - } - - if (_.isNaN(value)) { - return; - } - const numericValue = Number(value); - this.colorState[style.colorMode] = this.getColorForValue(numericValue, style); - } - - renderRowVariables(rowIndex: number) { - const scopedVars: any = {}; - const row = this.rowGetter({ index: rowIndex }); - for (let i = 0; i < row.length; i++) { - scopedVars[`__cell_${i}`] = { value: row[i] }; - } - return scopedVars; - } - - renderCell(columnIndex: number, rowIndex: number, value: any): ReactNode { - const column = this.columns[columnIndex]; - if (column.formatter) { - value = column.formatter(value, column.style); - } - - const style: CSSProperties = {}; - const cellClasses = []; - let cellClass = ''; - - if (this.colorState.cell) { - style.backgroundColor = this.colorState.cell; - style.color = 'white'; - this.colorState.cell = null; - } else if (this.colorState.value) { - style.color = this.colorState.value; - this.colorState.value = null; - } - - if (value === undefined) { - style.display = 'none'; - column.hidden = true; - } else { - column.hidden = false; - } - - if (column.style && column.style.preserveFormat) { - cellClasses.push('table-panel-cell-pre'); - } - - let columnHtml: JSX.Element; - if (column.style && column.style.link) { - // Render cell as link - const { replaceVariables } = this.props; - const scopedVars = this.renderRowVariables(rowIndex); - scopedVars['__cell'] = { value: value }; - - const cellLink = replaceVariables(column.style.linkUrl, scopedVars, encodeURIComponent); - const cellLinkTooltip = replaceVariables(column.style.linkTooltip, scopedVars); - const cellTarget = column.style.linkTargetBlank ? '_blank' : ''; - - cellClasses.push('table-panel-cell-link'); - columnHtml = ( - - {value} - - ); - } else { - columnHtml = {value}; - } - - let filterLink: JSX.Element | null = null; - if (column.filterable) { - cellClasses.push('table-panel-cell-filterable'); - filterLink = ( - - - - - - - - - ); - } - - if (cellClasses.length) { - cellClass = cellClasses.join(' '); - } - - style.width = '100%'; - style.height = '100%'; - columnHtml = ( -
- {columnHtml} - {filterLink} -
- ); - return columnHtml; - } - - //---------------------------------------------------------------------- - //---------------------------------------------------------------------- - - rowGetter = ({ index }: Index) => { - return this.state.data!.rows[index]; - }; - - doSort = (info: any) => { - let dir = info.sortDirection; - let sort = info.sortBy; - if (sort !== this.state.sortBy) { - dir = 'DESC'; - } else if (dir === 'DESC') { - dir = 'ASC'; - } else { - sort = null; - } - this.setState({ sortBy: sort, sortDirection: dir }); - }; - - headerRenderer = (header: TableHeaderProps): ReactNode => { - const dataKey = header.dataKey as any; // types say string, but it is number! - const { data, sortBy, sortDirection } = this.state; - const col = data!.columns[dataKey]; - - return ( -
- {col.text} {sortBy === dataKey && } -
- ); - }; - - cellRenderer = (cell: TableCellProps) => { - const { columnIndex, rowIndex } = cell; - const row = this.state.data!.rows[rowIndex]; - const val = row[columnIndex]; - return this.renderCell(columnIndex, rowIndex, val); - }; - - render() { - const { width, height, showHeader } = this.props; - const { data } = this.props; - if (!data) { - return
NO Data
; - } - - return ( - - {data.columns.map((col, index) => { - return ( - - ); - })} - - ); - } -} - -export default TableXXXX; diff --git a/packages/grafana-ui/src/components/Table/_Table.scss b/packages/grafana-ui/src/components/Table/_Table.scss index f9cb0271561..22170ecf913 100644 --- a/packages/grafana-ui/src/components/Table/_Table.scss +++ b/packages/grafana-ui/src/components/Table/_Table.scss @@ -59,6 +59,7 @@ border-bottom: 2px solid $body-bg; cursor: pointer; + white-space: nowrap; color: $blue; } diff --git a/packages/grafana-ui/src/components/Table/examples.ts b/packages/grafana-ui/src/components/Table/examples.ts index 026c5446f6b..9f05488d839 100644 --- a/packages/grafana-ui/src/components/Table/examples.ts +++ b/packages/grafana-ui/src/components/Table/examples.ts @@ -1,5 +1,5 @@ import { TableData } from '../../types/data'; -import { ColumnStyle } from './Table'; +import { ColumnStyle } from './TableCellBuilder'; import { getColorDefinitionByName } from '@grafana/ui'; diff --git a/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx b/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx index c6efbee462f..98c3ccb2e7f 100644 --- a/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx +++ b/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { AutoSizer } from 'react-virtualized'; +/** This will add full size with & height properties */ export const withFullSizeStory = (component: React.ComponentType, props: any) => (