Commit 91a2307b by ryan

rotate!

parent b80c773e
...@@ -19,17 +19,26 @@ const replaceVariables = (value: string, scopedVars?: ScopedVars) => { ...@@ -19,17 +19,26 @@ const replaceVariables = (value: string, scopedVars?: ScopedVars) => {
return value; return value;
}; };
export function makeDummyTable(columnCount: number, rowCount: number): TableData { export function columnIndexToLeter(column: number) {
const A = 'A'.charCodeAt(0); const A = 'A'.charCodeAt(0);
const c1 = Math.floor(column / 26);
const c2 = column % 26;
if (c1 > 0) {
return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2);
}
return String.fromCharCode(A + c2);
}
export function makeDummyTable(columnCount: number, rowCount: number): TableData {
return { return {
columns: Array.from(new Array(columnCount), (x, i) => { columns: Array.from(new Array(columnCount), (x, i) => {
return { return {
text: String.fromCharCode(A + i), text: columnIndexToLeter(i),
}; };
}), }),
rows: Array.from(new Array(rowCount), (x, rowId) => { rows: Array.from(new Array(rowCount), (x, rowId) => {
const suffix = (rowId + 1).toString(); const suffix = (rowId + 1).toString();
return Array.from(new Array(columnCount), (x, colId) => String.fromCharCode(A + colId) + suffix); return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
}), }),
type: 'table', type: 'table',
columnMap: {}, columnMap: {},
...@@ -37,45 +46,54 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData ...@@ -37,45 +46,54 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
} }
storiesOf('Alpha/Table', module) storiesOf('Alpha/Table', module)
.add('basic', () => { .add('Basic Table', () => {
// NOTE: This example does not seem to survice rotate &
// Changing fixed headers... but the next one does?
// perhaps `simpleTable` is static and reused?
const showHeader = boolean('Show Header', true); const showHeader = boolean('Show Header', true);
const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); const fixedHeader = boolean('Fixed Header', true);
const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false });
const rotate = boolean('Rotate', false);
return withFullSizeStory(Table, { return withFullSizeStory(Table, {
styles: [], styles: [],
data: simpleTable, data: simpleTable,
replaceVariables, replaceVariables,
fixedRowCount,
fixedColumnCount,
showHeader, showHeader,
fixedHeader,
fixedColumns,
rotate,
theme: getTheme(GrafanaThemeType.Light), theme: getTheme(GrafanaThemeType.Light),
}); });
}) })
.add('variable size', () => { .add('Variable Size', () => {
const columnCount = number('Column Count', 20, { min: 2, max: 50, step: 1, range: false }); const columnCount = number('Column Count', 15, { min: 2, max: 50, step: 1, range: false });
const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false }); const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false });
const showHeader = boolean('Show Header', true); const showHeader = boolean('Show Header', true);
const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); const fixedHeader = boolean('Fixed Header', true);
const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false });
const rotate = boolean('Rotate', false);
return withFullSizeStory(Table, { return withFullSizeStory(Table, {
styles: [], styles: [],
data: makeDummyTable(columnCount, rowCount), data: makeDummyTable(columnCount, rowCount),
replaceVariables, replaceVariables,
fixedRowCount,
fixedColumnCount,
showHeader, showHeader,
fixedHeader,
fixedColumns,
rotate,
theme: getTheme(GrafanaThemeType.Light), theme: getTheme(GrafanaThemeType.Light),
}); });
}) })
.add('Old tests configuration', () => { .add('Test Config (migrated)', () => {
return withFullSizeStory(Table, { return withFullSizeStory(Table, {
styles: migratedTestStyles, styles: migratedTestStyles,
data: migratedTestTable, data: migratedTestTable,
replaceVariables, replaceVariables,
showHeader: true, showHeader: true,
rotate: true,
theme: getTheme(GrafanaThemeType.Light), theme: getTheme(GrafanaThemeType.Light),
}); });
}); });
...@@ -14,21 +14,24 @@ import { Themeable } from '../../types/theme'; ...@@ -14,21 +14,24 @@ import { Themeable } from '../../types/theme';
import { sortTableData } from '../../utils/processTimeSeries'; import { sortTableData } from '../../utils/processTimeSeries';
import { TableData, InterpolateFunction } from '@grafana/ui'; import { TableData, InterpolateFunction } from '@grafana/ui';
import { TableCellBuilder, ColumnStyle, getCellBuilder, TableCellBuilderOptions } from './TableCellBuilder'; import {
TableCellBuilder,
ColumnStyle,
getCellBuilder,
TableCellBuilderOptions,
simpleCellBuilder,
} from './TableCellBuilder';
import { stringToJsRegex } from '../../utils/index'; import { stringToJsRegex } from '../../utils/index';
interface ColumnInfo {
index: number;
header: string;
builder: TableCellBuilder;
}
export interface Props extends Themeable { export interface Props extends Themeable {
data: TableData; data: TableData;
showHeader: boolean; showHeader: boolean;
fixedColumnCount: number; fixedHeader: boolean;
fixedRowCount: number; fixedColumns: number;
rotate: boolean;
styles: ColumnStyle[]; styles: ColumnStyle[];
replaceVariables: InterpolateFunction; replaceVariables: InterpolateFunction;
width: number; width: number;
height: number; height: number;
...@@ -41,15 +44,26 @@ interface State { ...@@ -41,15 +44,26 @@ interface State {
data: TableData; data: TableData;
} }
interface ColumnRenderInfo {
header: string;
builder: TableCellBuilder;
}
interface DataIndex {
column: number;
row: number; // -1 is the header!
}
export class Table extends Component<Props, State> { export class Table extends Component<Props, State> {
columns: ColumnInfo[]; renderer: ColumnRenderInfo[];
measurer: CellMeasurerCache; measurer: CellMeasurerCache;
scrollToTop = false; scrollToTop = false;
static defaultProps = { static defaultProps = {
showHeader: true, showHeader: true,
fixedRowCount: 1, fixedHeader: true,
fixedColumnCount: 0, fixedColumns: 0,
rotate: false,
}; };
constructor(props: Props) { constructor(props: Props) {
...@@ -59,7 +73,7 @@ export class Table extends Component<Props, State> { ...@@ -59,7 +73,7 @@ export class Table extends Component<Props, State> {
data: props.data, data: props.data,
}; };
this.columns = this.initColumns(props); this.renderer = this.initColumns(props);
this.measurer = new CellMeasurerCache({ this.measurer = new CellMeasurerCache({
defaultHeight: 30, defaultHeight: 30,
defaultWidth: 150, defaultWidth: 150,
...@@ -70,9 +84,11 @@ export class Table extends Component<Props, State> { ...@@ -70,9 +84,11 @@ export class Table extends Component<Props, State> {
const { data, styles, showHeader } = this.props; const { data, styles, showHeader } = this.props;
const { sortBy, sortDirection } = this.state; const { sortBy, sortDirection } = this.state;
const dataChanged = data !== prevProps.data; const dataChanged = data !== prevProps.data;
const configsChanged = showHeader !== prevProps.showHeader; const configsChanged =
showHeader !== prevProps.showHeader ||
console.log('TABLE', this.props.theme); this.props.rotate !== prevProps.rotate ||
this.props.fixedColumns !== prevProps.fixedColumns ||
this.props.fixedHeader !== prevProps.fixedHeader;
// Reset the size cache // Reset the size cache
if (dataChanged || configsChanged) { if (dataChanged || configsChanged) {
...@@ -80,8 +96,9 @@ export class Table extends Component<Props, State> { ...@@ -80,8 +96,9 @@ export class Table extends Component<Props, State> {
} }
// Update the renderer if options change // Update the renderer if options change
// We only *need* do to this if the header values changes, but this does every data update
if (dataChanged || styles !== prevProps.styles) { if (dataChanged || styles !== prevProps.styles) {
this.columns = this.initColumns(this.props); this.renderer = this.initColumns(this.props);
} }
// Update the data when data or sort changes // Update the data when data or sort changes
...@@ -91,9 +108,9 @@ export class Table extends Component<Props, State> { ...@@ -91,9 +108,9 @@ export class Table extends Component<Props, State> {
} }
} }
initColumns(props: Props): ColumnInfo[] { /** Given the configuration, setup how each column gets rendered */
initColumns(props: Props): ColumnRenderInfo[] {
const { styles, data } = props; const { styles, data } = props;
console.log('STYLES', styles);
return data.columns.map((col, index) => { return data.columns.map((col, index) => {
let title = col.text; let title = col.text;
...@@ -113,7 +130,6 @@ export class Table extends Component<Props, State> { ...@@ -113,7 +130,6 @@ export class Table extends Component<Props, State> {
} }
return { return {
index,
header: title, header: title,
builder: getCellBuilder(col, style, this.props), builder: getCellBuilder(col, style, this.props),
}; };
...@@ -137,27 +153,37 @@ export class Table extends Component<Props, State> { ...@@ -137,27 +153,37 @@ export class Table extends Component<Props, State> {
this.setState({ sortBy: sort, sortDirection: dir }); this.setState({ sortBy: sort, sortDirection: dir });
}; };
/** Converts the grid coordinates to TableData coordinates */
getCellRef = (rowIndex: number, columnIndex: number): DataIndex => {
const { showHeader, rotate } = this.props;
const rowOffset = showHeader ? -1 : 0;
if (rotate) {
return { column: rowIndex, row: columnIndex + rowOffset };
} else {
return { column: columnIndex, row: rowIndex + rowOffset };
}
};
handleCellClick = (rowIndex: number, columnIndex: number) => { handleCellClick = (rowIndex: number, columnIndex: number) => {
const { showHeader } = this.props; const { row, column } = this.getCellRef(rowIndex, columnIndex);
const { data } = this.state; if (row < 0) {
const realRowIndex = rowIndex - (showHeader ? 1 : 0); this.doSort(column);
if (realRowIndex < 0) {
this.doSort(columnIndex);
} else { } else {
const row = data.rows[realRowIndex]; const values = this.state.data.rows[row];
const value = row[columnIndex]; const value = values[column];
console.log('CLICK', rowIndex, columnIndex, value); console.log('CLICK', value, row);
} }
}; };
headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => { headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
const { data, sortBy, sortDirection } = this.state; const { data, sortBy, sortDirection } = this.state;
const { columnIndex, rowIndex, style } = cell.props; const { columnIndex, rowIndex, style } = cell.props;
const { column } = this.getCellRef(rowIndex, columnIndex);
let col = data.columns[columnIndex]; let col = data.columns[column];
const sorting = sortBy === columnIndex; const sorting = sortBy === column;
if (!col) { if (!col) {
// NOT SURE Why this happens sometimes
col = { col = {
text: '??' + columnIndex + '???', text: '??' + columnIndex + '???',
}; };
...@@ -171,47 +197,60 @@ export class Table extends Component<Props, State> { ...@@ -171,47 +197,60 @@ export class Table extends Component<Props, State> {
); );
}; };
getTableCellBuilder = (column: number): TableCellBuilder => {
const render = this.renderer[column];
if (render && render.builder) {
return render.builder;
}
return simpleCellBuilder; // the default
};
cellRenderer = (props: GridCellProps): React.ReactNode => { cellRenderer = (props: GridCellProps): React.ReactNode => {
const { rowIndex, columnIndex, key, parent } = props; const { rowIndex, columnIndex, key, parent } = props;
const { showHeader } = this.props; const { row, column } = this.getCellRef(rowIndex, columnIndex);
const { data } = this.state; const { data } = this.state;
const column = this.columns[columnIndex]; const isHeader = row < 0;
if (!column) { const rowData = isHeader ? data.columns : data.rows[row];
// NOT SURE HOW/WHY THIS HAPPENS! const value = rowData ? rowData[column] : `[${columnIndex}:${rowIndex}]`;
// Without it it will crash in storybook when you cycle up/down the # of columns const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
// this cell is never visible in the output?
return (
<div key={key} style={props.style}>
XXXXX
</div>
);
}
const realRowIndex = rowIndex - (showHeader ? 1 : 0);
const isHeader = realRowIndex < 0;
const row = isHeader ? data.columns : data.rows[realRowIndex];
const value = row[columnIndex];
const builder = isHeader ? this.headerBuilder : column.builder;
return ( return (
<CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}> <CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
{builder({ value, row, table: this, props })} {builder({
value,
row: rowData,
column: data.columns[column],
table: this,
props,
})}
</CellMeasurer> </CellMeasurer>
); );
}; };
render() { render() {
const { data, showHeader, width, height } = this.props; const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
const { data } = this.state;
const columnCount = data.columns.length; let columnCount = data.columns.length;
const rowCount = data.rows.length + (showHeader ? 1 : 0); let rowCount = data.rows.length + (showHeader ? 1 : 0);
const fixedColumnCount = Math.min(this.props.fixedColumnCount, columnCount); let fixedColumnCount = Math.min(fixedColumns, columnCount);
const fixedRowCount = Math.min(this.props.fixedRowCount, rowCount); let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
if (rotate) {
const temp = columnCount;
columnCount = rowCount;
rowCount = temp;
fixedRowCount = 0;
fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0);
}
// Usually called after a sort or the data changes // Called after sort or the data changes
const scrollToRow = this.scrollToTop ? 1 : -1; const scroll = this.scrollToTop ? 1 : -1;
const scrollToRow = rotate ? -1 : scroll;
const scrollToColumn = rotate ? scroll : -1;
if (this.scrollToTop) { if (this.scrollToTop) {
this.scrollToTop = false; this.scrollToTop = false;
} }
...@@ -226,9 +265,10 @@ export class Table extends Component<Props, State> { ...@@ -226,9 +265,10 @@ export class Table extends Component<Props, State> {
} }
scrollToRow={scrollToRow} scrollToRow={scrollToRow}
columnCount={columnCount} columnCount={columnCount}
scrollToColumn={scrollToColumn}
rowCount={rowCount} rowCount={rowCount}
overscanColumnCount={2} overscanColumnCount={8}
overscanRowCount={2} overscanRowCount={8}
columnWidth={this.measurer.columnWidth} columnWidth={this.measurer.columnWidth}
deferredMeasurementCache={this.measurer} deferredMeasurementCache={this.measurer}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
......
...@@ -11,6 +11,7 @@ import { InterpolateFunction } from '../../types/panel'; ...@@ -11,6 +11,7 @@ import { InterpolateFunction } from '../../types/panel';
export interface TableCellBuilderOptions { export interface TableCellBuilderOptions {
value: any; value: any;
column?: Column;
row?: any[]; row?: any[];
table?: Table; table?: Table;
className?: string; className?: string;
......
...@@ -163,5 +163,5 @@ export const migratedTestStyles: ColumnStyle[] = [ ...@@ -163,5 +163,5 @@ export const migratedTestStyles: ColumnStyle[] = [
export const simpleTable = { export const simpleTable = {
type: 'table', type: 'table',
columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }], columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }],
rows: [[10, 23, 35], [11, 22, 31], [12, 21, 34]], rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]],
} as TableData; };
...@@ -3,7 +3,7 @@ import _ from 'lodash'; ...@@ -3,7 +3,7 @@ import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Types // Types
import { PanelEditorProps, Switch } from '@grafana/ui'; import { PanelEditorProps, Switch, FormField } from '@grafana/ui';
import { Options } from './types'; import { Options } from './types';
export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> { export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
...@@ -11,14 +11,43 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> { ...@@ -11,14 +11,43 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader }); this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader });
}; };
onToggleFixedHeader = () => {
this.props.onOptionsChange({ ...this.props.options, fixedHeader: !this.props.options.fixedHeader });
};
onToggleRotate = () => {
this.props.onOptionsChange({ ...this.props.options, rotate: !this.props.options.rotate });
};
onFixedColumnsChange = ({ target }) => {
this.props.onOptionsChange({ ...this.props.options, fixedColumns: target.value });
};
render() { render() {
const { showHeader } = this.props.options; const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options;
return ( return (
<div> <div>
<div className="section gf-form-group"> <div className="section gf-form-group">
<h5 className="section-heading">Header</h5> <h5 className="section-heading">Header</h5>
<Switch label="Show" labelClass="width-5" checked={showHeader} onChange={this.onToggleShowHeader} /> <Switch label="Show" labelClass="width-6" checked={showHeader} onChange={this.onToggleShowHeader} />
<Switch label="Fixed" labelClass="width-6" checked={fixedHeader} onChange={this.onToggleFixedHeader} />
</div>
<div className="section gf-form-group">
<h5 className="section-heading">Display</h5>
<Switch label="Rotate" labelClass="width-8" checked={rotate} onChange={this.onToggleRotate} />
<FormField
label="Fixed Columns"
labelWidth={8}
inputWidth={4}
type="number"
step="1"
min="0"
max="100"
onChange={this.onFixedColumnsChange}
value={fixedColumns}
/>
</div> </div>
</div> </div>
); );
......
...@@ -2,11 +2,18 @@ import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder'; ...@@ -2,11 +2,18 @@ import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
export interface Options { export interface Options {
showHeader: boolean; showHeader: boolean;
fixedHeader: boolean;
fixedColumns: number;
rotate: boolean;
styles: ColumnStyle[]; styles: ColumnStyle[];
} }
export const defaults: Options = { export const defaults: Options = {
showHeader: true, showHeader: true,
fixedHeader: true,
fixedColumns: 0,
rotate: false,
styles: [ styles: [
{ {
type: 'date', type: 'date',
......
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