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>
This commit is contained in:
Torkel Ödegaard 2019-12-23 06:22:54 +01:00 committed by GitHub
parent 8d537b7afb
commit 3347b45a95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 750 additions and 1918 deletions

View File

@ -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

View File

@ -1,11 +1,19 @@
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,

View File

@ -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;

View File

@ -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",

View File

@ -1,6 +1,6 @@
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,

View File

@ -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' },

View File

@ -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);

View File

@ -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`

View File

@ -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};

View File

@ -0,0 +1,49 @@
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>
);
};

View File

@ -0,0 +1,41 @@
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>;
};

View File

@ -1,696 +0,0 @@
import React from 'react';
import { NewTable } from './NewTable';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './NewTable.mdx';
import { DataFrame, toDataFrame } from '@grafana/data';
export default {
title: 'UI/Table/NewTable',
component: NewTable,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const mockDataFrame: DataFrame = toDataFrame({
refId: 'A',
fields: [
{
name: 'Time',
config: {},
values: [
1575951385249,
1575951415249,
1575951445249,
1575951475249,
1575951505249,
1575951535249,
1575951565249,
1575951595249,
1575951625249,
1575951655249,
1575951685249,
1575951715249,
1575951745249,
1575951775249,
1575951805249,
1575951835249,
1575951865249,
1575951895249,
1575951925249,
1575951955249,
1575951985249,
1575952015249,
1575952045249,
1575952075249,
1575952105249,
1575952135249,
1575952165249,
1575952195249,
1575952225249,
1575952255249,
1575952285249,
1575952315249,
1575952345249,
1575952375249,
1575952405249,
1575952435249,
1575952465249,
1575952495249,
1575952525249,
1575952555249,
1575952585249,
1575952615249,
1575952645249,
1575952675249,
1575952705249,
1575952735249,
1575952765249,
1575952795249,
1575952825249,
1575952855249,
1575952885249,
1575952915249,
1575952945249,
1575952975249,
1575953005249,
1575953035249,
1575953065249,
1575953095249,
1575953125249,
1575953155249,
1575953185249,
1575953215249,
1575953245249,
1575953275249,
1575953305249,
1575953335249,
1575953365249,
1575953395249,
1575953425249,
1575953455249,
1575953485249,
1575953515249,
1575953545249,
1575953575249,
1575953605249,
1575953635249,
1575953665249,
1575953695249,
1575953725249,
1575953755249,
1575953785249,
1575953815249,
1575953845249,
1575953875249,
1575953905249,
1575953935249,
1575953965249,
1575953995249,
1575954025249,
1575954055249,
1575954085249,
1575954115249,
1575954145249,
1575954175249,
1575954205249,
1575954235249,
1575954265249,
1575954295249,
1575954325249,
1575954355249,
1575954385249,
1575954415249,
1575954445249,
1575954475249,
1575954505249,
1575954535249,
1575954565249,
1575954595249,
1575954625249,
1575954655249,
1575954685249,
1575954715249,
1575954745249,
1575954775249,
1575954805249,
1575954835249,
1575954865249,
1575954895249,
1575954925249,
1575954955249,
1575954985249,
1575955015249,
1575955045249,
1575955075249,
1575955105249,
1575955135249,
],
type: 'time',
calcs: null,
},
{
name: 'Value',
config: {},
values: [
1.5000254936150939,
1.0764011931793371,
0.909466911538386,
1.3833044968776655,
1.6330889934233457,
1.6709668856700475,
1.2645776897559702,
1.4943939986749317,
1.2437720606210307,
0.7883544129607633,
0.5531910797833525,
1.0457642543811316,
1.382667661114637,
1.1568426549305126,
1.1402599321207862,
0.8483451908731177,
1.0317957901350685,
0.920556237728358,
0.5119097189376975,
0.3251335563131686,
0.7108123250650906,
0.4363759782763406,
0.8734924745632022,
0.7937634746603249,
0.68612096153115,
0.18988522007983766,
0.6576558170343919,
0.25301964245089764,
0.5277493691957931,
0.46587339439817715,
0.36227094078701305,
0.778938585446131,
1.2377378919033881,
0.9610752640855127,
1.1282013840938148,
0.9283745710857219,
0.674699599869639,
0.8974372977635153,
0.4075745811903667,
0.26659108532003495,
0.38371243779290887,
0.7832793846413076,
0.6259127747122668,
0.37112598577068756,
0.0613275616200267,
0.3576963952766255,
0.1665079834081022,
0.38990450556730366,
0.2214623170358675,
0.20133177397788687,
0.3346036375598696,
-0.0192909674691417,
-0.3396463212802728,
-0.4335565435802561,
-0.7225467771514991,
-0.9774803860859353,
-0.5596691380280212,
-0.8782204226162458,
-1.0379530805725203,
-0.6536190918926427,
-0.6041277845604902,
-0.5788054694749062,
-0.2328568877382495,
0.20668025552369196,
0.28117302534150723,
0.05146978829879428,
0.10096018560031056,
0.4377253335922943,
0.15035296580222518,
0.4462013793721102,
0.24812034480027956,
0.2889496029069344,
0.6625835159165457,
0.8233581741905972,
0.8643260393590533,
0.540435640675776,
0.22824187203884172,
0.10870831703051109,
0.554460912136038,
0.6954620954703324,
0.865285151446149,
0.4590424126447909,
0.9320188770520323,
1.2566497496880387,
1.092195469867563,
1.1523766803990934,
1.1257555973909357,
1.0506441818153138,
1.5101269591969075,
1.4541940510170406,
1.0403320961662672,
0.6435721407405451,
0.8163247288505439,
0.4178187803641794,
0.09659517321642153,
0.45565501812146547,
0.7800744768194877,
1.1266848495467974,
1.2708917952148475,
1.3699637357813657,
1.119789948351233,
1.117797940539129,
1.00138292407247,
-0.5357586740093938,
0.9678918429118637,
1.1063917602204467,
0.6309338947655425,
0.5056026636112202,
0.18233746755391178,
0.48969623185962496,
0.0383434172496539,
0.4599318303590843,
0.5291405395663644,
0.05265539291739823,
0.08586187516518445,
0.3449932185560655,
-0.100853852934114,
0.3440262305668972,
0.5284856664940929,
0.6022770024221724,
0.9832806104256895,
0.8094695843624107,
1.242977252512111,
0.9569361971250467,
0.7403221157223554,
0.5468202659043195,
],
type: 'number',
calcs: null,
},
{
name: 'Min',
config: {},
values: [
0.5129631778211692,
0.7232700407273998,
0.16692418924289476,
1.024024764754911,
0.42165950459107204,
-0.3468147988918235,
-0.3167406429414228,
0.6210986915363178,
0.561637573529631,
-1.5683226420805032,
-0.6132946301719228,
-1.2930551884568044,
0.5969846324236683,
-0.6778519171751289,
1.0732733813642974,
-0.0485285093265273,
-1.4112601180935862,
0.4756584208435448,
-1.012064501202603,
-0.5689593016201331,
-1.3031450412933574,
-1.7399086775804553,
-0.4439715146977987,
-0.9065877953376303,
0.6573706451163406,
-0.444001654734782,
-0.8360433807048399,
-1.52827092245891,
-0.7065563339902561,
-0.865577257288909,
0.3329923695343399,
1.5753706037331285,
-1.0567027770721693,
-0.2765803248460174,
-0.5510056746352008,
-0.1362911756340101,
0.34259473615447383,
-0.862039744634204,
-0.2241446290872408,
0.0566330081088942,
-1.786862521362104,
-1.2217196242864676,
-0.5525489161472246,
-0.446196522037665,
-1.08755269321232,
-0.991794378457034,
-1.3196225564796082,
0.3304532639159836,
-0.6722727699185566,
-0.280099872766854,
-0.6484766249626335,
-2.2303404241475,
-1.28856885905242,
-2.07345573739152,
-2.581726910011512,
-2.692554424690295,
-1.899011280327467,
-2.276770149534113,
-2.906877502022452,
-2.07669049858435,
-1.862840289223948,
-1.086158472280301,
-1.57239417318637,
-1.39281505610553,
-1.750680246703866,
-1.528573710879417,
-1.281799433260939,
-0.2599408767486406,
-1.586049427178506,
-1.6670976842241279,
-0.684309922512285,
-0.8545089289126312,
0.5201061712499606,
-1.6652198364354112,
-0.9509609321749597,
0.4192582642664334,
-1.196222288493242,
-0.494764687123139,
1.3552818811266552,
-1.0234990807153594,
0.46793077094438473,
-1.7338037743327357,
-1.5493688554047804,
0.255316081309259,
0.7604396194452134,
-0.9407405613725452,
-0.943211396012134,
0.8183880116376108,
1.2329439293285518,
-0.7894319837927357,
-0.1457874726259571,
-1.5441109867114653,
-1.6234083951372025,
0.06122646291653344,
-0.672772416353517,
-1.855602297194385,
-0.8581361043718111,
-1.2492249157608226,
0.7605483882403175,
0.6895236955186106,
0.5298057924915338,
0.6476408123812207,
0.8803790600375676,
-1.6612590895779469,
0.39253794740960923,
0.09690185316807232,
0.6185716145887097,
-1.009104994225043,
-0.231484679530671,
-1.471267622823986,
-0.0833848908404255,
-0.8881970225399474,
-1.8962127836845482,
0.009913417830268,
-0.134180309099165,
-0.1502931584064166,
-0.91729270269899,
-1.3759721816599786,
0.16530985337628945,
-1.134691317842945,
-0.2562806810548419,
-0.1965185306373604,
0.6468148901350499,
-0.5662636534063714,
-1.32379253681237,
-1.702413625201898,
],
type: 'number',
calcs: null,
},
{
name: 'Max',
config: {},
values: [
2.8039573837115217,
2.6926679712037664,
1.372618545664734,
0.2330280293445357,
3.257945982361415,
3.760438343013144,
3.059552434000727,
1.77318515129639,
0.8064077504286082,
1.944602327610411,
1.4599691331772737,
1.7681328471222444,
0.577781199251837,
3.3997499156309985,
2.9528330295399954,
2.430318614428602,
2.3611702277354194,
2.630203980321264,
2.6988109280000505,
2.3242683979742083,
2.917442297706927,
2.2740903408929314,
3.2823972469296656,
1.5795427555845518,
0.7227749081990877,
1.07447590570726,
1.6951780861683337,
0.368757195632994,
2.3404684729518603,
2.130240287562029,
1.3899466672909542,
0.8059006834071859,
1.3584656113532596,
3.133274081047666,
3.002448585766932,
1.787231129006741,
1.4640413244581476,
3.276124198626958,
2.087269772786033,
2.30377367911983,
1.4608814767279665,
1.8833281423383506,
1.3067082695296433,
1.253273064443,
1.969057793049115,
1.3339218780045436,
2.062355825798883,
1.1698837256390395,
2.6118487933225496,
2.03861316329396,
0.8473854714851563,
2.2316983803163,
0.540932137109768,
-0.04956492174973,
-0.450813944553707,
0.375695756046867,
0.785759004771657,
0.0954526298052838,
0.59597948146765,
0.6572571584655279,
0.605149574038378,
1.870031884630303,
1.37713543903307,
0.914062948450487,
2.649030343824894,
0.579644863230952,
1.944602570596079,
0.7252492696045203,
1.257270614288119,
1.9385686341149715,
1.797919709901303,
2.5158662971442625,
1.4514368283700079,
0.866789052718946,
1.3049445896679253,
2.198601835481704,
1.8277158002442289,
1.24254816068483,
0.990008891408783,
3.181979463237438,
1.206035697890917,
1.1511435801749499,
2.495205603723621,
0.574817574306939,
0.383599298845083,
3.6200850261933706,
3.5922041855779407,
1.5478860273718356,
2.8506619018807706,
3.4281960853527425,
3.289980262325269,
1.103020879390874,
1.942842426855671,
0.857081451423311,
2.112393659806772,
1.601319369146539,
2.230806028974528,
1.5729019554294,
2.4371495138987163,
2.2635324007929634,
0.5089790871644464,
1.3764330789309522,
0.535805463302483,
2.6704309768995014,
1.605171233903551,
1.3849464601885664,
1.4699084469214156,
1.2065460833969008,
2.665190338566064,
2.65455092203953,
1.33562376437657,
1.6303855496985555,
2.2931655808635956,
0.53289540133395,
2.42344985717817,
1.034880799185153,
2.02710661062796,
0.589535726373407,
1.1198114199523561,
2.3500012113011195,
2.911904933444892,
3.271532648889891,
2.181016258408353,
3.1900649798681133,
0.154494474449462,
2.7911175973201736,
],
type: 'number',
calcs: null,
},
{
name: 'Info',
config: {},
values: [
'up',
'down fast',
'down',
'up fast',
'up',
'up',
'down fast',
'up',
'down',
'down fast',
'down',
'up fast',
'up',
'down',
'down',
'down',
'up',
'down',
'down fast',
'down',
'up',
'down',
'up fast',
'down',
'down',
'down fast',
'up fast',
'down fast',
'up',
'down',
'down',
'up fast',
'up fast',
'down',
'up',
'down',
'down',
'up',
'down fast',
'down',
'up',
'up',
'down',
'down',
'down fast',
'up fast',
'down',
'up',
'down',
'down',
'up',
'down',
'down',
'down',
'down',
'down',
'up fast',
'down',
'down',
'up',
'up',
'up',
'up',
'up fast',
'up',
'down',
'up',
'up',
'down',
'up',
'down',
'up',
'up',
'up',
'up',
'down',
'down',
'down',
'up fast',
'up',
'up',
'down fast',
'up fast',
'up',
'down',
'up',
'down',
'down',
'up fast',
'down',
'down fast',
'down',
'up',
'down',
'down',
'up',
'up',
'up',
'up',
'up',
'down',
'down',
'down',
'down fast',
'up fast',
'up',
'down fast',
'down',
'down',
'up',
'down fast',
'up fast',
'up',
'down fast',
'up',
'up',
'down fast',
'up fast',
'up',
'up',
'up',
'down',
'up fast',
'down',
'down',
'down',
],
type: 'string',
calcs: null,
},
],
});
export const simple = () => {
return <NewTable data={mockDataFrame} height={500} width={500} />;
};

View File

@ -1,140 +0,0 @@
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>
);
};

View File

@ -1,103 +1,153 @@
// 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);
}
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));
}
return {
name: colId,
type: FieldType.string,
config: {},
values,
};
}),
length: rowCount,
};
}
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?
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),
});
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,
},
},
},
],
});
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 applyFieldOverrides({
data: [data],
fieldOptions: {
overrides,
defaults: {},
},
theme,
replaceVariables: (value: string) => value,
})[0];
}
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>
);
};
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 (
<div className="panel-container" style={{ width: 'auto' }}>
<Table data={data} height={500} width={width} />
</div>
);
};
const defaultThresholds = [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
];
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>
);
};

View File

@ -1,48 +0,0 @@
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();
});
});

View File

@ -1,324 +1,104 @@
// Libraries
import _ from 'lodash';
import React, { Component, ReactElement } from 'react';
import {
SortDirectionType,
SortIndicator,
MultiGrid,
CellMeasurerCache,
CellMeasurer,
GridCellProps,
Index,
} from 'react-virtualized';
import { Themeable } from '../../types/theme';
import React, { useMemo, CSSProperties } from 'react';
import { DataFrame } from '@grafana/data';
// @ts-ignore
import { useSortBy, useTable, useBlockLayout } from 'react-table';
import { FixedSizeList } from 'react-window';
import { getTableStyles } from './styles';
import { getColumns, getTableRows } from './utils';
import { TableColumn } from './types';
import { useTheme } from '../../themes';
import {
stringToJsRegex,
DataFrame,
sortDataFrame,
getDataFrameRow,
ArrayVector,
FieldType,
InterpolateFunction,
} from '@grafana/data';
import {
TableCellBuilder,
ColumnStyle,
getFieldCellBuilder,
TableCellBuilderOptions,
simpleCellBuilder,
} from './TableCellBuilder';
export interface Props extends Themeable {
export interface Props {
data: DataFrame;
minColumnWidth: number;
showHeader: boolean;
fixedHeader: boolean;
fixedColumns: number;
rotate: boolean;
styles: ColumnStyle[];
replaceVariables: InterpolateFunction;
width: number;
height: number;
isUTC?: boolean;
onCellClick?: TableFilterActionCallback;
}
interface State {
sortBy?: number;
sortDirection?: SortDirectionType;
data: DataFrame;
}
type TableFilterActionCallback = (key: string, value: string) => void;
interface ColumnRenderInfo {
header: string;
width: number;
builder: TableCellBuilder;
}
export const Table = ({ data, height, onCellClick, width }: Props) => {
const theme = useTheme();
const tableStyles = getTableStyles(theme);
interface DataIndex {
column: number;
row: number; // -1 is the header!
}
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns: useMemo(() => getColumns(data, width, theme), [data]),
data: useMemo(() => getTableRows(data), [data]),
tableStyles,
},
useSortBy,
useBlockLayout
);
export class Table extends Component<Props, State> {
renderer: ColumnRenderInfo[];
measurer: CellMeasurerCache;
scrollToTop = false;
rotateWidth = 100;
const RenderRow = React.useCallback(
({ index, style }) => {
const row = rows[index];
prepareRow(row);
return (
<div {...row.getRowProps({ style })} className={tableStyles.row}>
{row.cells.map((cell: RenderCellProps) => renderCell(cell, onCellClick))}
</div>
);
},
[prepareRow, rows]
);
static defaultProps = {
showHeader: true,
fixedHeader: true,
fixedColumns: 0,
rotate: false,
minColumnWidth: 150,
};
constructor(props: Props) {
super(props);
this.state = {
data: props.data,
};
this.renderer = this.initColumns(props);
this.measurer = new CellMeasurerCache({
defaultHeight: 30,
fixedWidth: true,
});
}
componentDidUpdate(prevProps: Props, prevState: State) {
const { data, styles, showHeader, rotate } = 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);
}
if (dataChanged || rotate !== prevProps.rotate) {
const { width, minColumnWidth } = this.props;
this.rotateWidth = Math.max(width / data.length, minColumnWidth);
}
// Update the data when data or sort changes
if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
this.scrollToTop = true;
this.setState({ data: sortDataFrame(data, sortBy, sortDirection === 'DESC') });
}
}
/** Given the configuration, setup how each column gets rendered */
initColumns(props: Props): ColumnRenderInfo[] {
const { styles, data, width, minColumnWidth } = props;
if (!data || !data.fields || !data.fields.length || !styles) {
return [];
}
const columnWidth = Math.max(width / data.fields.length, minColumnWidth);
return data.fields.map((col, index) => {
let title = col.name;
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,
width: columnWidth,
builder: getFieldCellBuilder(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 DataFrame 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 field = this.state.data.fields[columnIndex];
const value = field.values.get(rowIndex);
console.log('CLICK', value, field.name);
}
};
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.fields[column];
const sorting = sortBy === column;
if (!col) {
col = {
name: '??' + columnIndex + '???',
config: {},
values: new ArrayVector(),
type: FieldType.other,
};
}
return (
<div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
{col.name}
{sorting && <SortIndicator sortDirection={sortDirection} />}
return (
<div {...getTableProps()} className={tableStyles.table}>
<div>
{headerGroups.map((headerGroup: any) => (
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any) => renderHeaderCell(column, tableStyles.headerCell))}
</div>
))}
</div>
);
};
<FixedSizeList height={height} itemCount={rows.length} itemSize={tableStyles.rowHeight} width={width}>
{RenderRow}
</FixedSizeList>
</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.fields : getDataFrameRow(data, row); // TODO! improve
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.fields[column],
table: this,
props,
})}
</CellMeasurer>
);
};
getColumnWidth = (col: Index): number => {
if (this.props.rotate) {
return this.rotateWidth; // fixed for now
}
return this.renderer[col.index].width;
};
render() {
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
const { data } = this.state;
if (!data || !data.fields || !data.fields.length) {
return <span>Missing Fields</span>; // nothing
}
let columnCount = data.fields.length;
let rowCount = data.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;
}
// Force MultiGrid to rerender if these options change
// See: https://github.com/bvaughn/react-virtualized#pass-thru-props
const refreshKeys = {
...this.state, // Includes data and sort parameters
d1: this.props.data,
s0: this.props.styles,
};
return (
<MultiGrid
{...refreshKeys}
scrollToRow={scrollToRow}
columnCount={columnCount}
scrollToColumn={scrollToColumn}
rowCount={rowCount}
overscanColumnCount={8}
overscanRowCount={8}
columnWidth={this.getColumnWidth}
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"
/>
);
}
interface RenderCellProps {
column: TableColumn;
value: any;
getCellProps: () => { style: CSSProperties };
render: (component: string) => React.ReactNode;
}
export default Table;
function renderCell(cell: RenderCellProps, onCellClick?: TableFilterActionCallback) {
const filterable = cell.column.field.config.filterable;
const cellProps = cell.getCellProps();
let onClick: ((event: React.SyntheticEvent) => void) | undefined = undefined;
if (filterable && onCellClick) {
cellProps.style.cursor = 'pointer';
onClick = () => onCellClick(cell.column.Header, cell.value);
}
if (cell.column.textAlign) {
cellProps.style.textAlign = cell.column.textAlign;
}
return (
<div {...cellProps} onClick={onClick}>
{cell.render('Cell')}
</div>
);
}
function renderHeaderCell(column: any, className: string) {
const headerProps = column.getHeaderProps(column.getSortByToggleProps());
if (column.textAlign) {
headerProps.style.textAlign = column.textAlign;
}
return (
<div className={className} {...headerProps}>
{column.render('Header')}
<span>{column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}</span>
</div>
);
}

View File

@ -1,324 +0,0 @@
// 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>
);
};
}

View File

@ -1,169 +0,0 @@
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],
],
};

View File

@ -0,0 +1,59 @@
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;
`,
};
}
);

View File

@ -0,0 +1,45 @@
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;
}

View File

@ -0,0 +1,66 @@
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');
});
});
});

View File

@ -0,0 +1,88 @@
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;
}

View File

@ -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,
},

View File

@ -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};
`,
});

View File

@ -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';

View File

@ -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';

View File

@ -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;

View File

@ -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',

View File

@ -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);
});
});

View File

@ -1,9 +1,18 @@
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 {

View File

@ -1,20 +1,20 @@
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,

View File

@ -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[];

View File

@ -1,6 +1,5 @@
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;
}

View File

@ -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} />;
}
}

View File

@ -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>
);

View File

@ -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);

View File

@ -1,34 +1,14 @@
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 = {
fieldOptions: {
defaults: {},
overrides: [],
},
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)'],
pattern: '/.*/',
thresholds: [],
},
],
};

View File

@ -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"