diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index 87f71d8e757..8d71a89dfed 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -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 diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 71503f2ceb3..9203f077ce3 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -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, diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index eb37d811e4e..cabf37841d0 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -44,13 +44,23 @@ export interface FieldConfig { // Alternative to empty string noValue?: string; - // Visual options color?: string; + + custom?: Record; } export interface Field> { - 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; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 1888479aeb6..04ff7681fba 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -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", diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx index dbb9cd0d70e..fbc50c7838c 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx @@ -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) { 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) { } 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, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index 332c00cf80f..e5371b4b110 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -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 { const props: Props = { maxValue: 100, minValue: 0, - displayMode: 'basic', + displayMode: BarGaugeDisplayMode.Basic, thresholds: [ { value: -Infinity, color: 'green' }, { value: 70, color: 'orange' }, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 72a6cbc3b8f..2e98f2e3d6a 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -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; className?: string; showUnfilled?: boolean; alignmentFactors?: DisplayValueAlignmentFactors; } +export enum BarGaugeDisplayMode { + Basic = 'basic', + Lcd = 'lcd', + Gradient = 'gradient', +} + export class BarGauge extends PureComponent { static defaultProps: Partial = { 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 { } 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 { 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); diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index 62e97fa3ef1..6efd2322239 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -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` diff --git a/packages/grafana-ui/src/components/Forms/commonStyles.ts b/packages/grafana-ui/src/components/Forms/commonStyles.ts index ac5568d113b..6d6d850824d 100644 --- a/packages/grafana-ui/src/components/Forms/commonStyles.ts +++ b/packages/grafana-ui/src/components/Forms/commonStyles.ts @@ -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}; diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx new file mode 100644 index 00000000000..76cedffd645 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -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 = 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 ( +
+ +
+ ); +}; diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx new file mode 100644 index 00000000000..2ccbfae3556 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -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 = props => { + const { column, cell, tableStyles } = props; + + if (!column.field.display) { + return null; + } + + const displayValue = column.field.display(cell.value); + return
{formattedValueToString(displayValue)}
; +}; + +export const BackgroundColoredCell: FC = 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
{formattedValueToString(displayValue)}
; +}; diff --git a/packages/grafana-ui/src/components/Table/NewTable.story.tsx b/packages/grafana-ui/src/components/Table/NewTable.story.tsx deleted file mode 100644 index e2f12fad433..00000000000 --- a/packages/grafana-ui/src/components/Table/NewTable.story.tsx +++ /dev/null @@ -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 ; -}; diff --git a/packages/grafana-ui/src/components/Table/NewTable.tsx b/packages/grafana-ui/src/components/Table/NewTable.tsx deleted file mode 100644 index ee4ae972426..00000000000 --- a/packages/grafana-ui/src/components/Table/NewTable.tsx +++ /dev/null @@ -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 ( -
onCellClick(cell.column.Header, cell.value) : undefined} - style={style} - > - {cell.render('Cell')} -
- ); -}; - -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 ( -
- {row.cells.map((cell: any) => renderCell(cell, columnWidth, tableStyles.tableCell, onCellClick))} -
- ); - }, - [prepareRow, rows] - ); - - return ( -
-
- {headerGroups.map((headerGroup: any) => ( -
- {headerGroup.headers.map((column: any) => ( -
- {column.render('Header')} - {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''} -
- ))} -
- ))} -
- - {RenderRow} - -
- ); -}; diff --git a/packages/grafana-ui/src/components/Table/NewTable.mdx b/packages/grafana-ui/src/components/Table/Table.mdx similarity index 100% rename from packages/grafana-ui/src/components/Table/NewTable.mdx rename to packages/grafana-ui/src/components/Table/Table.mdx diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 0ad2bc135e0..65bc759552b 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -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(); - 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 ( +
+ + + ); +}; + +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 ( +
+
+ + ); +}; + +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 ( +
+
+ + ); +}; diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx deleted file mode 100644 index 34512cde7c5..00000000000 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ /dev/null @@ -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(
); - 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(); - }); -}); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index e81678d2669..8a0c46f1adb 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -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 { - renderer: ColumnRenderInfo[]; - measurer: CellMeasurerCache; - scrollToTop = false; - rotateWidth = 100; + const RenderRow = React.useCallback( + ({ index, style }) => { + const row = rows[index]; + prepareRow(row); + return ( +
+ {row.cells.map((cell: RenderCellProps) => renderCell(cell, onCellClick))} +
+ ); + }, + [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 ( -
this.onCellClick(rowIndex, columnIndex)}> - {col.name} - {sorting && } + return ( +
+
+ {headerGroups.map((headerGroup: any) => ( +
+ {headerGroup.headers.map((column: any) => renderHeaderCell(column, tableStyles.headerCell))} +
+ ))}
- ); - }; + + {RenderRow} + +
+ ); +}; - 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 ( - - {builder({ - value, - row: rowData, - column: data.fields[column], - table: this, - props, - })} - - ); - }; - - 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 Missing Fields; // 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 ( - - ); - } +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 ( +
+ {cell.render('Cell')} +
+ ); +} + +function renderHeaderCell(column: any, className: string) { + const headerProps = column.getHeaderProps(column.getSortByToggleProps()); + + if (column.textAlign) { + headerProps.style.textAlign = column.textAlign; + } + + return ( +
+ {column.render('Header')} + {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''} +
+ ); +} diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx deleted file mode 100644 index b0384b4d2ef..00000000000 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ /dev/null @@ -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 ( -
- {value} -
- ); -}; - -// *************************************************************************** -// 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 = ( - - {value} - - ); - } - - // ??? I don't think this will still work! - if (this.schema.filterable) { - cellClasses.push('table-panel-cell-filterable'); - value = ( - <> - {value} - - - - - - - - - - ); - } - - 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 ( -
- {formattedValueToString(disp)} -
- ); - }; -} diff --git a/packages/grafana-ui/src/components/Table/examples.ts b/packages/grafana-ui/src/components/Table/examples.ts deleted file mode 100644 index df8ad7ddc6b..00000000000 --- a/packages/grafana-ui/src/components/Table/examples.ts +++ /dev/null @@ -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¶m_1=$__cell_1¶m_2=$__cell_2', - linkTooltip: '$__cell $__cell_1 $__cell_6', - linkTargetBlank: true, - }, - { - pattern: 'Array', - type: 'number', - unit: 'ms', - decimals: 3, - }, - { - pattern: 'Mapping', - type: 'string', - mappingType: 1, - valueMaps: [ - { - value: '1', - 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], - ], -}; diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts new file mode 100644 index 00000000000..846d00f2ed8 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -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; + `, + }; + } +); diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts new file mode 100644 index 00000000000..b48f50778a0 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -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; + // 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; +} diff --git a/packages/grafana-ui/src/components/Table/utils.test.ts b/packages/grafana-ui/src/components/Table/utils.test.ts new file mode 100644 index 00000000000..45b63a420aa --- /dev/null +++ b/packages/grafana-ui/src/components/Table/utils.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts new file mode 100644 index 00000000000..d21bb42493f --- /dev/null +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -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; +} diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx similarity index 100% rename from packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx rename to packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.test.tsx similarity index 100% rename from packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx rename to packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.test.tsx diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx similarity index 100% rename from packages/grafana-ui/src/components/Table/TableInputCSV.tsx rename to packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx diff --git a/packages/grafana-ui/src/components/Table/_TableInputCSV.scss b/packages/grafana-ui/src/components/TableInputCSV/_TableInputCSV.scss similarity index 100% rename from packages/grafana-ui/src/components/Table/_TableInputCSV.scss rename to packages/grafana-ui/src/components/TableInputCSV/_TableInputCSV.scss diff --git a/packages/grafana-ui/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap b/packages/grafana-ui/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap index 1dc6507c838..a853e65a0c8 100644 --- a/packages/grafana-ui/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap +++ b/packages/grafana-ui/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap @@ -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, }, diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx index fc1e23a9c2d..23bf54298a3 100644 --- a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx @@ -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}; `, }); diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index 55fa6175325..b3f50fe7cea 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -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'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 724b3f05023..85fc25ed7f9 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts index 4736cb532ab..8180c4a7666 100644 --- a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts @@ -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; diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts index 909efc8448a..33d5ba290ec 100644 --- a/packages/grafana-ui/src/themes/default.ts +++ b/packages/grafana-ui/src/themes/default.ts @@ -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', diff --git a/public/app/features/explore/utils/ResultProcessor.test.ts b/public/app/features/explore/utils/ResultProcessor.test.ts index 70349321dcf..46eaad357d3 100644 --- a/public/app/features/explore/utils/ResultProcessor.test.ts +++ b/public/app/features/explore/utils/ResultProcessor.test.ts @@ -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); }); }); diff --git a/public/app/features/explore/utils/ResultProcessor.ts b/public/app/features/explore/utils/ResultProcessor.ts index 5e290d43b16..e25b907153e 100644 --- a/public/app/features/explore/utils/ResultProcessor.ts +++ b/public/app/features/explore/utils/ResultProcessor.ts @@ -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 { diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts index ea4851e7701..6c25c1c6810 100644 --- a/public/app/plugins/panel/bargauge/types.ts +++ b/public/app/plugins/panel/bargauge/types.ts @@ -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> = [ - { 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, diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 752ec2c0893..ae631d35fc4 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -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[]; diff --git a/public/app/plugins/panel/table/types.ts b/public/app/plugins/panel/table/types.ts index 10f46e99b96..01ba33e0692 100644 --- a/public/app/plugins/panel/table/types.ts +++ b/public/app/plugins/panel/table/types.ts @@ -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; +} diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index c8fc5155474..1e30e901f61 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -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 {} -// 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 { constructor(props: Props) { @@ -17,12 +17,19 @@ export class TablePanel extends Component { } render() { - const { data, height, width } = this.props; + const { data, height, width, replaceVariables, options } = this.props; if (data.series.length < 1) { return
No Table Data...
; } - return
; + const dataProcessed = applyFieldOverrides({ + data: data.series, + fieldOptions: options.fieldOptions, + theme: config.theme, + replaceVariables, + })[0]; + + return
; } } diff --git a/public/app/plugins/panel/table2/TablePanelEditor.tsx b/public/app/plugins/panel/table2/TablePanelEditor.tsx index 98574b222ae..80d9811c0b7 100644 --- a/public/app/plugins/panel/table2/TablePanelEditor.tsx +++ b/public/app/plugins/panel/table2/TablePanelEditor.tsx @@ -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> { @@ -12,43 +12,14 @@ export class TablePanelEditor extends PureComponent> { 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 (
Header
- -
- -
-
Display
- -
); diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx index 5373cb486d4..e8fa2a510dd 100644 --- a/public/app/plugins/panel/table2/module.tsx +++ b/public/app/plugins/panel/table2/module.tsx @@ -4,4 +4,7 @@ import { TablePanelEditor } from './TablePanelEditor'; import { TablePanel } from './TablePanel'; import { Options, defaults } from './types'; -export const plugin = new PanelPlugin(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor); +export const plugin = new PanelPlugin(TablePanel) + .setNoPadding() + .setDefaults(defaults) + .setEditor(TablePanelEditor); diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 0798cc05d08..8a4cf873f47 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -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: [], - }, - ], }; diff --git a/yarn.lock b/yarn.lock index bd8379f5467..a11c83cf29f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"