mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #15842 from ryantxu/alpha-react-virtualized-table
Alpha react-virtualized table
This commit is contained in:
commit
aa6a9329c3
99
packages/grafana-ui/src/components/Table/Table.story.tsx
Normal file
99
packages/grafana-ui/src/components/Table/Table.story.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
// import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { Table } from './Table';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
|
||||
import { ScopedVars, TableData, GrafanaThemeType } from '../../types/index';
|
||||
import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
|
||||
import { number, boolean } from '@storybook/addon-knobs';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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: columnIndexToLeter(i),
|
||||
};
|
||||
}),
|
||||
rows: Array.from(new Array(rowCount), (x, rowId) => {
|
||||
const suffix = (rowId + 1).toString();
|
||||
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
|
||||
}),
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
};
|
||||
}
|
||||
|
||||
storiesOf('Alpha/Table', module)
|
||||
.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 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,
|
||||
showHeader,
|
||||
fixedHeader,
|
||||
fixedColumns,
|
||||
rotate,
|
||||
theme: getTheme(GrafanaThemeType.Light),
|
||||
});
|
||||
})
|
||||
.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 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,
|
||||
showHeader,
|
||||
fixedHeader,
|
||||
fixedColumns,
|
||||
rotate,
|
||||
theme: getTheme(GrafanaThemeType.Light),
|
||||
});
|
||||
})
|
||||
.add('Test Config (migrated)', () => {
|
||||
return withFullSizeStory(Table, {
|
||||
styles: migratedTestStyles,
|
||||
data: migratedTestTable,
|
||||
replaceVariables,
|
||||
showHeader: true,
|
||||
rotate: true,
|
||||
theme: getTheme(GrafanaThemeType.Light),
|
||||
});
|
||||
});
|
287
packages/grafana-ui/src/components/Table/Table.tsx
Normal file
287
packages/grafana-ui/src/components/Table/Table.tsx
Normal file
@ -0,0 +1,287 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import {
|
||||
SortDirectionType,
|
||||
SortIndicator,
|
||||
MultiGrid,
|
||||
CellMeasurerCache,
|
||||
CellMeasurer,
|
||||
GridCellProps,
|
||||
} from 'react-virtualized';
|
||||
import { Themeable } from '../../types/theme';
|
||||
|
||||
import { sortTableData } from '../../utils/processTableData';
|
||||
|
||||
import { TableData, InterpolateFunction } from '@grafana/ui';
|
||||
import {
|
||||
TableCellBuilder,
|
||||
ColumnStyle,
|
||||
getCellBuilder,
|
||||
TableCellBuilderOptions,
|
||||
simpleCellBuilder,
|
||||
} from './TableCellBuilder';
|
||||
import { stringToJsRegex } from '../../utils/index';
|
||||
|
||||
export interface Props extends Themeable {
|
||||
data: TableData;
|
||||
|
||||
showHeader: boolean;
|
||||
fixedHeader: boolean;
|
||||
fixedColumns: number;
|
||||
rotate: boolean;
|
||||
styles: ColumnStyle[];
|
||||
|
||||
replaceVariables: InterpolateFunction;
|
||||
width: number;
|
||||
height: number;
|
||||
isUTC?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
sortBy?: number;
|
||||
sortDirection?: SortDirectionType;
|
||||
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> {
|
||||
renderer: ColumnRenderInfo[];
|
||||
measurer: CellMeasurerCache;
|
||||
scrollToTop = false;
|
||||
|
||||
static defaultProps = {
|
||||
showHeader: true,
|
||||
fixedHeader: true,
|
||||
fixedColumns: 0,
|
||||
rotate: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: props.data,
|
||||
};
|
||||
|
||||
this.renderer = this.initColumns(props);
|
||||
this.measurer = new CellMeasurerCache({
|
||||
defaultHeight: 30,
|
||||
defaultWidth: 150,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { data, styles, showHeader } = this.props;
|
||||
const { sortBy, sortDirection } = this.state;
|
||||
const dataChanged = data !== prevProps.data;
|
||||
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) {
|
||||
this.measurer.clearAll();
|
||||
}
|
||||
|
||||
// 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.renderer = this.initColumns(this.props);
|
||||
}
|
||||
|
||||
// Update the data when data or sort changes
|
||||
if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
|
||||
this.scrollToTop = true;
|
||||
this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') });
|
||||
}
|
||||
}
|
||||
|
||||
/** Given the configuration, setup how each column gets rendered */
|
||||
initColumns(props: Props): ColumnRenderInfo[] {
|
||||
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 = stringToJsRegex(s.pattern);
|
||||
if (title.match(regex)) {
|
||||
style = s;
|
||||
if (s.alias) {
|
||||
title = title.replace(regex, s.alias);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
header: title,
|
||||
builder: getCellBuilder(col, style, this.props),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
doSort = (columnIndex: number) => {
|
||||
let sort: any = this.state.sortBy;
|
||||
let dir = this.state.sortDirection;
|
||||
if (sort !== columnIndex) {
|
||||
dir = 'DESC';
|
||||
sort = columnIndex;
|
||||
} else if (dir === 'DESC') {
|
||||
dir = 'ASC';
|
||||
} else {
|
||||
sort = null;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
};
|
||||
|
||||
onCellClick = (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[column];
|
||||
const sorting = sortBy === column;
|
||||
if (!col) {
|
||||
col = {
|
||||
text: '??' + columnIndex + '???',
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
|
||||
{col.text}
|
||||
{sorting && <SortIndicator sortDirection={sortDirection} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 { row, column } = this.getCellRef(rowIndex, columnIndex);
|
||||
const { data } = this.state;
|
||||
|
||||
const isHeader = row < 0;
|
||||
const rowData = isHeader ? data.columns : data.rows[row];
|
||||
const value = rowData ? rowData[column] : '';
|
||||
const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
|
||||
|
||||
return (
|
||||
<CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
|
||||
{builder({
|
||||
value,
|
||||
row: rowData,
|
||||
column: data.columns[column],
|
||||
table: this,
|
||||
props,
|
||||
})}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
|
||||
const { data } = this.state;
|
||||
|
||||
let columnCount = data.columns.length;
|
||||
let rowCount = data.rows.length + (showHeader ? 1 : 0);
|
||||
|
||||
let fixedColumnCount = Math.min(fixedColumns, columnCount);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiGrid
|
||||
{
|
||||
...this.state /** Force MultiGrid to update when data changes */
|
||||
}
|
||||
{
|
||||
...this.props /** Force MultiGrid to update when data changes */
|
||||
}
|
||||
scrollToRow={scrollToRow}
|
||||
columnCount={columnCount}
|
||||
scrollToColumn={scrollToColumn}
|
||||
rowCount={rowCount}
|
||||
overscanColumnCount={8}
|
||||
overscanRowCount={8}
|
||||
columnWidth={this.measurer.columnWidth}
|
||||
deferredMeasurementCache={this.measurer}
|
||||
cellRenderer={this.cellRenderer}
|
||||
rowHeight={this.measurer.rowHeight}
|
||||
width={width}
|
||||
height={height}
|
||||
fixedColumnCount={fixedColumnCount}
|
||||
fixedRowCount={fixedRowCount}
|
||||
classNameTopLeftGrid="gf-table-fixed-column"
|
||||
classNameBottomLeftGrid="gf-table-fixed-column"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Table;
|
291
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx
Normal file
291
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx
Normal file
@ -0,0 +1,291 @@
|
||||
// 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;
|
||||
column?: Column;
|
||||
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={'gf-table-cell ' + 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!
|
||||
//
|
||||
// ***************************************************************************
|
||||
|
||||
// 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
|
||||
) {
|
||||
//
|
||||
console.log('COLUMN', column.text, theme);
|
||||
}
|
||||
|
||||
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 });
|
||||
};
|
||||
}
|
80
packages/grafana-ui/src/components/Table/_Table.scss
Normal file
80
packages/grafana-ui/src/components/Table/_Table.scss
Normal file
@ -0,0 +1,80 @@
|
||||
// .ReactVirtualized__Table {
|
||||
// }
|
||||
|
||||
// .ReactVirtualized__Table__Grid {
|
||||
// }
|
||||
|
||||
.ReactVirtualized__Table__headerRow {
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: left;
|
||||
}
|
||||
.ReactVirtualized__Table__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
border-bottom: 2px solid $body-bg;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__headerTruncatedText {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__headerColumn,
|
||||
.ReactVirtualized__Table__rowColumn {
|
||||
margin-right: 10px;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__headerColumn:first-of-type,
|
||||
.ReactVirtualized__Table__rowColumn:first-of-type {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.ReactVirtualized__Table__sortableHeaderColumn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__sortableHeaderIconContainer {
|
||||
align-items: center;
|
||||
}
|
||||
.ReactVirtualized__Table__sortableHeaderIcon {
|
||||
flex: 0 0 24px;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.gf-table-header {
|
||||
padding: 3px 10px;
|
||||
|
||||
background: $list-item-bg;
|
||||
border-top: 2px solid $body-bg;
|
||||
border-bottom: 2px solid $body-bg;
|
||||
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.gf-table-cell {
|
||||
padding: 3px 10px;
|
||||
|
||||
background: $page-gradient;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
border-right: 2px solid $body-bg;
|
||||
border-bottom: 2px solid $body-bg;
|
||||
}
|
||||
|
||||
.gf-table-fixed-column {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
167
packages/grafana-ui/src/components/Table/examples.ts
Normal file
167
packages/grafana-ui/src/components/Table/examples.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { TableData } from '../../types/data';
|
||||
import { ColumnStyle } from './TableCellBuilder';
|
||||
|
||||
import { getColorDefinitionByName } from '@grafana/ui';
|
||||
|
||||
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
|
||||
|
||||
export const migratedTestTable = {
|
||||
type: 'table',
|
||||
columns: [
|
||||
{ text: 'Time' },
|
||||
{ text: 'Value' },
|
||||
{ text: 'Colored' },
|
||||
{ text: 'Undefined' },
|
||||
{ text: 'String' },
|
||||
{ text: 'United', unit: 'bps' },
|
||||
{ text: 'Sanitized' },
|
||||
{ text: 'Link' },
|
||||
{ text: 'Array' },
|
||||
{ text: 'Mapping' },
|
||||
{ text: 'RangeMapping' },
|
||||
{ text: 'MappingColored' },
|
||||
{ text: 'RangeMappingColored' },
|
||||
],
|
||||
rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
|
||||
} as TableData;
|
||||
|
||||
export const migratedTestStyles: ColumnStyle[] = [
|
||||
{
|
||||
pattern: 'Time',
|
||||
type: 'date',
|
||||
alias: 'Timestamp',
|
||||
},
|
||||
{
|
||||
pattern: '/(Val)ue/',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 3,
|
||||
alias: '$1',
|
||||
},
|
||||
{
|
||||
pattern: 'Colored',
|
||||
type: 'number',
|
||||
unit: 'none',
|
||||
decimals: 1,
|
||||
colorMode: 'value',
|
||||
thresholds: [50, 80],
|
||||
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
|
||||
},
|
||||
{
|
||||
pattern: 'String',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
pattern: 'String',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
pattern: 'United',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 2,
|
||||
},
|
||||
{
|
||||
pattern: 'Sanitized',
|
||||
type: 'string',
|
||||
sanitize: true,
|
||||
},
|
||||
{
|
||||
pattern: 'Link',
|
||||
type: 'string',
|
||||
link: true,
|
||||
linkUrl: '/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2',
|
||||
linkTooltip: '$__cell $__cell_1 $__cell_6',
|
||||
linkTargetBlank: true,
|
||||
},
|
||||
{
|
||||
pattern: 'Array',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 3,
|
||||
},
|
||||
{
|
||||
pattern: 'Mapping',
|
||||
type: 'string',
|
||||
mappingType: 1,
|
||||
valueMaps: [
|
||||
{
|
||||
value: '1',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
text: 'off',
|
||||
},
|
||||
{
|
||||
value: 'HELLO WORLD',
|
||||
text: 'HELLO GRAFANA',
|
||||
},
|
||||
{
|
||||
value: 'value1, value2',
|
||||
text: 'value3, value4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: 'RangeMapping',
|
||||
type: 'string',
|
||||
mappingType: 2,
|
||||
rangeMaps: [
|
||||
{
|
||||
from: '1',
|
||||
to: '3',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
from: '3',
|
||||
to: '6',
|
||||
text: 'off',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: 'MappingColored',
|
||||
type: 'string',
|
||||
mappingType: 1,
|
||||
valueMaps: [
|
||||
{
|
||||
value: '1',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
text: 'off',
|
||||
},
|
||||
],
|
||||
colorMode: 'value',
|
||||
thresholds: [1, 2],
|
||||
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
|
||||
},
|
||||
{
|
||||
pattern: 'RangeMappingColored',
|
||||
type: 'string',
|
||||
mappingType: 2,
|
||||
rangeMaps: [
|
||||
{
|
||||
from: '1',
|
||||
to: '3',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
from: '3',
|
||||
to: '6',
|
||||
text: 'off',
|
||||
},
|
||||
],
|
||||
colorMode: 'value',
|
||||
thresholds: [2, 5],
|
||||
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
|
||||
},
|
||||
];
|
||||
|
||||
export const simpleTable = {
|
||||
type: 'table',
|
||||
columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }],
|
||||
rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]],
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'Table/Table';
|
||||
@import 'Table/TableInputCSV';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'Select/Select';
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { TableData, Column } from '../types/index';
|
||||
|
||||
// Libraries
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
||||
|
||||
// Types
|
||||
import { TableData, Column } from '../types';
|
||||
|
||||
// Subset of all parse options
|
||||
export interface TableParseOptions {
|
||||
headerIsFirstLine?: boolean; // Not a papa-parse option
|
||||
@ -131,3 +134,24 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
|
||||
columnMap: {},
|
||||
});
|
||||
}
|
||||
|
||||
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
|
||||
if (isNumber(sortIndex)) {
|
||||
const copy = {
|
||||
...data,
|
||||
rows: [...data.rows].sort((a, b) => {
|
||||
a = a[sortIndex];
|
||||
b = b[sortIndex];
|
||||
// Sort null or undefined separately from comparable values
|
||||
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
|
||||
}),
|
||||
};
|
||||
|
||||
if (reverse) {
|
||||
copy.rows.reverse();
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
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={{
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<>
|
||||
{React.createElement(component, {
|
||||
...props,
|
||||
width,
|
||||
height,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
@ -23,6 +23,7 @@ import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
||||
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
||||
import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as table2Panel from 'app/plugins/panel/table2/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
@ -53,6 +54,7 @@ const builtInPlugins = {
|
||||
'app/plugins/panel/alertlist/module': alertListPanel,
|
||||
'app/plugins/panel/heatmap/module': heatmapPanel,
|
||||
'app/plugins/panel/table/module': tablePanel,
|
||||
'app/plugins/panel/table2/module': table2Panel,
|
||||
'app/plugins/panel/singlestat/module': singlestatPanel,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, stringToJsRegex } from '@grafana/ui';
|
||||
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
|
||||
|
||||
export class TableRenderer {
|
||||
formatters: any[];
|
||||
@ -50,7 +51,7 @@ export class TableRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
getColorForValue(value, style) {
|
||||
getColorForValue(value, style: ColumnStyle) {
|
||||
if (!style.thresholds) {
|
||||
return null;
|
||||
}
|
||||
@ -62,7 +63,7 @@ export class TableRenderer {
|
||||
return getColorFromHexRgbOrName(_.first(style.colors), this.theme);
|
||||
}
|
||||
|
||||
defaultCellFormatter(v, style) {
|
||||
defaultCellFormatter(v, style: ColumnStyle) {
|
||||
if (v === null || v === void 0 || v === undefined) {
|
||||
return '';
|
||||
}
|
||||
@ -189,7 +190,7 @@ export class TableRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
setColorState(value, style) {
|
||||
setColorState(value, style: ColumnStyle) {
|
||||
if (!style.colorMode) {
|
||||
return;
|
||||
}
|
||||
|
9
public/app/plugins/panel/table2/README.md
Normal file
9
public/app/plugins/panel/table2/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Table Panel - Native Plugin
|
||||
|
||||
The Table Panel is **included** with Grafana.
|
||||
|
||||
The table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
|
||||
|
||||
Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here:
|
||||
|
||||
[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/)
|
29
public/app/plugins/panel/table2/TablePanel.tsx
Normal file
29
public/app/plugins/panel/table2/TablePanel.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// Types
|
||||
import { PanelProps, ThemeContext } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
import Table from '@grafana/ui/src/components/Table/Table';
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
export class TablePanel extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panelData, options } = this.props;
|
||||
|
||||
if (!panelData || !panelData.tableData) {
|
||||
return <div>No Table Data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
55
public/app/plugins/panel/table2/TablePanelEditor.tsx
Normal file
55
public/app/plugins/panel/table2/TablePanelEditor.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
//// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Types
|
||||
import { PanelEditorProps, Switch, FormField } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
|
||||
onToggleShowHeader = () => {
|
||||
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, fixedHeader, rotate, fixedColumns } = this.props.options;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Header</h5>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
67
public/app/plugins/panel/table2/img/icn-table-panel.svg
Normal file
67
public/app/plugins/panel/table2/img/icn-table-panel.svg
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="15.8113" y1="25" x2="15.8113" y2="-2.5362">
|
||||
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||
</linearGradient>
|
||||
<rect x="0" style="fill:url(#SVGID_1_);" width="31.623" height="15.049"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="50" y1="25" x2="50" y2="-2.5362">
|
||||
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||
</linearGradient>
|
||||
<rect x="34.188" style="fill:url(#SVGID_2_);" width="31.623" height="15.049"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="84.1887" y1="25" x2="84.1887" y2="-2.5362">
|
||||
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||
</linearGradient>
|
||||
<rect x="68.377" style="fill:url(#SVGID_3_);" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
9
public/app/plugins/panel/table2/module.tsx
Normal file
9
public/app/plugins/panel/table2/module.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
import { TablePanelEditor } from './TablePanelEditor';
|
||||
import { TablePanel } from './TablePanel';
|
||||
import { Options, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<Options>(TablePanel);
|
||||
reactPanel.setEditor(TablePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
19
public/app/plugins/panel/table2/plugin.json
Normal file
19
public/app/plugins/panel/table2/plugin.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "React Table",
|
||||
"id": "table2",
|
||||
"state": "alpha",
|
||||
|
||||
"dataFormats": ["table"],
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-table-panel.svg",
|
||||
"large": "img/icn-table-panel.svg"
|
||||
}
|
||||
}
|
||||
}
|
35
public/app/plugins/panel/table2/types.ts
Normal file
35
public/app/plugins/panel/table2/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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',
|
||||
pattern: 'Time',
|
||||
alias: 'Time',
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
{
|
||||
unit: 'short',
|
||||
type: 'number',
|
||||
alias: '',
|
||||
decimals: 2,
|
||||
colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
|
||||
colorMode: null,
|
||||
pattern: '/.*/',
|
||||
thresholds: [],
|
||||
},
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue
Block a user