mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user