Commit 3347b45a by Torkel Ödegaard Committed by GitHub

Table: Component progress & custom FieldConfig options (#21231)

* Table: Set & use field display processor

* Use applyFieldOverrides outside in story instead

* Change types a bit

* Table: Move to flexible layout

* Simplest possible custom field option

* Skip default column

* Added textAlign

* Explore: Set display processor for table data frame

* Fixed storybook

* Refactoring

* Progress on cell display mode

* Major progress

* Progress & refactoring

* Fixes

* Updated tests

* Added more tests

* Table: Progress on cell style customization

* Restored testdata random walk table scenario

* put back unrelated change

* remove unused things

* Updated table story

* Renamed property

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
parent 8d537b7a
......@@ -148,22 +148,6 @@ export function getColorFromThreshold(value: number, thresholds: Threshold[], th
return getColorFromHexRgbOrName(thresholds[0].color, themeType);
}
// function getSignificantDigitCount(n: number): number {
// // remove decimal and make positive
// n = Math.abs(parseInt(String(n).replace('.', ''), 10));
// if (n === 0) {
// return 0;
// }
//
// // kill the 0s at the end of n
// while (n !== 0 && n % 10 === 0) {
// n /= 10;
// }
//
// // get number of digits
// return Math.floor(Math.log(n) / Math.LN10) + 1;
// }
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
if (_.isNumber(decimalOverride)) {
// It's important that scaledDecimals is null here
......
import set from 'lodash/set';
import { DynamicConfigValue, FieldConfig, InterpolateFunction, DataFrame, Field, FieldType } from '../types';
import {
GrafanaTheme,
DynamicConfigValue,
FieldConfig,
InterpolateFunction,
DataFrame,
Field,
FieldType,
FieldConfigSource,
} from '../types';
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
import { FieldMatcher } from '../types/transformations';
import isNumber from 'lodash/isNumber';
import toNumber from 'lodash/toNumber';
import { getDisplayProcessor } from './displayProcessor';
import { GetFieldDisplayValuesOptions } from './fieldDisplay';
interface OverrideProps {
match: FieldMatcher;
......@@ -17,6 +25,14 @@ interface GlobalMinMax {
max: number;
}
export interface ApplyFieldOverrideOptions {
data?: DataFrame[];
fieldOptions: FieldConfigSource;
replaceVariables: InterpolateFunction;
theme: GrafanaTheme;
autoMinMax?: boolean;
}
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
let min = Number.MAX_VALUE;
let max = Number.MIN_VALUE;
......@@ -42,14 +58,16 @@ export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
/**
* Return a copy of the DataFrame with all rules applied
*/
export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): DataFrame[] {
export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFrame[] {
if (!options.data) {
return [];
}
const source = options.fieldOptions;
if (!source) {
return options.data;
}
let range: GlobalMinMax | undefined = undefined;
// Prepare the Matchers
......@@ -59,7 +77,7 @@ export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): Data
const info = fieldMatchers.get(rule.matcher.id);
if (info) {
override.push({
match: info.get(rule.matcher),
match: info.get(rule.matcher.options),
properties: rule.properties,
});
}
......@@ -72,7 +90,7 @@ export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): Data
name = `Series[${index}]`;
}
const fields = frame.fields.map(field => {
const fields: Field[] = frame.fields.map(field => {
// Config is mutable within this scope
const config: FieldConfig = { ...field.config } || {};
if (field.type === FieldType.number) {
......@@ -116,7 +134,7 @@ export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): Data
config,
// Set the display processor
processor: getDisplayProcessor({
display: getDisplayProcessor({
type: field.type,
config: config,
theme: options.theme,
......
......@@ -44,13 +44,23 @@ export interface FieldConfig {
// Alternative to empty string
noValue?: string;
// Visual options
color?: string;
custom?: Record<string, any>;
}
export interface Field<T = any, V = Vector<T>> {
name: string; // The column name
/**
* Name of the field (column)
*/
name: string;
/**
* Field value type (string, number, etc)
*/
type: FieldType;
/**
* Meta info about how field and how to display it
*/
config: FieldConfig;
values: V; // The raw field values
labels?: Labels;
......
......@@ -50,7 +50,7 @@
"react-highlight-words": "0.11.0",
"react-popper": "1.3.3",
"react-storybook-addon-props-combinations": "1.1.0",
"react-table": "7.0.0-rc.4",
"react-table": "7.0.0-rc.15",
"react-transition-group": "2.6.1",
"react-virtualized": "9.21.0",
"slate": "0.47.8",
......
import { storiesOf } from '@storybook/react';
import { number, text } from '@storybook/addon-knobs';
import { BarGauge, Props } from './BarGauge';
import { BarGauge, Props, BarGaugeDisplayMode } from './BarGauge';
import { VizOrientation } from '@grafana/data';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
......@@ -18,7 +18,7 @@ const getKnobs = () => {
};
};
const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
const BarGaugeStories = storiesOf('Visualizations/BarGauge', module);
BarGaugeStories.addDecorator(withCenteredStory);
......@@ -47,7 +47,7 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
minValue: minValue,
maxValue: maxValue,
orientation: VizOrientation.Vertical,
displayMode: 'basic',
displayMode: BarGaugeDisplayMode.Basic,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: threshold1Value, color: threshold1Color },
......@@ -61,21 +61,21 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
}
addBarGaugeStory('Gradient Vertical', {
displayMode: 'gradient',
displayMode: BarGaugeDisplayMode.Gradient,
orientation: VizOrientation.Vertical,
height: 500,
width: 100,
});
addBarGaugeStory('Gradient Horizontal', {
displayMode: 'gradient',
displayMode: BarGaugeDisplayMode.Gradient,
orientation: VizOrientation.Horizontal,
height: 100,
width: 500,
});
addBarGaugeStory('LCD Horizontal', {
displayMode: 'lcd',
displayMode: BarGaugeDisplayMode.Lcd,
orientation: VizOrientation.Vertical,
height: 500,
width: 100,
......
......@@ -9,6 +9,7 @@ import {
getBarGradient,
getTitleStyles,
getValuePercent,
BarGaugeDisplayMode,
} from './BarGauge';
import { VizOrientation } from '@grafana/data';
import { getTheme } from '../../themes';
......@@ -20,7 +21,7 @@ function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
maxValue: 100,
minValue: 0,
displayMode: 'basic',
displayMode: BarGaugeDisplayMode.Basic,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: 70, color: 'orange' },
......
......@@ -39,22 +39,30 @@ export interface Props extends Themeable {
minValue: number;
orientation: VizOrientation;
itemSpacing?: number;
displayMode: 'basic' | 'lcd' | 'gradient';
lcdCellWidth?: number;
displayMode: BarGaugeDisplayMode;
onClick?: React.MouseEventHandler<HTMLElement>;
className?: string;
showUnfilled?: boolean;
alignmentFactors?: DisplayValueAlignmentFactors;
}
export enum BarGaugeDisplayMode {
Basic = 'basic',
Lcd = 'lcd',
Gradient = 'gradient',
}
export class BarGauge extends PureComponent<Props> {
static defaultProps: Partial<Props> = {
maxValue: 100,
minValue: 0,
lcdCellWidth: 12,
value: {
text: '100',
numeric: 100,
},
displayMode: 'lcd',
displayMode: BarGaugeDisplayMode.Gradient,
orientation: VizOrientation.Horizontal,
thresholds: [],
itemSpacing: 10,
......@@ -152,7 +160,7 @@ export class BarGauge extends PureComponent<Props> {
}
renderRetroBars(): ReactNode {
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation } = this.props;
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
const {
valueHeight,
valueWidth,
......@@ -166,8 +174,7 @@ export class BarGauge extends PureComponent<Props> {
const valueRange = maxValue - minValue;
const maxSize = isVert ? maxBarHeight : maxBarWidth;
const cellSpacing = itemSpacing!;
const cellWidth = 12;
const cellCount = Math.floor(maxSize / cellWidth);
const cellCount = Math.floor(maxSize / lcdCellWidth!);
const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount);
const valueColor = getValueColor(this.props);
......
......@@ -135,7 +135,7 @@ const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
groupLabel: css`
color: ${groupLabelColor};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.lg};
line-height: ${theme.typography.lineHeight.md};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
`,
icon: css`
......
......@@ -21,7 +21,7 @@ export const sharedInputStyle = (theme: GrafanaTheme, invalid = false) => {
return css`
background-color: ${colors.formInputBg};
line-height: ${theme.typography.lineHeight.lg};
line-height: ${theme.typography.lineHeight.md};
font-size: ${theme.typography.size.md};
color: ${colors.formInputText};
border: 1px solid ${borderColor};
......
import React, { FC } from 'react';
import { ReactTableCellProps, TableCellDisplayMode } from './types';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { VizOrientation } from '@grafana/data';
const defaultThresholds = [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
];
export const BarGaugeCell: FC<ReactTableCellProps> = props => {
const { column, tableStyles, cell } = props;
const { field } = column;
if (!field.display) {
return null;
}
const displayValue = field.display(cell.value);
let barGaugeMode = BarGaugeDisplayMode.Gradient;
if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.LcdGauge) {
barGaugeMode = BarGaugeDisplayMode.Lcd;
}
return (
<div className={tableStyles.tableCell}>
<BarGauge
width={column.width - tableStyles.cellPadding * 2}
height={tableStyles.cellHeightInner}
thresholds={field.config.thresholds || defaultThresholds}
value={displayValue}
maxValue={field.config.max || 100}
minValue={field.config.min || 0}
orientation={VizOrientation.Horizontal}
theme={tableStyles.theme}
itemSpacing={1}
lcdCellWidth={8}
displayMode={barGaugeMode}
/>
</div>
);
};
import React, { FC, CSSProperties } from 'react';
import { ReactTableCellProps } from './types';
import { formattedValueToString } from '@grafana/data';
import tinycolor from 'tinycolor2';
export const DefaultCell: FC<ReactTableCellProps> = props => {
const { column, cell, tableStyles } = props;
if (!column.field.display) {
return null;
}
const displayValue = column.field.display(cell.value);
return <div className={tableStyles.tableCell}>{formattedValueToString(displayValue)}</div>;
};
export const BackgroundColoredCell: FC<ReactTableCellProps> = props => {
const { column, cell, tableStyles } = props;
if (!column.field.display) {
return null;
}
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
const displayValue = column.field.display(cell.value);
const bgColor2 = tinycolor(displayValue.color)
.darken(10 * themeFactor)
.spin(5)
.toRgbString();
const styles: CSSProperties = {
background: `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`,
borderRadius: '0px',
color: 'white',
height: tableStyles.cellHeight,
padding: tableStyles.cellPadding,
};
return <div style={styles}>{formattedValueToString(displayValue)}</div>;
};
import React, { useMemo } from 'react';
import { DataFrame, GrafanaTheme } from '@grafana/data';
// @ts-ignore
import { useBlockLayout, useSortBy, useTable } from 'react-table';
import { FixedSizeList } from 'react-window';
import { css } from 'emotion';
import { stylesFactory, useTheme, selectThemeVariant as stv } from '../../themes';
export interface Props {
data: DataFrame;
width: number;
height: number;
onCellClick?: (key: string, value: string) => void;
}
const getTableData = (data: DataFrame) => {
const tableData = [];
for (let i = 0; i < data.length; i++) {
const row: { [key: string]: string | number } = {};
for (let j = 0; j < data.fields.length; j++) {
const prop = data.fields[j].name;
row[prop] = data.fields[j].values.get(i);
}
tableData.push(row);
}
return tableData;
};
const getColumns = (data: DataFrame) => {
return data.fields.map(field => {
return {
Header: field.name,
accessor: field.name,
field: field,
};
});
};
const getTableStyles = stylesFactory((theme: GrafanaTheme, columnWidth: number) => {
const colors = theme.colors;
const headerBg = stv({ light: colors.gray6, dark: colors.dark7 }, theme.type);
const padding = 5;
return {
cellHeight: padding * 2 + 14 * 1.5 + 2,
tableHeader: css`
padding: ${padding}px 10px;
background: ${headerBg};
cursor: pointer;
white-space: nowrap;
color: ${colors.blue};
border-bottom: 2px solid ${colors.bodyBg};
`,
tableCell: css`
display: 'table-cell';
padding: ${padding}px 10px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: ${columnWidth}px;
border-right: 2px solid ${colors.bodyBg};
border-bottom: 2px solid ${colors.bodyBg};
`,
};
});
const renderCell = (cell: any, columnWidth: number, cellStyles: string, onCellClick?: any) => {
const filterable = cell.column.field.config.filterable;
const style = {
cursor: `${filterable && onCellClick ? 'pointer' : 'default'}`,
};
return (
<div
className={cellStyles}
{...cell.getCellProps()}
onClick={filterable ? () => onCellClick(cell.column.Header, cell.value) : undefined}
style={style}
>
{cell.render('Cell')}
</div>
);
};
export const NewTable = ({ data, height, onCellClick, width }: Props) => {
const theme = useTheme();
const columnWidth = Math.floor(width / data.fields.length);
const tableStyles = getTableStyles(theme, columnWidth);
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns: useMemo(() => getColumns(data), [data]),
data: useMemo(() => getTableData(data), [data]),
},
useSortBy,
useBlockLayout
);
const RenderRow = React.useCallback(
({ index, style }) => {
const row = rows[index];
prepareRow(row);
return (
<div {...row.getRowProps({ style })}>
{row.cells.map((cell: any) => renderCell(cell, columnWidth, tableStyles.tableCell, onCellClick))}
</div>
);
},
[prepareRow, rows]
);
return (
<div {...getTableProps()}>
<div>
{headerGroups.map((headerGroup: any) => (
<div {...headerGroup.getHeaderGroupProps()} style={{ display: 'table-row' }}>
{headerGroup.headers.map((column: any) => (
<div
className={tableStyles.tableHeader}
{...column.getHeaderProps(column.getSortByToggleProps())}
style={{ display: 'table-cell', width: `${columnWidth}px` }}
>
{column.render('Header')}
<span>{column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}</span>
</div>
))}
</div>
))}
</div>
<FixedSizeList height={height} itemCount={rows.length} itemSize={tableStyles.cellHeight} width={width}>
{RenderRow}
</FixedSizeList>
</div>
);
};
// import React from 'react';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { Table } from './Table';
import { getTheme } from '../../themes';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { number } from '@storybook/addon-knobs';
import { useTheme } from '../../themes';
import mdx from './Table.mdx';
import {
DataFrame,
MutableDataFrame,
FieldType,
GrafanaTheme,
applyFieldOverrides,
FieldMatcherID,
ConfigOverrideRule,
} from '@grafana/data';
import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
import { GrafanaThemeType } from '@grafana/data';
import { DataFrame, FieldType, ArrayVector, ScopedVars } from '@grafana/data';
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 default {
title: 'Visualizations/Table',
component: Table,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
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);
}
function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFrame {
const data = new MutableDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [] }, // The time field
{
name: 'Quantity',
type: FieldType.number,
values: [],
config: {
decimals: 0,
custom: {
align: 'center',
},
},
},
{ name: 'Status', type: FieldType.string, values: [] }, // The time field
{
name: 'Value',
type: FieldType.number,
values: [],
config: {
decimals: 2,
},
},
{
name: 'Progress',
type: FieldType.number,
values: [],
config: {
unit: 'percent',
custom: {
width: 50,
},
},
},
],
});
export function makeDummyTable(columnCount: number, rowCount: number): DataFrame {
return {
fields: Array.from(new Array(columnCount), (x, i) => {
const colId = columnIndexToLeter(i);
const values = new ArrayVector<string>();
for (let i = 0; i < rowCount; i++) {
values.buffer.push(colId + (i + 1));
for (let i = 0; i < 1000; i++) {
data.appendRow([
new Date().getTime(),
Math.random() * 2,
Math.random() > 0.7 ? 'Active' : 'Cancelled',
Math.random() * 100,
Math.random() * 100,
]);
}
return {
name: colId,
type: FieldType.string,
config: {},
values,
};
}),
length: rowCount,
};
return applyFieldOverrides({
data: [data],
fieldOptions: {
overrides,
defaults: {},
},
theme,
replaceVariables: (value: string) => value,
})[0];
}
storiesOf('UI/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?
export const Simple = () => {
const theme = useTheme();
const width = number('width', 700, {}, 'Props');
const data = buildData(theme, []);
return (
<div className="panel-container" style={{ width: 'auto' }}>
<Table data={data} height={500} width={width} />
</div>
);
};
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);
export const BarGaugeCell = () => {
const theme = useTheme();
const width = number('width', 700, {}, 'Props');
const data = buildData(theme, [
{
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
properties: [
{ path: 'custom.width', value: '200' },
{ path: 'custom.displayMode', value: 'gradient-gauge' },
{ path: 'min', value: '0' },
{ path: 'max', value: '100' },
],
},
]);
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 });
return (
<div className="panel-container" style={{ width: 'auto' }}>
<Table data={data} height={500} width={width} />
</div>
);
};
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);
const defaultThresholds = [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
];
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),
});
});
export const ColoredCells = () => {
const theme = useTheme();
const width = number('width', 750, {}, 'Props');
const data = buildData(theme, [
{
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
properties: [
{ path: 'custom.width', value: '80' },
{ path: 'custom.displayMode', value: 'color-background' },
{ path: 'min', value: '0' },
{ path: 'max', value: '100' },
{ path: 'thresholds', value: defaultThresholds },
],
},
]);
return (
<div className="panel-container" style={{ width: 'auto' }}>
<Table data={data} height={500} width={width} />
</div>
);
};
import React from 'react';
import { readCSV } from '@grafana/data';
import { Table, Props } from './Table';
import { getTheme } from '../../themes/index';
import { GrafanaThemeType } from '@grafana/data';
import renderer from 'react-test-renderer';
const series = readCSV('a,b,c\n1,2,3\n4,5,6')[0];
const setup = (propOverrides?: object) => {
const props: Props = {
data: series,
minColumnWidth: 100,
showHeader: true,
fixedHeader: true,
fixedColumns: 0,
rotate: false,
styles: [],
replaceVariables: (value: string) => value,
width: 600,
height: 800,
theme: getTheme(GrafanaThemeType.Dark),
}; // partial
Object.assign(props, propOverrides);
const tree = renderer.create(<Table {...props} />);
const instance = (tree.getInstance() as unknown) as Table;
return {
tree,
instance,
};
};
describe('Table', () => {
it('ignore invalid properties', () => {
const { tree, instance } = setup();
expect(tree.toJSON() + '').toEqual(
setup({
id: 3, // Don't pass invalid parameters to MultiGrid
}).tree.toJSON() + ''
);
expect(instance.measurer.has(0, 0)).toBeTruthy();
});
});
// Libraries
import _ from 'lodash';
import React, { ReactElement } from 'react';
import { GridCellProps } from 'react-virtualized';
import { Table, Props } from './Table';
import {
Field,
dateTime,
FieldConfig,
getValueFormat,
GrafanaTheme,
ValueFormatter,
getColorFromHexRgbOrName,
InterpolateFunction,
formattedValueToString,
} from '@grafana/data';
export interface TableCellBuilderOptions {
value: any;
column?: Field;
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: FieldConfig, 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 = dateTime(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 schema: FieldConfig,
private replaceVariables: InterpolateFunction,
private fmt?: ValueFormatter
) {}
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.schema.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 });
};
}
export function getFieldCellBuilder(field: Field, style: ColumnStyle | null, p: Props): TableCellBuilder {
if (!field.display) {
return getCellBuilder(field.config || {}, style, p);
}
return (cell: TableCellBuilderOptions) => {
const { props } = cell;
const disp = field.display!(cell.value);
let style = props.style;
if (disp.color) {
style = {
...props.style,
background: disp.color,
};
}
let clazz = 'gf-table-cell';
if (cell.className) {
clazz += ' ' + cell.className;
}
return (
<div style={style} className={clazz} title={disp.title}>
{formattedValueToString(disp)}
</div>
);
};
}
import { toDataFrame, getColorDefinitionByName } from '@grafana/data';
import { ColumnStyle } from './TableCellBuilder';
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
export const migratedTestTable = toDataFrame({
type: 'table',
columns: [
{ name: 'Time' },
{ name: 'Value' },
{ name: 'Colored' },
{ name: 'Undefined' },
{ name: 'String' },
{ name: 'United', unit: 'bps' },
{ name: 'Sanitized' },
{ name: 'Link' },
{ name: 'Array' },
{ name: 'Mapping' },
{ name: 'RangeMapping' },
{ name: 'MappingColored' },
{ name: 'RangeMappingColored' },
],
rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
});
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&param_1=$__cell_1&param_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',
name: 'on',
},
{
value: '0',
name: 'off',
},
{
value: 'HELLO WORLD',
name: 'HELLO GRAFANA',
},
{
value: 'value1, value2',
name: 'value3, value4',
},
],
},
{
pattern: 'RangeMapping',
type: 'string',
mappingType: 2,
rangeMaps: [
{
from: '1',
to: '3',
name: 'on',
},
{
from: '3',
to: '6',
name: 'off',
},
],
},
{
pattern: 'MappingColored',
type: 'string',
mappingType: 1,
valueMaps: [
{
value: '1',
name: 'on',
},
{
value: '0',
name: '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',
name: 'on',
},
{
from: '3',
to: '6',
name: 'off',
},
],
colorMode: 'value',
thresholds: [2, 5],
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
},
];
export const simpleTable = {
type: 'table',
fields: [{ name: 'First' }, { name: 'Second' }, { name: 'Third' }],
rows: [
[701, 205, 305],
[702, 206, 301],
[703, 207, 304],
],
};
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, selectThemeVariant as stv } from '../../themes';
export interface TableStyles {
cellHeight: number;
cellHeightInner: number;
cellPadding: number;
rowHeight: number;
table: string;
thead: string;
headerCell: string;
tableCell: string;
row: string;
theme: GrafanaTheme;
}
export const getTableStyles = stylesFactory(
(theme: GrafanaTheme): TableStyles => {
const colors = theme.colors;
const headerBg = stv({ light: colors.gray6, dark: colors.dark7 }, theme.type);
const padding = 6;
const lineHeight = theme.typography.lineHeight.md;
const bodyFontSize = 14;
const cellHeight = padding * 2 + bodyFontSize * lineHeight;
return {
theme,
cellHeight,
cellPadding: padding,
cellHeightInner: bodyFontSize * lineHeight,
rowHeight: cellHeight + 2,
table: css`
overflow: auto;
border-spacing: 0;
`,
thead: css`
overflow-y: auto;
overflow-x: hidden;
background: ${headerBg};
`,
headerCell: css`
padding: ${padding}px 10px;
cursor: pointer;
white-space: nowrap;
color: ${colors.blue};
`,
row: css`
border-bottom: 2px solid ${colors.bodyBg};
`,
tableCell: css`
padding: ${padding}px 10px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
`,
};
}
);
import { TextAlignProperty } from 'csstype';
import { ComponentType } from 'react';
import { Field } from '@grafana/data';
import { TableStyles } from './styles';
export interface TableFieldOptions {
width: number;
align: FieldTextAlignment;
displayMode: TableCellDisplayMode;
}
export enum TableCellDisplayMode {
Auto = 'auto',
ColorText = 'color-text',
ColorBackground = 'color-background',
GradientGauge = 'gradient-gauge',
LcdGauge = 'lcd-gauge',
}
export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center';
export interface TableColumn {
// React table props
Header: string;
accessor: string | Function;
Cell: ComponentType<ReactTableCellProps>;
// Grafana additions
field: Field;
width: number;
textAlign: TextAlignProperty;
}
export interface TableRow {
[x: string]: any;
}
export interface ReactTableCellProps {
cell: ReactTableCell;
column: TableColumn;
tableStyles: TableStyles;
}
export interface ReactTableCell {
value: any;
}
import { MutableDataFrame, GrafanaThemeType, FieldType } from '@grafana/data';
import { getColumns } from './utils';
import { getTheme } from '../../themes';
function getData() {
const data = new MutableDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [] },
{
name: 'Value',
type: FieldType.number,
values: [],
config: {
custom: {
width: 100,
},
},
},
{
name: 'Message',
type: FieldType.string,
values: [],
config: {
custom: {
align: 'center',
},
},
},
],
});
return data;
}
describe('Table utils', () => {
describe('getColumns', () => {
it('Should build columns from DataFrame', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
expect(columns[0].Header).toBe('Time');
expect(columns[1].Header).toBe('Value');
});
it('Should distribute width and use field config width', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
expect(columns[0].width).toBe(450);
expect(columns[1].width).toBe(100);
});
it('Should use textAlign from custom', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
expect(columns[2].textAlign).toBe('center');
});
it('Should set textAlign to right for number values', () => {
const theme = getTheme(GrafanaThemeType.Dark);
const columns = getColumns(getData(), 1000, theme);
expect(columns[1].textAlign).toBe('right');
});
});
});
import { TextAlignProperty } from 'csstype';
import { DataFrame, Field, GrafanaTheme, FieldType } from '@grafana/data';
import { TableColumn, TableRow, TableFieldOptions, TableCellDisplayMode } from './types';
import { BarGaugeCell } from './BarGaugeCell';
import { DefaultCell, BackgroundColoredCell } from './DefaultCell';
export function getTableRows(data: DataFrame): TableRow[] {
const tableData = [];
for (let i = 0; i < data.length; i++) {
const row: { [key: string]: string | number } = {};
for (let j = 0; j < data.fields.length; j++) {
const prop = data.fields[j].name;
row[prop] = data.fields[j].values.get(i);
}
tableData.push(row);
}
return tableData;
}
function getTextAlign(field: Field): TextAlignProperty {
if (field.config.custom) {
const custom = field.config.custom as TableFieldOptions;
switch (custom.align) {
case 'right':
return 'right';
case 'left':
return 'left';
case 'center':
return 'center';
}
}
if (field.type === FieldType.number) {
return 'right';
}
return 'left';
}
export function getColumns(data: DataFrame, availableWidth: number, theme: GrafanaTheme): TableColumn[] {
const cols: TableColumn[] = [];
let fieldCountWithoutWidth = data.fields.length;
for (const field of data.fields) {
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
if (fieldTableOptions.width) {
availableWidth -= fieldTableOptions.width;
fieldCountWithoutWidth -= 1;
}
let Cell = DefaultCell;
let textAlign = getTextAlign(field);
switch (fieldTableOptions.displayMode) {
case TableCellDisplayMode.ColorBackground:
Cell = BackgroundColoredCell;
break;
case TableCellDisplayMode.LcdGauge:
case TableCellDisplayMode.GradientGauge:
Cell = BarGaugeCell;
textAlign = 'center';
break;
}
cols.push({
field,
Cell,
textAlign,
Header: field.name,
accessor: field.name,
width: fieldTableOptions.width,
});
}
// divide up the rest of the space
const sharedWidth = availableWidth / fieldCountWithoutWidth;
for (const column of cols) {
if (!column.width) {
column.width = sharedWidth;
}
}
return cols;
}
......@@ -227,8 +227,8 @@ exports[`TimePicker renders buttons correctly 1`] = `
"h6": "14px",
},
"lineHeight": Object {
"lg": 1.5,
"md": 1.3333333333333333,
"lg": 2,
"md": 1.5,
"sm": 1.1,
"xs": 1,
},
......@@ -534,8 +534,8 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
"h6": "14px",
},
"lineHeight": Object {
"lg": 1.5,
"md": 1.3333333333333333,
"lg": 2,
"md": 1.5,
"sm": 1.1,
"xs": 1,
},
......
......@@ -53,7 +53,7 @@ const getStyles = (theme: GrafanaTheme) => ({
label: type-ahead-item-group-title;
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.lg};
line-height: ${theme.typography.lineHeight.md};
padding: ${theme.spacing.sm};
`,
});
......
......@@ -9,7 +9,7 @@
@import 'PanelOptionsGroup/PanelOptionsGroup';
@import 'RefreshPicker/RefreshPicker';
@import 'Select/Select';
@import 'Table/TableInputCSV';
@import 'TableInputCSV/TableInputCSV';
@import 'ThresholdsEditor/ThresholdsEditor';
@import 'TimePicker/TimeOfDayPicker';
@import 'Tooltip/Tooltip';
......
......@@ -46,8 +46,8 @@ export { QueryField } from './QueryField/QueryField';
// Renderless
export { SetInterval } from './SetInterval/SetInterval';
export { NewTable as Table } from './Table/NewTable';
export { TableInputCSV } from './Table/TableInputCSV';
export { Table } from './Table/Table';
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
// Visualizations
export {
......@@ -62,7 +62,7 @@ export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
export { GraphLegend } from './Graph/GraphLegend';
export { GraphWithLegend } from './Graph/GraphWithLegend';
export { BarGauge } from './BarGauge/BarGauge';
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
export { VizRepeater } from './VizRepeater/VizRepeater';
......
......@@ -106,7 +106,7 @@ $font-size-md: ${theme.typography.size.md} !default;
$font-size-sm: ${theme.typography.size.sm} !default;
$font-size-xs: ${theme.typography.size.xs} !default;
$line-height-base: ${theme.typography.lineHeight.lg} !default;
$line-height-base: ${theme.typography.lineHeight.md} !default;
$font-weight-regular: ${theme.typography.weight.regular} !default;
$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
......
......@@ -54,8 +54,8 @@ const theme: GrafanaThemeCommons = {
lineHeight: {
xs: 1,
sm: 1.1,
md: 4 / 3,
lg: 1.5,
md: 1.5,
lg: 2,
},
link: {
decoration: 'none',
......
......@@ -130,8 +130,15 @@ describe('ResultProcessor', () => {
describe('when calling getTableResult', () => {
it('then it should return correct table result', () => {
const { resultProcessor } = testContext();
const theResult = resultProcessor.getTableResult();
const resultDataFrame = toDataFrame(
let theResult = resultProcessor.getTableResult();
expect(theResult.fields[0].name).toEqual('value');
expect(theResult.fields[1].name).toEqual('time');
expect(theResult.fields[2].name).toEqual('message');
expect(theResult.fields[1].display).not.toBeNull();
expect(theResult.length).toBe(3);
// Same data though a DataFrame
theResult = toDataFrame(
new TableModel({
columns: [
{ text: 'value', type: 'number' },
......@@ -146,8 +153,11 @@ describe('ResultProcessor', () => {
type: 'table',
})
);
expect(theResult).toEqual(resultDataFrame);
expect(theResult.fields[0].name).toEqual('value');
expect(theResult.fields[1].name).toEqual('time');
expect(theResult.fields[2].name).toEqual('message');
expect(theResult.fields[1].display).not.toBeNull();
expect(theResult.length).toBe(3);
});
});
......
import { LogsModel, GraphSeriesXY, DataFrame, FieldType, TimeZone, toDataFrame } from '@grafana/data';
import {
LogsModel,
GraphSeriesXY,
DataFrame,
FieldType,
TimeZone,
toDataFrame,
getDisplayProcessor,
} 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';
import { dataFrameToLogsModel } from 'app/core/logs_model';
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
import { config } from 'app/core/config';
export class ResultProcessor {
constructor(
......@@ -75,7 +84,17 @@ export class ResultProcessor {
});
const mergedTable = mergeTablesIntoModel(new TableModel(), ...tables);
return toDataFrame(mergedTable);
const data = toDataFrame(mergedTable);
// set display processor
for (const field of data.fields) {
field.display = getDisplayProcessor({
config: field.config,
theme: config.theme,
});
}
return data;
}
getLogsResult(): LogsModel | null {
......
import { SingleStatBaseOptions } from '@grafana/ui';
import { SingleStatBaseOptions, BarGaugeDisplayMode } from '@grafana/ui';
import { standardGaugeFieldOptions } from '../gauge/types';
import { VizOrientation, SelectableValue } from '@grafana/data';
export interface BarGaugeOptions extends SingleStatBaseOptions {
displayMode: 'basic' | 'lcd' | 'gradient';
displayMode: BarGaugeDisplayMode;
showUnfilled: boolean;
}
export const displayModes: Array<SelectableValue<string>> = [
{ value: 'gradient', label: 'Gradient' },
{ value: 'lcd', label: 'Retro LCD' },
{ value: 'basic', label: 'Basic' },
{ value: BarGaugeDisplayMode.Gradient, label: 'Gradient' },
{ value: BarGaugeDisplayMode.Lcd, label: 'Retro LCD' },
{ value: BarGaugeDisplayMode.Basic, label: 'Basic' },
];
export const defaults: BarGaugeOptions = {
displayMode: 'lcd',
displayMode: BarGaugeDisplayMode.Lcd,
orientation: VizOrientation.Horizontal,
fieldOptions: standardGaugeFieldOptions,
showUnfilled: true,
......
......@@ -11,9 +11,8 @@ import {
stringToJsRegex,
unEscapeStringFromRegex,
} from '@grafana/data';
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { ColumnRender, TableRenderModel } from './types';
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
export class TableRenderer {
formatters: any[];
......
import TableModel from 'app/core/table_model';
import { Column } from '@grafana/data';
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
export interface TableTransform {
description: string;
......@@ -18,3 +17,26 @@ export interface TableRenderModel {
columns: ColumnRender[];
rows: any[][];
}
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;
}
......@@ -3,13 +3,13 @@ import React, { Component } from 'react';
// Types
import { Table } from '@grafana/ui';
import { PanelProps } from '@grafana/data';
import { PanelProps, applyFieldOverrides } from '@grafana/data';
import { Options } from './types';
import { config } from 'app/core/config';
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;
const paddingBottom = 16;
export class TablePanel extends Component<Props> {
constructor(props: Props) {
......@@ -17,12 +17,19 @@ export class TablePanel extends Component<Props> {
}
render() {
const { data, height, width } = this.props;
const { data, height, width, replaceVariables, options } = this.props;
if (data.series.length < 1) {
return <div>No Table Data...</div>;
}
return <Table height={height - paddingBottom} width={width} data={data.series[0]} />;
const dataProcessed = applyFieldOverrides({
data: data.series,
fieldOptions: options.fieldOptions,
theme: config.theme,
replaceVariables,
})[0];
return <Table height={height - paddingBottom} width={width} data={dataProcessed} />;
}
}
......@@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
// Types
import { PanelEditorProps } from '@grafana/data';
import { Switch, FormField } from '@grafana/ui';
import { Switch } from '@grafana/ui';
import { Options } from './types';
export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
......@@ -12,43 +12,14 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
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 }: any) => {
this.props.onOptionsChange({ ...this.props.options, fixedColumns: target.value });
};
render() {
const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options;
const { showHeader } = 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>
);
......
......@@ -4,4 +4,7 @@ import { TablePanelEditor } from './TablePanelEditor';
import { TablePanel } from './TablePanel';
import { Options, defaults } from './types';
export const plugin = new PanelPlugin<Options>(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor);
export const plugin = new PanelPlugin<Options>(TablePanel)
.setNoPadding()
.setDefaults(defaults)
.setEditor(TablePanelEditor);
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
import { FieldConfigSource } from '@grafana/data';
export interface Options {
fieldOptions: FieldConfigSource;
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',
fieldOptions: {
defaults: {},
overrides: [],
},
{
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)'],
pattern: '/.*/',
thresholds: [],
},
],
showHeader: true,
};
......@@ -18163,10 +18163,10 @@ react-syntax-highlighter@^8.0.1:
prismjs "^1.8.4"
refractor "^2.4.1"
react-table@7.0.0-rc.4:
version "7.0.0-rc.4"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.4.tgz#88bc61747821f3c3bbbfc7e1a4a088cbe94ed9ee"
integrity sha512-NOYmNmAIvQ9sSZd5xMNSthqiZ/o5h8h28MhFQFSxCu5u3v9J8PNh7x9wYMnk737MTjoKCZWIZT/dMFCPItXzEg==
react-table@latest:
version "7.0.0-rc.15"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a"
integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw==
react-test-renderer@16.9.0:
version "16.9.0"
......
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