diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index a9a32eb29f4..03765730343 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -19,17 +19,26 @@ const replaceVariables = (value: string, scopedVars?: ScopedVars) => { return value; }; -export function makeDummyTable(columnCount: number, rowCount: number): TableData { +export function columnIndexToLeter(column: number) { 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 { columns: Array.from(new Array(columnCount), (x, i) => { return { - text: String.fromCharCode(A + i), + text: columnIndexToLeter(i), }; }), rows: Array.from(new Array(rowCount), (x, rowId) => { 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', columnMap: {}, @@ -37,45 +46,54 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData } 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 fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); - const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const fixedHeader = boolean('Fixed Header', true); + const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false }); + const rotate = boolean('Rotate', false); return withFullSizeStory(Table, { styles: [], data: simpleTable, replaceVariables, - fixedRowCount, - fixedColumnCount, showHeader, + fixedHeader, + fixedColumns, + rotate, theme: getTheme(GrafanaThemeType.Light), }); }) - .add('variable size', () => { - const columnCount = number('Column Count', 20, { min: 2, max: 50, step: 1, range: false }); + .add('Variable Size', () => { + 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 showHeader = boolean('Show Header', true); - const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); - const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const fixedHeader = boolean('Fixed Header', true); + const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const rotate = boolean('Rotate', false); return withFullSizeStory(Table, { styles: [], data: makeDummyTable(columnCount, rowCount), replaceVariables, - fixedRowCount, - fixedColumnCount, showHeader, + fixedHeader, + fixedColumns, + rotate, theme: getTheme(GrafanaThemeType.Light), }); }) - .add('Old tests configuration', () => { + .add('Test Config (migrated)', () => { return withFullSizeStory(Table, { styles: migratedTestStyles, data: migratedTestTable, replaceVariables, showHeader: true, + rotate: true, theme: getTheme(GrafanaThemeType.Light), }); }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 8f2e3d7058b..4f7dce29f0d 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -14,21 +14,24 @@ import { Themeable } from '../../types/theme'; import { sortTableData } from '../../utils/processTimeSeries'; 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'; -interface ColumnInfo { - index: number; - header: string; - builder: TableCellBuilder; -} - export interface Props extends Themeable { data: TableData; + showHeader: boolean; - fixedColumnCount: number; - fixedRowCount: number; + fixedHeader: boolean; + fixedColumns: number; + rotate: boolean; styles: ColumnStyle[]; + replaceVariables: InterpolateFunction; width: number; height: number; @@ -41,15 +44,26 @@ interface State { data: TableData; } +interface ColumnRenderInfo { + header: string; + builder: TableCellBuilder; +} + +interface DataIndex { + column: number; + row: number; // -1 is the header! +} + export class Table extends Component { - columns: ColumnInfo[]; + renderer: ColumnRenderInfo[]; measurer: CellMeasurerCache; scrollToTop = false; static defaultProps = { showHeader: true, - fixedRowCount: 1, - fixedColumnCount: 0, + fixedHeader: true, + fixedColumns: 0, + rotate: false, }; constructor(props: Props) { @@ -59,7 +73,7 @@ export class Table extends Component { data: props.data, }; - this.columns = this.initColumns(props); + this.renderer = this.initColumns(props); this.measurer = new CellMeasurerCache({ defaultHeight: 30, defaultWidth: 150, @@ -70,9 +84,11 @@ export class Table extends Component { const { data, styles, showHeader } = this.props; const { sortBy, sortDirection } = this.state; const dataChanged = data !== prevProps.data; - const configsChanged = showHeader !== prevProps.showHeader; - - console.log('TABLE', this.props.theme); + const configsChanged = + showHeader !== prevProps.showHeader || + this.props.rotate !== prevProps.rotate || + this.props.fixedColumns !== prevProps.fixedColumns || + this.props.fixedHeader !== prevProps.fixedHeader; // Reset the size cache if (dataChanged || configsChanged) { @@ -80,8 +96,9 @@ export class Table extends Component { } // 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) { - this.columns = this.initColumns(this.props); + this.renderer = this.initColumns(this.props); } // Update the data when data or sort changes @@ -91,9 +108,9 @@ export class Table extends Component { } } - initColumns(props: Props): ColumnInfo[] { + /** Given the configuration, setup how each column gets rendered */ + initColumns(props: Props): ColumnRenderInfo[] { const { styles, data } = props; - console.log('STYLES', styles); return data.columns.map((col, index) => { let title = col.text; @@ -113,7 +130,6 @@ export class Table extends Component { } return { - index, header: title, builder: getCellBuilder(col, style, this.props), }; @@ -137,27 +153,37 @@ export class Table extends Component { this.setState({ sortBy: sort, sortDirection: dir }); }; - handleCellClick = (rowIndex: number, columnIndex: number) => { - const { showHeader } = this.props; - const { data } = this.state; - const realRowIndex = rowIndex - (showHeader ? 1 : 0); - if (realRowIndex < 0) { - this.doSort(columnIndex); + /** 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 { - const row = data.rows[realRowIndex]; - const value = row[columnIndex]; - console.log('CLICK', rowIndex, columnIndex, value); + return { column: columnIndex, row: rowIndex + rowOffset }; + } + }; + + handleCellClick = (rowIndex: number, columnIndex: number) => { + const { row, column } = this.getCellRef(rowIndex, columnIndex); + if (row < 0) { + this.doSort(column); + } else { + const values = this.state.data.rows[row]; + const value = values[column]; + console.log('CLICK', value, row); } }; headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => { const { data, sortBy, sortDirection } = this.state; const { columnIndex, rowIndex, style } = cell.props; + const { column } = this.getCellRef(rowIndex, columnIndex); - let col = data.columns[columnIndex]; - const sorting = sortBy === columnIndex; + let col = data.columns[column]; + const sorting = sortBy === column; if (!col) { - // NOT SURE Why this happens sometimes col = { text: '??' + columnIndex + '???', }; @@ -171,47 +197,60 @@ export class Table extends Component { ); }; + 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 => { const { rowIndex, columnIndex, key, parent } = props; - const { showHeader } = this.props; + const { row, column } = this.getCellRef(rowIndex, columnIndex); const { data } = this.state; - const column = this.columns[columnIndex]; - if (!column) { - // NOT SURE HOW/WHY THIS HAPPENS! - // Without it it will crash in storybook when you cycle up/down the # of columns - // this cell is never visible in the output? - return ( -
- XXXXX -
- ); - } - - 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; + const isHeader = row < 0; + const rowData = isHeader ? data.columns : data.rows[row]; + const value = rowData ? rowData[column] : `[${columnIndex}:${rowIndex}]`; + const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column); return ( - {builder({ value, row, table: this, props })} + {builder({ + value, + row: rowData, + column: data.columns[column], + table: this, + props, + })} ); }; 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; - const rowCount = data.rows.length + (showHeader ? 1 : 0); + let columnCount = data.columns.length; + let rowCount = data.rows.length + (showHeader ? 1 : 0); - const fixedColumnCount = Math.min(this.props.fixedColumnCount, columnCount); - const fixedRowCount = Math.min(this.props.fixedRowCount, rowCount); + let fixedColumnCount = Math.min(fixedColumns, columnCount); + let fixedRowCount = showHeader && fixedHeader ? 1 : 0; - // Usually called after a sort or the data changes - const scrollToRow = this.scrollToTop ? 1 : -1; + if (rotate) { + const temp = columnCount; + columnCount = rowCount; + rowCount = temp; + + fixedRowCount = 0; + fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0); + } + + // Called after sort or the data changes + const scroll = this.scrollToTop ? 1 : -1; + const scrollToRow = rotate ? -1 : scroll; + const scrollToColumn = rotate ? scroll : -1; if (this.scrollToTop) { this.scrollToTop = false; } @@ -226,9 +265,10 @@ export class Table extends Component { } scrollToRow={scrollToRow} columnCount={columnCount} + scrollToColumn={scrollToColumn} rowCount={rowCount} - overscanColumnCount={2} - overscanRowCount={2} + overscanColumnCount={8} + overscanRowCount={8} columnWidth={this.measurer.columnWidth} deferredMeasurementCache={this.measurer} cellRenderer={this.cellRenderer} diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx index 9e0a111aba1..b8cb91053d8 100644 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -11,6 +11,7 @@ import { InterpolateFunction } from '../../types/panel'; export interface TableCellBuilderOptions { value: any; + column?: Column; row?: any[]; table?: Table; className?: string; diff --git a/packages/grafana-ui/src/components/Table/examples.ts b/packages/grafana-ui/src/components/Table/examples.ts index 9f05488d839..2d08e5cdf0c 100644 --- a/packages/grafana-ui/src/components/Table/examples.ts +++ b/packages/grafana-ui/src/components/Table/examples.ts @@ -163,5 +163,5 @@ export const migratedTestStyles: ColumnStyle[] = [ export const simpleTable = { type: 'table', columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }], - rows: [[10, 23, 35], [11, 22, 31], [12, 21, 34]], -} as TableData; + rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]], +}; diff --git a/public/app/plugins/panel/table2/TablePanelEditor.tsx b/public/app/plugins/panel/table2/TablePanelEditor.tsx index 60d2eff9b85..ca64cc5e9d0 100644 --- a/public/app/plugins/panel/table2/TablePanelEditor.tsx +++ b/public/app/plugins/panel/table2/TablePanelEditor.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelEditorProps, Switch } from '@grafana/ui'; +import { PanelEditorProps, Switch, FormField } from '@grafana/ui'; import { Options } from './types'; export class TablePanelEditor extends PureComponent> { @@ -11,14 +11,43 @@ export class TablePanelEditor extends PureComponent> { 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() { - const { showHeader } = this.props.options; + const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options; return (
Header
- + + +
+ +
+
Display
+ +
); diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 2d588debe58..d58c58810ef 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -2,11 +2,18 @@ import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder'; export interface Options { showHeader: boolean; + fixedHeader: boolean; + fixedColumns: number; + rotate: boolean; + styles: ColumnStyle[]; } export const defaults: Options = { showHeader: true, + fixedHeader: true, + fixedColumns: 0, + rotate: false, styles: [ { type: 'date',