mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
8d537b7afb
commit
3347b45a95
@ -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
|
||||
|
@ -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,
|
||||
|
@ -44,13 +44,23 @@ export interface FieldConfig {
|
||||
// Alternative to empty string
|
||||
noValue?: string;
|
||||
|
||||
// Visual options
|
||||
color?: string;
|
||||
|
||||
custom?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Field<T = any, V = Vector<T>> {
|
||||
name: string; // The column name
|
||||
/**
|
||||
* Name of the field (column)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Field value type (string, number, etc)
|
||||
*/
|
||||
type: FieldType;
|
||||
/**
|
||||
* Meta info about how field and how to display it
|
||||
*/
|
||||
config: FieldConfig;
|
||||
values: V; // The raw field values
|
||||
labels?: Labels;
|
||||
|
@ -50,7 +50,7 @@
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "1.3.3",
|
||||
"react-storybook-addon-props-combinations": "1.1.0",
|
||||
"react-table": "7.0.0-rc.4",
|
||||
"react-table": "7.0.0-rc.15",
|
||||
"react-transition-group": "2.6.1",
|
||||
"react-virtualized": "9.21.0",
|
||||
"slate": "0.47.8",
|
||||
|
@ -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};
|
||||
|
49
packages/grafana-ui/src/components/Table/BarGaugeCell.tsx
Normal file
49
packages/grafana-ui/src/components/Table/BarGaugeCell.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { FC } from 'react';
|
||||
import { ReactTableCellProps, TableCellDisplayMode } from './types';
|
||||
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
|
||||
const defaultThresholds = [
|
||||
{
|
||||
color: 'blue',
|
||||
value: -Infinity,
|
||||
},
|
||||
{
|
||||
color: 'green',
|
||||
value: 20,
|
||||
},
|
||||
];
|
||||
|
||||
export const BarGaugeCell: FC<ReactTableCellProps> = props => {
|
||||
const { column, tableStyles, cell } = props;
|
||||
const { field } = column;
|
||||
|
||||
if (!field.display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayValue = field.display(cell.value);
|
||||
let barGaugeMode = BarGaugeDisplayMode.Gradient;
|
||||
|
||||
if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.LcdGauge) {
|
||||
barGaugeMode = BarGaugeDisplayMode.Lcd;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tableStyles.tableCell}>
|
||||
<BarGauge
|
||||
width={column.width - tableStyles.cellPadding * 2}
|
||||
height={tableStyles.cellHeightInner}
|
||||
thresholds={field.config.thresholds || defaultThresholds}
|
||||
value={displayValue}
|
||||
maxValue={field.config.max || 100}
|
||||
minValue={field.config.min || 0}
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={tableStyles.theme}
|
||||
itemSpacing={1}
|
||||
lcdCellWidth={8}
|
||||
displayMode={barGaugeMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
41
packages/grafana-ui/src/components/Table/DefaultCell.tsx
Normal file
41
packages/grafana-ui/src/components/Table/DefaultCell.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { FC, CSSProperties } from 'react';
|
||||
import { ReactTableCellProps } from './types';
|
||||
import { formattedValueToString } from '@grafana/data';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export const DefaultCell: FC<ReactTableCellProps> = props => {
|
||||
const { column, cell, tableStyles } = props;
|
||||
|
||||
if (!column.field.display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayValue = column.field.display(cell.value);
|
||||
return <div className={tableStyles.tableCell}>{formattedValueToString(displayValue)}</div>;
|
||||
};
|
||||
|
||||
export const BackgroundColoredCell: FC<ReactTableCellProps> = props => {
|
||||
const { column, cell, tableStyles } = props;
|
||||
|
||||
if (!column.field.display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
|
||||
const displayValue = column.field.display(cell.value);
|
||||
|
||||
const bgColor2 = tinycolor(displayValue.color)
|
||||
.darken(10 * themeFactor)
|
||||
.spin(5)
|
||||
.toRgbString();
|
||||
|
||||
const styles: CSSProperties = {
|
||||
background: `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`,
|
||||
borderRadius: '0px',
|
||||
color: 'white',
|
||||
height: tableStyles.cellHeight,
|
||||
padding: tableStyles.cellPadding,
|
||||
};
|
||||
|
||||
return <div style={styles}>{formattedValueToString(displayValue)}</div>;
|
||||
};
|
@ -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¶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],
|
||||
],
|
||||
};
|
59
packages/grafana-ui/src/components/Table/styles.ts
Normal file
59
packages/grafana-ui/src/components/Table/styles.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, selectThemeVariant as stv } from '../../themes';
|
||||
|
||||
export interface TableStyles {
|
||||
cellHeight: number;
|
||||
cellHeightInner: number;
|
||||
cellPadding: number;
|
||||
rowHeight: number;
|
||||
table: string;
|
||||
thead: string;
|
||||
headerCell: string;
|
||||
tableCell: string;
|
||||
row: string;
|
||||
theme: GrafanaTheme;
|
||||
}
|
||||
|
||||
export const getTableStyles = stylesFactory(
|
||||
(theme: GrafanaTheme): TableStyles => {
|
||||
const colors = theme.colors;
|
||||
const headerBg = stv({ light: colors.gray6, dark: colors.dark7 }, theme.type);
|
||||
const padding = 6;
|
||||
const lineHeight = theme.typography.lineHeight.md;
|
||||
const bodyFontSize = 14;
|
||||
const cellHeight = padding * 2 + bodyFontSize * lineHeight;
|
||||
|
||||
return {
|
||||
theme,
|
||||
cellHeight,
|
||||
cellPadding: padding,
|
||||
cellHeightInner: bodyFontSize * lineHeight,
|
||||
rowHeight: cellHeight + 2,
|
||||
table: css`
|
||||
overflow: auto;
|
||||
border-spacing: 0;
|
||||
`,
|
||||
thead: css`
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: ${headerBg};
|
||||
`,
|
||||
headerCell: css`
|
||||
padding: ${padding}px 10px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: ${colors.blue};
|
||||
`,
|
||||
row: css`
|
||||
border-bottom: 2px solid ${colors.bodyBg};
|
||||
`,
|
||||
tableCell: css`
|
||||
padding: ${padding}px 10px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`,
|
||||
};
|
||||
}
|
||||
);
|
45
packages/grafana-ui/src/components/Table/types.ts
Normal file
45
packages/grafana-ui/src/components/Table/types.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { TextAlignProperty } from 'csstype';
|
||||
import { ComponentType } from 'react';
|
||||
import { Field } from '@grafana/data';
|
||||
import { TableStyles } from './styles';
|
||||
|
||||
export interface TableFieldOptions {
|
||||
width: number;
|
||||
align: FieldTextAlignment;
|
||||
displayMode: TableCellDisplayMode;
|
||||
}
|
||||
|
||||
export enum TableCellDisplayMode {
|
||||
Auto = 'auto',
|
||||
ColorText = 'color-text',
|
||||
ColorBackground = 'color-background',
|
||||
GradientGauge = 'gradient-gauge',
|
||||
LcdGauge = 'lcd-gauge',
|
||||
}
|
||||
|
||||
export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center';
|
||||
|
||||
export interface TableColumn {
|
||||
// React table props
|
||||
Header: string;
|
||||
accessor: string | Function;
|
||||
Cell: ComponentType<ReactTableCellProps>;
|
||||
// Grafana additions
|
||||
field: Field;
|
||||
width: number;
|
||||
textAlign: TextAlignProperty;
|
||||
}
|
||||
|
||||
export interface TableRow {
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export interface ReactTableCellProps {
|
||||
cell: ReactTableCell;
|
||||
column: TableColumn;
|
||||
tableStyles: TableStyles;
|
||||
}
|
||||
|
||||
export interface ReactTableCell {
|
||||
value: any;
|
||||
}
|
66
packages/grafana-ui/src/components/Table/utils.test.ts
Normal file
66
packages/grafana-ui/src/components/Table/utils.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { MutableDataFrame, GrafanaThemeType, FieldType } from '@grafana/data';
|
||||
import { getColumns } from './utils';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
function getData() {
|
||||
const data = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [] },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [],
|
||||
config: {
|
||||
custom: {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
type: FieldType.string,
|
||||
values: [],
|
||||
config: {
|
||||
custom: {
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
describe('Table utils', () => {
|
||||
describe('getColumns', () => {
|
||||
it('Should build columns from DataFrame', () => {
|
||||
const theme = getTheme(GrafanaThemeType.Dark);
|
||||
const columns = getColumns(getData(), 1000, theme);
|
||||
|
||||
expect(columns[0].Header).toBe('Time');
|
||||
expect(columns[1].Header).toBe('Value');
|
||||
});
|
||||
|
||||
it('Should distribute width and use field config width', () => {
|
||||
const theme = getTheme(GrafanaThemeType.Dark);
|
||||
const columns = getColumns(getData(), 1000, theme);
|
||||
|
||||
expect(columns[0].width).toBe(450);
|
||||
expect(columns[1].width).toBe(100);
|
||||
});
|
||||
|
||||
it('Should use textAlign from custom', () => {
|
||||
const theme = getTheme(GrafanaThemeType.Dark);
|
||||
const columns = getColumns(getData(), 1000, theme);
|
||||
|
||||
expect(columns[2].textAlign).toBe('center');
|
||||
});
|
||||
|
||||
it('Should set textAlign to right for number values', () => {
|
||||
const theme = getTheme(GrafanaThemeType.Dark);
|
||||
const columns = getColumns(getData(), 1000, theme);
|
||||
|
||||
expect(columns[1].textAlign).toBe('right');
|
||||
});
|
||||
});
|
||||
});
|
88
packages/grafana-ui/src/components/Table/utils.ts
Normal file
88
packages/grafana-ui/src/components/Table/utils.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { TextAlignProperty } from 'csstype';
|
||||
import { DataFrame, Field, GrafanaTheme, FieldType } from '@grafana/data';
|
||||
import { TableColumn, TableRow, TableFieldOptions, TableCellDisplayMode } from './types';
|
||||
import { BarGaugeCell } from './BarGaugeCell';
|
||||
import { DefaultCell, BackgroundColoredCell } from './DefaultCell';
|
||||
|
||||
export function getTableRows(data: DataFrame): TableRow[] {
|
||||
const tableData = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row: { [key: string]: string | number } = {};
|
||||
for (let j = 0; j < data.fields.length; j++) {
|
||||
const prop = data.fields[j].name;
|
||||
row[prop] = data.fields[j].values.get(i);
|
||||
}
|
||||
tableData.push(row);
|
||||
}
|
||||
|
||||
return tableData;
|
||||
}
|
||||
|
||||
function getTextAlign(field: Field): TextAlignProperty {
|
||||
if (field.config.custom) {
|
||||
const custom = field.config.custom as TableFieldOptions;
|
||||
|
||||
switch (custom.align) {
|
||||
case 'right':
|
||||
return 'right';
|
||||
case 'left':
|
||||
return 'left';
|
||||
case 'center':
|
||||
return 'center';
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.number) {
|
||||
return 'right';
|
||||
}
|
||||
|
||||
return 'left';
|
||||
}
|
||||
|
||||
export function getColumns(data: DataFrame, availableWidth: number, theme: GrafanaTheme): TableColumn[] {
|
||||
const cols: TableColumn[] = [];
|
||||
let fieldCountWithoutWidth = data.fields.length;
|
||||
|
||||
for (const field of data.fields) {
|
||||
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
|
||||
|
||||
if (fieldTableOptions.width) {
|
||||
availableWidth -= fieldTableOptions.width;
|
||||
fieldCountWithoutWidth -= 1;
|
||||
}
|
||||
|
||||
let Cell = DefaultCell;
|
||||
let textAlign = getTextAlign(field);
|
||||
|
||||
switch (fieldTableOptions.displayMode) {
|
||||
case TableCellDisplayMode.ColorBackground:
|
||||
Cell = BackgroundColoredCell;
|
||||
break;
|
||||
case TableCellDisplayMode.LcdGauge:
|
||||
case TableCellDisplayMode.GradientGauge:
|
||||
Cell = BarGaugeCell;
|
||||
textAlign = 'center';
|
||||
break;
|
||||
}
|
||||
|
||||
cols.push({
|
||||
field,
|
||||
Cell,
|
||||
textAlign,
|
||||
Header: field.name,
|
||||
accessor: field.name,
|
||||
width: fieldTableOptions.width,
|
||||
});
|
||||
}
|
||||
|
||||
// divide up the rest of the space
|
||||
const sharedWidth = availableWidth / fieldCountWithoutWidth;
|
||||
for (const column of cols) {
|
||||
if (!column.width) {
|
||||
column.width = sharedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return cols;
|
||||
}
|
@ -227,8 +227,8 @@ exports[`TimePicker renders buttons correctly 1`] = `
|
||||
"h6": "14px",
|
||||
},
|
||||
"lineHeight": Object {
|
||||
"lg": 1.5,
|
||||
"md": 1.3333333333333333,
|
||||
"lg": 2,
|
||||
"md": 1.5,
|
||||
"sm": 1.1,
|
||||
"xs": 1,
|
||||
},
|
||||
@ -534,8 +534,8 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
|
||||
"h6": "14px",
|
||||
},
|
||||
"lineHeight": Object {
|
||||
"lg": 1.5,
|
||||
"md": 1.3333333333333333,
|
||||
"lg": 2,
|
||||
"md": 1.5,
|
||||
"sm": 1.1,
|
||||
"xs": 1,
|
||||
},
|
||||
|
@ -53,7 +53,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
||||
label: type-ahead-item-group-title;
|
||||
color: ${theme.colors.textWeak};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
line-height: ${theme.typography.lineHeight.lg};
|
||||
line-height: ${theme.typography.lineHeight.md};
|
||||
padding: ${theme.spacing.sm};
|
||||
`,
|
||||
});
|
||||
|
@ -9,7 +9,7 @@
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
@import 'RefreshPicker/RefreshPicker';
|
||||
@import 'Select/Select';
|
||||
@import 'Table/TableInputCSV';
|
||||
@import 'TableInputCSV/TableInputCSV';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'TimePicker/TimeOfDayPicker';
|
||||
@import 'Tooltip/Tooltip';
|
||||
|
@ -46,8 +46,8 @@ export { QueryField } from './QueryField/QueryField';
|
||||
// Renderless
|
||||
export { SetInterval } from './SetInterval/SetInterval';
|
||||
|
||||
export { NewTable as Table } from './Table/NewTable';
|
||||
export { TableInputCSV } from './Table/TableInputCSV';
|
||||
export { Table } from './Table/Table';
|
||||
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
|
||||
|
||||
// Visualizations
|
||||
export {
|
||||
@ -62,7 +62,7 @@ export { Gauge } from './Gauge/Gauge';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { GraphLegend } from './Graph/GraphLegend';
|
||||
export { GraphWithLegend } from './Graph/GraphWithLegend';
|
||||
export { BarGauge } from './BarGauge/BarGauge';
|
||||
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
||||
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||
|
||||
|
@ -106,7 +106,7 @@ $font-size-md: ${theme.typography.size.md} !default;
|
||||
$font-size-sm: ${theme.typography.size.sm} !default;
|
||||
$font-size-xs: ${theme.typography.size.xs} !default;
|
||||
|
||||
$line-height-base: ${theme.typography.lineHeight.lg} !default;
|
||||
$line-height-base: ${theme.typography.lineHeight.md} !default;
|
||||
|
||||
$font-weight-regular: ${theme.typography.weight.regular} !default;
|
||||
$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
|
||||
|
@ -54,8 +54,8 @@ const theme: GrafanaThemeCommons = {
|
||||
lineHeight: {
|
||||
xs: 1,
|
||||
sm: 1.1,
|
||||
md: 4 / 3,
|
||||
lg: 1.5,
|
||||
md: 1.5,
|
||||
lg: 2,
|
||||
},
|
||||
link: {
|
||||
decoration: 'none',
|
||||
|
@ -130,8 +130,15 @@ describe('ResultProcessor', () => {
|
||||
describe('when calling getTableResult', () => {
|
||||
it('then it should return correct table result', () => {
|
||||
const { resultProcessor } = testContext();
|
||||
const theResult = resultProcessor.getTableResult();
|
||||
const resultDataFrame = toDataFrame(
|
||||
let theResult = resultProcessor.getTableResult();
|
||||
expect(theResult.fields[0].name).toEqual('value');
|
||||
expect(theResult.fields[1].name).toEqual('time');
|
||||
expect(theResult.fields[2].name).toEqual('message');
|
||||
expect(theResult.fields[1].display).not.toBeNull();
|
||||
expect(theResult.length).toBe(3);
|
||||
|
||||
// Same data though a DataFrame
|
||||
theResult = toDataFrame(
|
||||
new TableModel({
|
||||
columns: [
|
||||
{ text: 'value', type: 'number' },
|
||||
@ -146,8 +153,11 @@ describe('ResultProcessor', () => {
|
||||
type: 'table',
|
||||
})
|
||||
);
|
||||
|
||||
expect(theResult).toEqual(resultDataFrame);
|
||||
expect(theResult.fields[0].name).toEqual('value');
|
||||
expect(theResult.fields[1].name).toEqual('time');
|
||||
expect(theResult.fields[2].name).toEqual('message');
|
||||
expect(theResult.fields[1].display).not.toBeNull();
|
||||
expect(theResult.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { SingleStatBaseOptions } from '@grafana/ui';
|
||||
import { SingleStatBaseOptions, BarGaugeDisplayMode } from '@grafana/ui';
|
||||
import { standardGaugeFieldOptions } from '../gauge/types';
|
||||
import { VizOrientation, SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
||||
displayMode: BarGaugeDisplayMode;
|
||||
showUnfilled: boolean;
|
||||
}
|
||||
|
||||
export const displayModes: Array<SelectableValue<string>> = [
|
||||
{ value: 'gradient', label: 'Gradient' },
|
||||
{ value: 'lcd', label: 'Retro LCD' },
|
||||
{ value: 'basic', label: 'Basic' },
|
||||
{ value: BarGaugeDisplayMode.Gradient, label: 'Gradient' },
|
||||
{ value: BarGaugeDisplayMode.Lcd, label: 'Retro LCD' },
|
||||
{ value: BarGaugeDisplayMode.Basic, label: 'Basic' },
|
||||
];
|
||||
|
||||
export const defaults: BarGaugeOptions = {
|
||||
displayMode: 'lcd',
|
||||
displayMode: BarGaugeDisplayMode.Lcd,
|
||||
orientation: VizOrientation.Horizontal,
|
||||
fieldOptions: standardGaugeFieldOptions,
|
||||
showUnfilled: true,
|
||||
|
@ -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[];
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ import React, { Component } from 'react';
|
||||
|
||||
// Types
|
||||
import { Table } from '@grafana/ui';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { PanelProps, applyFieldOverrides } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
// So that the table does not go all the way to the edge of the panel chrome
|
||||
const paddingBottom = 35;
|
||||
const paddingBottom = 16;
|
||||
|
||||
export class TablePanel extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
@ -17,12 +17,19 @@ export class TablePanel extends Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, height, width } = this.props;
|
||||
const { data, height, width, replaceVariables, options } = this.props;
|
||||
|
||||
if (data.series.length < 1) {
|
||||
return <div>No Table Data...</div>;
|
||||
}
|
||||
|
||||
return <Table height={height - paddingBottom} width={width} data={data.series[0]} />;
|
||||
const dataProcessed = applyFieldOverrides({
|
||||
data: data.series,
|
||||
fieldOptions: options.fieldOptions,
|
||||
theme: config.theme,
|
||||
replaceVariables,
|
||||
})[0];
|
||||
|
||||
return <Table height={height - paddingBottom} width={width} data={dataProcessed} />;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
// Types
|
||||
import { PanelEditorProps } from '@grafana/data';
|
||||
import { Switch, FormField } from '@grafana/ui';
|
||||
import { Switch } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
|
||||
@ -12,43 +12,14 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
|
||||
this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader });
|
||||
};
|
||||
|
||||
onToggleFixedHeader = () => {
|
||||
this.props.onOptionsChange({ ...this.props.options, fixedHeader: !this.props.options.fixedHeader });
|
||||
};
|
||||
|
||||
onToggleRotate = () => {
|
||||
this.props.onOptionsChange({ ...this.props.options, rotate: !this.props.options.rotate });
|
||||
};
|
||||
|
||||
onFixedColumnsChange = ({ target }: any) => {
|
||||
this.props.onOptionsChange({ ...this.props.options, fixedColumns: target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options;
|
||||
const { showHeader } = this.props.options;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Header</h5>
|
||||
<Switch label="Show" labelClass="width-6" checked={showHeader} onChange={this.onToggleShowHeader} />
|
||||
<Switch label="Fixed" labelClass="width-6" checked={fixedHeader} onChange={this.onToggleFixedHeader} />
|
||||
</div>
|
||||
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Display</h5>
|
||||
<Switch label="Rotate" labelClass="width-8" checked={rotate} onChange={this.onToggleRotate} />
|
||||
<FormField
|
||||
label="Fixed Columns"
|
||||
labelWidth={8}
|
||||
inputWidth={4}
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
onChange={this.onFixedColumnsChange}
|
||||
value={fixedColumns}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -4,4 +4,7 @@ import { TablePanelEditor } from './TablePanelEditor';
|
||||
import { TablePanel } from './TablePanel';
|
||||
import { Options, defaults } from './types';
|
||||
|
||||
export const plugin = new PanelPlugin<Options>(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor);
|
||||
export const plugin = new PanelPlugin<Options>(TablePanel)
|
||||
.setNoPadding()
|
||||
.setDefaults(defaults)
|
||||
.setEditor(TablePanelEditor);
|
||||
|
@ -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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user