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
co-authored by Ryan McKinley
parent 8d537b7afb
commit 3347b45a95
43 changed files with 750 additions and 1918 deletions
+1 -1
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",
@@ -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,
@@ -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};
@@ -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>
);
};
@@ -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>;
};
@@ -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} />;
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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();
});
});
@@ -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>
);
}
@@ -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>
);
};
}
@@ -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],
],
};
@@ -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;
`,
};
}
);
@@ -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;
}
@@ -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');
});
});
});
@@ -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;
}
@@ -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';
+3 -3
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';
@@ -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;
+2 -2
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',