Commit fe22d14e by ryan

cell builder cleanup

parent 078d8f12
......@@ -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', () => {
......
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 <td> with <div>
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('<td>2014-01-01T06:06:06Z</td>');
});
it('time column with epoch as string should be formatted', () => {
const html = renderer.renderCell(0, 0, '1388556366666');
expect(html).toBe('<td>2014-01-01T06:06:06Z</td>');
});
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('<td>2018-12-01T01:00:00Z</td>');
});
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('<td>2018-12-01T01:00:00Z</td>');
});
it('undefined time column should be rendered as -', () => {
const html = renderer.renderCell(0, 0, undefined);
expect(html).toBe('<td>-</td>');
});
it('null time column should be rendered as -', () => {
const html = renderer.renderCell(0, 0, null);
expect(html).toBe('<td>-</td>');
});
it('number column with unit specified should ignore style unit', () => {
const html = renderer.renderCell(5, 0, 1230);
expect(html).toBe('<td>1.23 kbps</td>');
});
it('number column should be formated', () => {
const html = renderer.renderCell(1, 0, 1230);
expect(html).toBe('<td>1.230 s</td>');
});
it('number style should ignore string values', () => {
const html = renderer.renderCell(1, 0, 'asd');
expect(html).toBe('<td>asd</td>');
});
it('colored cell should have style (handles HEX color values)', () => {
const html = renderer.renderCell(2, 0, 40);
expect(html).toBe('<td style="color:#00ff00">40.0</td>');
});
it('colored cell should have style (handles named color values', () => {
const html = renderer.renderCell(2, 0, 55);
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">55.0</td>`);
});
it('colored cell should have style handles(rgb color values)', () => {
const html = renderer.renderCell(2, 0, 85);
expect(html).toBe('<td style="color:rgb(1,0,0)">85.0</td>');
});
it('unformated undefined should be rendered as string', () => {
const html = renderer.renderCell(3, 0, 'value');
expect(html).toBe('<td>value</td>');
});
it('string style with escape html should return escaped html', () => {
const html = renderer.renderCell(4, 0, '&breaking <br /> the <br /> row');
expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
});
it('undefined formater should return escaped html', () => {
const html = renderer.renderCell(3, 0, '&breaking <br /> the <br /> row');
expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
});
it('undefined value should render as -', () => {
const html = renderer.renderCell(3, 0, undefined);
expect(html).toBe('<td></td>');
});
it('sanitized value should render as', () => {
const html = renderer.renderCell(6, 0, 'text <a href="http://google.com">link</a>');
expect(html).toBe('<td>sanitized</td>');
});
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 = `
<td class="table-panel-cell-link">
<a href="/dashboard?param=host1&param_1=1230&param_2=40"
target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right">
host1
</a>
</td>
`;
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('<td>value1, value2</td>');
});
it('numeric value should be mapped to text', () => {
const html = renderer.renderCell(9, 0, 1);
expect(html).toBe('<td>on</td>');
});
it('string numeric value should be mapped to text', () => {
const html = renderer.renderCell(9, 0, '0');
expect(html).toBe('<td>off</td>');
});
it('string value should be mapped to text', () => {
const html = renderer.renderCell(9, 0, 'HELLO WORLD');
expect(html).toBe('<td>HELLO GRAFANA</td>');
});
it('array column value should be mapped to text', () => {
const html = renderer.renderCell(9, 0, ['value1', 'value2']);
expect(html).toBe('<td>value3, value4</td>');
});
it('value should be mapped to text (range)', () => {
const html = renderer.renderCell(10, 0, 2);
expect(html).toBe('<td>on</td>');
});
it('value should be mapped to text (range)', () => {
const html = renderer.renderCell(10, 0, 5);
expect(html).toBe('<td>off</td>');
});
it('array column value should not be mapped to text', () => {
const html = renderer.renderCell(10, 0, ['value1', 'value2']);
expect(html).toBe('<td>value1, value2</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, 1);
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">on</td>`);
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, '1');
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">on</td>`);
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, 0);
expect(html).toBe('<td style="color:#00ff00">off</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, '0');
expect(html).toBe('<td style="color:#00ff00">off</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, '2.1');
expect(html).toBe('<td style="color:rgb(1,0,0)">2.1</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, 0);
expect(html).toBe('<td style="color:#00ff00">0</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, 1);
expect(html).toBe('<td style="color:#00ff00">on</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, 4);
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">off</td>`);
});
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('<td style="color:rgb(1,0,0)">7.1</td>');
});
});
});
function normalize(str: string) {
return str.replace(/\s+/gm, ' ').trim();
}
// 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<Props, State> {
columns: ColumnInfo[] = [];
colorState: any;
_cache: CellMeasurerCache;
columns: ColumnInfo[];
measurer: CellMeasurerCache;
static defaultProps = {
showHeader: true,
......@@ -92,12 +57,11 @@ export class Table extends Component<Props, State> {
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<Props, State> {
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<Props, State> {
}
}
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<Props, State> {
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<Props, State> {
}
};
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 (
<div>
{col.text}{' '}
<div className="gf-table-header" style={style} onClick={() => this.handleCellClick(rowIndex, columnIndex)}>
{col.text}
{sorting && (
<span>
{sortDirection}
......@@ -168,43 +164,22 @@ export class Table extends Component<Props, State> {
};
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 <div>?</div>;
return <div>??</div>;
}
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 isHeader = realRowIndex < 0;
const row = isHeader ? (data.columns as any[]) : data.rows[realRowIndex];
const value = row[columnIndex];
content = (
<div>
{rowIndex}/{columnIndex}: {value}
</div>
);
}
const builder = isHeader ? this.headerBuilder : this.columns[columnIndex].builder;
return (
<CellMeasurer cache={this._cache} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
<div
onClick={() => this.handelClick(rowIndex, columnIndex)}
className={classNames}
style={{
...style,
whiteSpace: 'nowrap',
}}
>
{content}
</div>
<CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
{builder({ value, row, table: this, props })}
</CellMeasurer>
);
};
......@@ -218,16 +193,16 @@ export class Table extends Component<Props, State> {
return (
<MultiGrid
{
...this.state /** Force MultiGrid to update when any property updates */
...this.state /** Force MultiGrid to update when data changes */
}
columnCount={data.columns.length}
rowCount={data.rows.length + (showHeader ? 1 : 0)}
overscanColumnCount={2}
overscanRowCount={2}
columnWidth={this._cache.columnWidth}
deferredMeasurementCache={this._cache}
columnWidth={this.measurer.columnWidth}
deferredMeasurementCache={this.measurer}
cellRenderer={this.cellRenderer}
rowHeight={this._cache.rowHeight}
rowHeight={this.measurer.rowHeight}
width={width}
height={height}
fixedColumnCount={fixedColumnCount}
......
// Libraries
import _ from 'lodash';
import React, { ReactElement } from 'react';
import { GridCellProps } from 'react-virtualized';
import { Table, Props } from './Table';
import moment from 'moment';
import { ValueFormatter } from '../../utils/index';
import { GrafanaTheme } from '../../types/theme';
import { getValueFormat, getColorFromHexRgbOrName, Column } from '@grafana/ui';
import { InterpolateFunction } from '../../types/panel';
export interface TableCellBuilderOptions {
value: any;
row?: any[];
table?: Table;
className?: string;
props: GridCellProps;
}
export type TableCellBuilder = (cell: TableCellBuilderOptions) => 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 (
<div style={style} className={className}>
{value}
</div>
);
};
// ***************************************************************************
// 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 = (
<a
href={cellLink}
target={cellTarget}
data-link-tooltip
data-original-title={cellLinkTooltip}
data-placement="right"
>
{value}
</a>
);
}
// ??? I don't think this will still work!
if (this.column.filterable) {
cellClasses.push('table-panel-cell-filterable');
value = (
<>
{value}
<span>
<a
className="table-panel-filter-link"
data-link-tooltip
data-original-title="Filter out value"
data-placement="bottom"
data-row={props.rowIndex}
data-column={props.columnIndex}
data-operator="!="
>
<i className="fa fa-search-minus" />
</a>
<a
className="table-panel-filter-link"
data-link-tooltip
data-original-title="Filter for value"
data-placement="bottom"
data-row={props.rowIndex}
data-column={props.columnIndex}
data-operator="="
>
<i className="fa fa-search-plus" />
</a>
</span>
</>
);
}
let className;
if (cellClasses.length) {
className = cellClasses.join(' ');
}
return simpleCellBuilder({ value, props, className });
};
}
......@@ -59,6 +59,7 @@
border-bottom: 2px solid $body-bg;
cursor: pointer;
white-space: nowrap;
color: $blue;
}
......
import { TableData } from '../../types/data';
import { ColumnStyle } from './Table';
import { ColumnStyle } from './TableCellBuilder';
import { getColorDefinitionByName } from '@grafana/ui';
......
import React from 'react';
import { AutoSizer } from 'react-virtualized';
/** This will add full size with & height properties */
export const withFullSizeStory = (component: React.ComponentType<any>, props: any) => (
<div
style={{
......
......@@ -2,7 +2,7 @@ import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType } from '@grafana/ui';
import { ColumnStyle } from '@grafana/ui/src/components/Table/Table';
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
export class TableRenderer {
formatters: any[];
......
import { ColumnStyle } from '@grafana/ui/src/components/Table/Table';
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
export interface Options {
showHeader: boolean;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment