grafana/ui: New table component (#20991)

* first working example

* Support sorting, adding types while waiting for official ones

* using react-window for windowing

* styles via emotion

* sizing

* set an offset for the table

* change table export

* fixing table cell widths

* Explore: Use new table component in explore (#21031)

* Explore: Use new table component in explore

* enable oncellclick

* only let filterable columns be clickable, refactor renderrow

* remove explore table

* Keep using old merge tables logic

* prettier

* remove unused typings file

* fixing tests

* Fixed explore table issue

* NewTable: Updated styles

* Fixed unit test

* Updated TableModel

* Minor update to explore table height

* typing
This commit is contained in:
Peter Holmberg
2019-12-18 08:38:50 +01:00
committed by GitHub
parent 841cffbe9a
commit e9079c3faa
19 changed files with 909 additions and 222 deletions

View File

@@ -315,7 +315,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
/>
)}
{mode === ExploreMode.Metrics && (
<TableContainer exploreId={exploreId} onClickCell={this.onClickFilterLabel} />
<TableContainer width={width} exploreId={exploreId} onClickCell={this.onClickFilterLabel} />
)}
{mode === ExploreMode.Logs && (
<LogsContainer

View File

@@ -1,68 +0,0 @@
import _ from 'lodash';
import React, { PureComponent } from 'react';
import ReactTable, { RowInfo } from 'react-table';
import TableModel from 'app/core/table_model';
const EMPTY_TABLE = new TableModel();
// Identify columns that contain values
const VALUE_REGEX = /^[Vv]alue #\d+/;
interface TableProps {
data: TableModel;
loading: boolean;
onClickCell?: (columnKey: string, rowValue: string) => void;
}
function prepareRows(rows: any[], columnNames: string[]) {
return rows.map(cells => _.zipObject(columnNames, cells));
}
export default class Table extends PureComponent<TableProps> {
getCellProps = (state: any, rowInfo: RowInfo, column: any) => {
return {
onClick: (e: React.SyntheticEvent) => {
// Only handle click on link, not the cell
if (e.target) {
const link = e.target as HTMLElement;
if (link.className === 'link') {
const columnKey = column.Header().props.title;
const rowValue = rowInfo.row[columnKey];
this.props.onClickCell?.(columnKey, rowValue);
}
}
},
};
};
render() {
const { data, loading } = this.props;
const tableModel = data || EMPTY_TABLE;
const columnNames = tableModel.columns.map(({ text }) => text);
const columns = tableModel.columns.map(({ filterable, text }) => ({
Header: () => <span title={text}>{text}</span>,
accessor: text,
className: VALUE_REGEX.test(text) ? 'text-right' : '',
show: text !== 'Time',
Cell: (row: any) => (
<span className={filterable ? 'link' : ''} title={text + ': ' + row.value}>
{typeof row.value === 'string' ? row.value : JSON.stringify(row.value)}
</span>
),
}));
const noDataText = data ? 'The queries returned no data for a table.' : '';
return (
<ReactTable
columns={columns}
data={tableModel.rows}
getTdProps={this.getCellProps}
loading={loading}
minRows={0}
noDataText={noDataText}
resolveData={data => prepareRows(data, columnNames)}
showPagination={Boolean(data)}
/>
);
}
}

View File

@@ -1,21 +1,19 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { Collapse } from '@grafana/ui';
import { DataFrame } from '@grafana/data';
import { Table, Collapse } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { toggleTable } from './state/actions';
import Table from './Table';
import TableModel from 'app/core/table_model';
interface TableContainerProps {
exploreId: ExploreId;
loading: boolean;
width: number;
onClickCell: (key: string, value: string) => void;
showingTable: boolean;
tableResult?: TableModel;
tableResult?: DataFrame;
toggleTable: typeof toggleTable;
}
@@ -24,12 +22,27 @@ export class TableContainer extends PureComponent<TableContainerProps> {
this.props.toggleTable(this.props.exploreId, this.props.showingTable);
};
getTableHeight() {
const { tableResult } = this.props;
if (!tableResult || tableResult.length === 0) {
return 200;
}
// tries to estimate table height
return Math.max(Math.min(600, tableResult.length * 35) + 35);
}
render() {
const { loading, onClickCell, showingTable, tableResult } = this.props;
const { loading, onClickCell, showingTable, tableResult, width } = this.props;
const height = this.getTableHeight();
const paddingWidth = 16;
const tableWidth = width - paddingWidth;
return (
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
{tableResult && <Table data={tableResult} loading={loading} onClickCell={onClickCell} />}
{tableResult && <Table data={tableResult} width={tableWidth} height={height} onCellClick={onClickCell} />}
</Collapse>
);
}
@@ -40,7 +53,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const { loading: loadingInState, showingTable, tableResult } = item;
const loading = tableResult && tableResult.rows.length > 0 ? false : loadingInState;
const loading = tableResult && tableResult.length > 0 ? false : loadingInState;
return { loading, showingTable, tableResult };
}

View File

@@ -24,8 +24,7 @@ import { Reducer } from 'redux';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { updateLocation } from 'app/core/actions/location';
import { serializeStateToUrlParam } from 'app/core/utils/explore';
import TableModel from 'app/core/table_model';
import { DataSourceApi, DataQuery, LogsDedupStrategy, dateTime, LoadingState } from '@grafana/data';
import { DataSourceApi, DataQuery, LogsDedupStrategy, dateTime, LoadingState, toDataFrame } from '@grafana/data';
describe('Explore item reducer', () => {
describe('scanning', () => {
@@ -174,12 +173,23 @@ describe('Explore item reducer', () => {
describe('when toggleTableAction is dispatched', () => {
it('then it should set correct state', () => {
const table = toDataFrame({
name: 'logs',
fields: [
{
name: 'time',
type: 'number',
values: [1, 2],
},
],
});
reducerTester()
.givenReducer(itemReducer, { tableResult: {} })
.givenReducer(itemReducer, { tableResult: table })
.whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({ showingTable: true, tableResult: {} })
.thenStateShouldEqual({ showingTable: true, tableResult: table })
.whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({ showingTable: false, tableResult: new TableModel() });
.thenStateShouldEqual({ showingTable: false, tableResult: null });
});
});
});

View File

@@ -62,7 +62,6 @@ import {
import { reducerFactory, ActionOf } from 'app/core/redux';
import { updateLocation } from 'app/core/actions/location';
import { LocationUpdate } from '@grafana/runtime';
import TableModel from 'app/core/table_model';
import { ResultProcessor } from '../utils/ResultProcessor';
export const DEFAULT_RANGE = {
@@ -448,7 +447,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
return { ...state, showingTable };
}
return { ...state, showingTable, tableResult: new TableModel() };
return { ...state, showingTable, tableResult: null };
},
})
.addMapper({

View File

@@ -131,21 +131,23 @@ describe('ResultProcessor', () => {
it('then it should return correct table result', () => {
const { resultProcessor } = testContext();
const theResult = resultProcessor.getTableResult();
const resultDataFrame = toDataFrame(
new TableModel({
columns: [
{ text: 'value', type: 'number' },
{ text: 'time', type: 'time' },
{ text: 'message', type: 'string' },
],
rows: [
[4, 100, 'this is a message'],
[5, 200, 'second message'],
[6, 300, 'third'],
],
type: 'table',
})
);
expect(theResult).toEqual({
columnMap: {},
columns: [
{ text: 'value', type: 'number', filterable: undefined },
{ text: 'time', type: 'time', filterable: undefined },
{ text: 'message', type: 'string', filterable: undefined },
],
rows: [
[4, 100, 'this is a message'],
[5, 200, 'second message'],
[6, 300, 'third'],
],
type: 'table',
});
expect(theResult).toEqual(resultDataFrame);
});
});

View File

@@ -1,5 +1,4 @@
import { LogsModel, GraphSeriesXY, DataFrame, FieldType, TimeZone } from '@grafana/data';
import { LogsModel, GraphSeriesXY, DataFrame, FieldType, TimeZone, toDataFrame } from '@grafana/data';
import { ExploreItemState, ExploreMode } from 'app/types/explore';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { sortLogsResult, refreshIntervalToSortOrder } from 'app/core/utils/explore';
@@ -34,7 +33,7 @@ export class ResultProcessor {
);
}
getTableResult(): TableModel | null {
getTableResult(): DataFrame | null {
if (this.state.mode !== ExploreMode.Metrics) {
return null;
}
@@ -75,7 +74,8 @@ export class ResultProcessor {
});
});
return mergeTablesIntoModel(new TableModel(), ...tables);
const mergedTable = mergeTablesIntoModel(new TableModel(), ...tables);
return toDataFrame(mergedTable);
}
getLogsResult(): LogsModel | null {

View File

@@ -2,29 +2,27 @@
import React, { Component } from 'react';
// Types
import { ThemeContext } from '@grafana/ui';
import { Table } from '@grafana/ui';
import { PanelProps } from '@grafana/data';
import { Options } from './types';
import Table from '@grafana/ui/src/components/Table/Table';
interface Props extends PanelProps<Options> {}
// So that the table does not go all the way to the edge of the panel chrome
const paddingBottom = 35;
export class TablePanel extends Component<Props> {
constructor(props: Props) {
super(props);
}
render() {
const { data, options } = this.props;
const { data, height, width } = this.props;
if (data.series.length < 1) {
return <div>No Table Data...</div>;
}
return (
<ThemeContext.Consumer>
{theme => <Table {...this.props} {...options} theme={theme} data={data.series[0]} />}
</ThemeContext.Consumer>
);
return <Table height={height - paddingBottom} width={width} data={data.series[0]} />;
}
}

View File

@@ -13,10 +13,10 @@ import {
LogsDedupStrategy,
AbsoluteTimeRange,
GraphSeriesXY,
DataFrame,
} from '@grafana/data';
import { Emitter } from 'app/core/core';
import TableModel from 'app/core/table_model';
export enum ExploreMode {
Metrics = 'Metrics',
@@ -130,7 +130,7 @@ export interface ExploreItemState {
/**
* Table model that combines all query table results into a single table.
*/
tableResult?: TableModel;
tableResult?: DataFrame;
/**
* React keys for rendering of QueryRows