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);
|
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 {
|
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
|
||||||
if (_.isNumber(decimalOverride)) {
|
if (_.isNumber(decimalOverride)) {
|
||||||
// It's important that scaledDecimals is null here
|
// It's important that scaledDecimals is null here
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import set from 'lodash/set';
|
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 { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||||
import { FieldMatcher } from '../types/transformations';
|
import { FieldMatcher } from '../types/transformations';
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
import toNumber from 'lodash/toNumber';
|
import toNumber from 'lodash/toNumber';
|
||||||
import { getDisplayProcessor } from './displayProcessor';
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
import { GetFieldDisplayValuesOptions } from './fieldDisplay';
|
|
||||||
|
|
||||||
interface OverrideProps {
|
interface OverrideProps {
|
||||||
match: FieldMatcher;
|
match: FieldMatcher;
|
||||||
@ -17,6 +25,14 @@ interface GlobalMinMax {
|
|||||||
max: number;
|
max: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplyFieldOverrideOptions {
|
||||||
|
data?: DataFrame[];
|
||||||
|
fieldOptions: FieldConfigSource;
|
||||||
|
replaceVariables: InterpolateFunction;
|
||||||
|
theme: GrafanaTheme;
|
||||||
|
autoMinMax?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
|
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
|
||||||
let min = Number.MAX_VALUE;
|
let min = Number.MAX_VALUE;
|
||||||
let max = Number.MIN_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
|
* 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) {
|
if (!options.data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = options.fieldOptions;
|
const source = options.fieldOptions;
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return options.data;
|
return options.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
let range: GlobalMinMax | undefined = undefined;
|
let range: GlobalMinMax | undefined = undefined;
|
||||||
|
|
||||||
// Prepare the Matchers
|
// Prepare the Matchers
|
||||||
@ -59,7 +77,7 @@ export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): Data
|
|||||||
const info = fieldMatchers.get(rule.matcher.id);
|
const info = fieldMatchers.get(rule.matcher.id);
|
||||||
if (info) {
|
if (info) {
|
||||||
override.push({
|
override.push({
|
||||||
match: info.get(rule.matcher),
|
match: info.get(rule.matcher.options),
|
||||||
properties: rule.properties,
|
properties: rule.properties,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -72,7 +90,7 @@ export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): Data
|
|||||||
name = `Series[${index}]`;
|
name = `Series[${index}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = frame.fields.map(field => {
|
const fields: Field[] = frame.fields.map(field => {
|
||||||
// Config is mutable within this scope
|
// Config is mutable within this scope
|
||||||
const config: FieldConfig = { ...field.config } || {};
|
const config: FieldConfig = { ...field.config } || {};
|
||||||
if (field.type === FieldType.number) {
|
if (field.type === FieldType.number) {
|
||||||
@ -116,7 +134,7 @@ export function applyFieldOverrides(options: GetFieldDisplayValuesOptions): Data
|
|||||||
config,
|
config,
|
||||||
|
|
||||||
// Set the display processor
|
// Set the display processor
|
||||||
processor: getDisplayProcessor({
|
display: getDisplayProcessor({
|
||||||
type: field.type,
|
type: field.type,
|
||||||
config: config,
|
config: config,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
|
@ -44,13 +44,23 @@ export interface FieldConfig {
|
|||||||
// Alternative to empty string
|
// Alternative to empty string
|
||||||
noValue?: string;
|
noValue?: string;
|
||||||
|
|
||||||
// Visual options
|
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
||||||
|
custom?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Field<T = any, V = Vector<T>> {
|
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;
|
type: FieldType;
|
||||||
|
/**
|
||||||
|
* Meta info about how field and how to display it
|
||||||
|
*/
|
||||||
config: FieldConfig;
|
config: FieldConfig;
|
||||||
values: V; // The raw field values
|
values: V; // The raw field values
|
||||||
labels?: Labels;
|
labels?: Labels;
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
"react-highlight-words": "0.11.0",
|
"react-highlight-words": "0.11.0",
|
||||||
"react-popper": "1.3.3",
|
"react-popper": "1.3.3",
|
||||||
"react-storybook-addon-props-combinations": "1.1.0",
|
"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-transition-group": "2.6.1",
|
||||||
"react-virtualized": "9.21.0",
|
"react-virtualized": "9.21.0",
|
||||||
"slate": "0.47.8",
|
"slate": "0.47.8",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { number, text } from '@storybook/addon-knobs';
|
import { number, text } from '@storybook/addon-knobs';
|
||||||
import { BarGauge, Props } from './BarGauge';
|
import { BarGauge, Props, BarGaugeDisplayMode } from './BarGauge';
|
||||||
import { VizOrientation } from '@grafana/data';
|
import { VizOrientation } from '@grafana/data';
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
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);
|
BarGaugeStories.addDecorator(withCenteredStory);
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
|
|||||||
minValue: minValue,
|
minValue: minValue,
|
||||||
maxValue: maxValue,
|
maxValue: maxValue,
|
||||||
orientation: VizOrientation.Vertical,
|
orientation: VizOrientation.Vertical,
|
||||||
displayMode: 'basic',
|
displayMode: BarGaugeDisplayMode.Basic,
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ value: -Infinity, color: 'green' },
|
{ value: -Infinity, color: 'green' },
|
||||||
{ value: threshold1Value, color: threshold1Color },
|
{ value: threshold1Value, color: threshold1Color },
|
||||||
@ -61,21 +61,21 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addBarGaugeStory('Gradient Vertical', {
|
addBarGaugeStory('Gradient Vertical', {
|
||||||
displayMode: 'gradient',
|
displayMode: BarGaugeDisplayMode.Gradient,
|
||||||
orientation: VizOrientation.Vertical,
|
orientation: VizOrientation.Vertical,
|
||||||
height: 500,
|
height: 500,
|
||||||
width: 100,
|
width: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
addBarGaugeStory('Gradient Horizontal', {
|
addBarGaugeStory('Gradient Horizontal', {
|
||||||
displayMode: 'gradient',
|
displayMode: BarGaugeDisplayMode.Gradient,
|
||||||
orientation: VizOrientation.Horizontal,
|
orientation: VizOrientation.Horizontal,
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 500,
|
width: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
addBarGaugeStory('LCD Horizontal', {
|
addBarGaugeStory('LCD Horizontal', {
|
||||||
displayMode: 'lcd',
|
displayMode: BarGaugeDisplayMode.Lcd,
|
||||||
orientation: VizOrientation.Vertical,
|
orientation: VizOrientation.Vertical,
|
||||||
height: 500,
|
height: 500,
|
||||||
width: 100,
|
width: 100,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
getBarGradient,
|
getBarGradient,
|
||||||
getTitleStyles,
|
getTitleStyles,
|
||||||
getValuePercent,
|
getValuePercent,
|
||||||
|
BarGaugeDisplayMode,
|
||||||
} from './BarGauge';
|
} from './BarGauge';
|
||||||
import { VizOrientation } from '@grafana/data';
|
import { VizOrientation } from '@grafana/data';
|
||||||
import { getTheme } from '../../themes';
|
import { getTheme } from '../../themes';
|
||||||
@ -20,7 +21,7 @@ function getProps(propOverrides?: Partial<Props>): Props {
|
|||||||
const props: Props = {
|
const props: Props = {
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
displayMode: 'basic',
|
displayMode: BarGaugeDisplayMode.Basic,
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ value: -Infinity, color: 'green' },
|
{ value: -Infinity, color: 'green' },
|
||||||
{ value: 70, color: 'orange' },
|
{ value: 70, color: 'orange' },
|
||||||
|
@ -39,22 +39,30 @@ export interface Props extends Themeable {
|
|||||||
minValue: number;
|
minValue: number;
|
||||||
orientation: VizOrientation;
|
orientation: VizOrientation;
|
||||||
itemSpacing?: number;
|
itemSpacing?: number;
|
||||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
lcdCellWidth?: number;
|
||||||
|
displayMode: BarGaugeDisplayMode;
|
||||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
className?: string;
|
className?: string;
|
||||||
showUnfilled?: boolean;
|
showUnfilled?: boolean;
|
||||||
alignmentFactors?: DisplayValueAlignmentFactors;
|
alignmentFactors?: DisplayValueAlignmentFactors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum BarGaugeDisplayMode {
|
||||||
|
Basic = 'basic',
|
||||||
|
Lcd = 'lcd',
|
||||||
|
Gradient = 'gradient',
|
||||||
|
}
|
||||||
|
|
||||||
export class BarGauge extends PureComponent<Props> {
|
export class BarGauge extends PureComponent<Props> {
|
||||||
static defaultProps: Partial<Props> = {
|
static defaultProps: Partial<Props> = {
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
|
lcdCellWidth: 12,
|
||||||
value: {
|
value: {
|
||||||
text: '100',
|
text: '100',
|
||||||
numeric: 100,
|
numeric: 100,
|
||||||
},
|
},
|
||||||
displayMode: 'lcd',
|
displayMode: BarGaugeDisplayMode.Gradient,
|
||||||
orientation: VizOrientation.Horizontal,
|
orientation: VizOrientation.Horizontal,
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
itemSpacing: 10,
|
itemSpacing: 10,
|
||||||
@ -152,7 +160,7 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderRetroBars(): ReactNode {
|
renderRetroBars(): ReactNode {
|
||||||
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation } = this.props;
|
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
|
||||||
const {
|
const {
|
||||||
valueHeight,
|
valueHeight,
|
||||||
valueWidth,
|
valueWidth,
|
||||||
@ -166,8 +174,7 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
const valueRange = maxValue - minValue;
|
const valueRange = maxValue - minValue;
|
||||||
const maxSize = isVert ? maxBarHeight : maxBarWidth;
|
const maxSize = isVert ? maxBarHeight : maxBarWidth;
|
||||||
const cellSpacing = itemSpacing!;
|
const cellSpacing = itemSpacing!;
|
||||||
const cellWidth = 12;
|
const cellCount = Math.floor(maxSize / lcdCellWidth!);
|
||||||
const cellCount = Math.floor(maxSize / cellWidth);
|
|
||||||
const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount);
|
const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount);
|
||||||
const valueColor = getValueColor(this.props);
|
const valueColor = getValueColor(this.props);
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
groupLabel: css`
|
groupLabel: css`
|
||||||
color: ${groupLabelColor};
|
color: ${groupLabelColor};
|
||||||
font-size: ${theme.typography.size.sm};
|
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};
|
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
||||||
`,
|
`,
|
||||||
icon: css`
|
icon: css`
|
||||||
|
@ -21,7 +21,7 @@ export const sharedInputStyle = (theme: GrafanaTheme, invalid = false) => {
|
|||||||
|
|
||||||
return css`
|
return css`
|
||||||
background-color: ${colors.formInputBg};
|
background-color: ${colors.formInputBg};
|
||||||
line-height: ${theme.typography.lineHeight.lg};
|
line-height: ${theme.typography.lineHeight.md};
|
||||||
font-size: ${theme.typography.size.md};
|
font-size: ${theme.typography.size.md};
|
||||||
color: ${colors.formInputText};
|
color: ${colors.formInputText};
|
||||||
border: 1px solid ${borderColor};
|
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 React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
|
||||||
import { Table } from './Table';
|
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';
|
export default {
|
||||||
import { GrafanaThemeType } from '@grafana/data';
|
title: 'Visualizations/Table',
|
||||||
import { DataFrame, FieldType, ArrayVector, ScopedVars } from '@grafana/data';
|
component: Table,
|
||||||
import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
|
decorators: [withCenteredStory],
|
||||||
import { number, boolean } from '@storybook/addon-knobs';
|
parameters: {
|
||||||
|
docs: {
|
||||||
const replaceVariables = (value: string, scopedVars?: ScopedVars) => {
|
page: mdx,
|
||||||
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 function columnIndexToLeter(column: number) {
|
function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFrame {
|
||||||
const A = 'A'.charCodeAt(0);
|
const data = new MutableDataFrame({
|
||||||
const c1 = Math.floor(column / 26);
|
fields: [
|
||||||
const c2 = column % 26;
|
{ name: 'Time', type: FieldType.time, values: [] }, // The time field
|
||||||
if (c1 > 0) {
|
{
|
||||||
return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2);
|
name: 'Quantity',
|
||||||
}
|
type: FieldType.number,
|
||||||
return String.fromCharCode(A + c2);
|
values: [],
|
||||||
}
|
config: {
|
||||||
|
decimals: 0,
|
||||||
export function makeDummyTable(columnCount: number, rowCount: number): DataFrame {
|
custom: {
|
||||||
return {
|
align: 'center',
|
||||||
fields: Array.from(new Array(columnCount), (x, i) => {
|
},
|
||||||
const colId = columnIndexToLeter(i);
|
},
|
||||||
const values = new ArrayVector<string>();
|
},
|
||||||
for (let i = 0; i < rowCount; i++) {
|
{ name: 'Status', type: FieldType.string, values: [] }, // The time field
|
||||||
values.buffer.push(colId + (i + 1));
|
{
|
||||||
}
|
name: 'Value',
|
||||||
return {
|
type: FieldType.number,
|
||||||
name: colId,
|
values: [],
|
||||||
type: FieldType.string,
|
config: {
|
||||||
config: {},
|
decimals: 2,
|
||||||
values,
|
},
|
||||||
};
|
},
|
||||||
}),
|
{
|
||||||
length: rowCount,
|
name: 'Progress',
|
||||||
};
|
type: FieldType.number,
|
||||||
}
|
values: [],
|
||||||
|
config: {
|
||||||
storiesOf('UI/Table', module)
|
unit: 'percent',
|
||||||
.add('Basic Table', () => {
|
custom: {
|
||||||
// NOTE: This example does not seem to survice rotate &
|
width: 50,
|
||||||
// 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),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 React, { useMemo, CSSProperties } from 'react';
|
||||||
import _ from 'lodash';
|
import { DataFrame } from '@grafana/data';
|
||||||
import React, { Component, ReactElement } from 'react';
|
// @ts-ignore
|
||||||
import {
|
import { useSortBy, useTable, useBlockLayout } from 'react-table';
|
||||||
SortDirectionType,
|
import { FixedSizeList } from 'react-window';
|
||||||
SortIndicator,
|
import { getTableStyles } from './styles';
|
||||||
MultiGrid,
|
import { getColumns, getTableRows } from './utils';
|
||||||
CellMeasurerCache,
|
import { TableColumn } from './types';
|
||||||
CellMeasurer,
|
import { useTheme } from '../../themes';
|
||||||
GridCellProps,
|
|
||||||
Index,
|
|
||||||
} from 'react-virtualized';
|
|
||||||
import { Themeable } from '../../types/theme';
|
|
||||||
|
|
||||||
import {
|
export interface Props {
|
||||||
stringToJsRegex,
|
|
||||||
DataFrame,
|
|
||||||
sortDataFrame,
|
|
||||||
getDataFrameRow,
|
|
||||||
ArrayVector,
|
|
||||||
FieldType,
|
|
||||||
InterpolateFunction,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
import {
|
|
||||||
TableCellBuilder,
|
|
||||||
ColumnStyle,
|
|
||||||
getFieldCellBuilder,
|
|
||||||
TableCellBuilderOptions,
|
|
||||||
simpleCellBuilder,
|
|
||||||
} from './TableCellBuilder';
|
|
||||||
|
|
||||||
export interface Props extends Themeable {
|
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
|
|
||||||
minColumnWidth: number;
|
|
||||||
showHeader: boolean;
|
|
||||||
fixedHeader: boolean;
|
|
||||||
fixedColumns: number;
|
|
||||||
rotate: boolean;
|
|
||||||
styles: ColumnStyle[];
|
|
||||||
|
|
||||||
replaceVariables: InterpolateFunction;
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
isUTC?: boolean;
|
onCellClick?: TableFilterActionCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
type TableFilterActionCallback = (key: string, value: string) => void;
|
||||||
sortBy?: number;
|
|
||||||
sortDirection?: SortDirectionType;
|
|
||||||
data: DataFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColumnRenderInfo {
|
export const Table = ({ data, height, onCellClick, width }: Props) => {
|
||||||
header: string;
|
const theme = useTheme();
|
||||||
width: number;
|
const tableStyles = getTableStyles(theme);
|
||||||
builder: TableCellBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataIndex {
|
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
|
||||||
column: number;
|
{
|
||||||
row: number; // -1 is the header!
|
columns: useMemo(() => getColumns(data, width, theme), [data]),
|
||||||
}
|
data: useMemo(() => getTableRows(data), [data]),
|
||||||
|
tableStyles,
|
||||||
|
},
|
||||||
|
useSortBy,
|
||||||
|
useBlockLayout
|
||||||
|
);
|
||||||
|
|
||||||
export class Table extends Component<Props, State> {
|
const RenderRow = React.useCallback(
|
||||||
renderer: ColumnRenderInfo[];
|
({ index, style }) => {
|
||||||
measurer: CellMeasurerCache;
|
const row = rows[index];
|
||||||
scrollToTop = false;
|
prepareRow(row);
|
||||||
rotateWidth = 100;
|
return (
|
||||||
|
<div {...row.getRowProps({ style })} className={tableStyles.row}>
|
||||||
|
{row.cells.map((cell: RenderCellProps) => renderCell(cell, onCellClick))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[prepareRow, rows]
|
||||||
|
);
|
||||||
|
|
||||||
static defaultProps = {
|
return (
|
||||||
showHeader: true,
|
<div {...getTableProps()} className={tableStyles.table}>
|
||||||
fixedHeader: true,
|
<div>
|
||||||
fixedColumns: 0,
|
{headerGroups.map((headerGroup: any) => (
|
||||||
rotate: false,
|
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()}>
|
||||||
minColumnWidth: 150,
|
{headerGroup.headers.map((column: any) => renderHeaderCell(column, tableStyles.headerCell))}
|
||||||
};
|
</div>
|
||||||
|
))}
|
||||||
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} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<FixedSizeList height={height} itemCount={rows.length} itemSize={tableStyles.rowHeight} width={width}>
|
||||||
};
|
{RenderRow}
|
||||||
|
</FixedSizeList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
getTableCellBuilder = (column: number): TableCellBuilder => {
|
interface RenderCellProps {
|
||||||
const render = this.renderer[column];
|
column: TableColumn;
|
||||||
if (render && render.builder) {
|
value: any;
|
||||||
return render.builder;
|
getCellProps: () => { style: CSSProperties };
|
||||||
}
|
render: (component: string) => React.ReactNode;
|
||||||
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"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
"h6": "14px",
|
||||||
},
|
},
|
||||||
"lineHeight": Object {
|
"lineHeight": Object {
|
||||||
"lg": 1.5,
|
"lg": 2,
|
||||||
"md": 1.3333333333333333,
|
"md": 1.5,
|
||||||
"sm": 1.1,
|
"sm": 1.1,
|
||||||
"xs": 1,
|
"xs": 1,
|
||||||
},
|
},
|
||||||
@ -534,8 +534,8 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
|
|||||||
"h6": "14px",
|
"h6": "14px",
|
||||||
},
|
},
|
||||||
"lineHeight": Object {
|
"lineHeight": Object {
|
||||||
"lg": 1.5,
|
"lg": 2,
|
||||||
"md": 1.3333333333333333,
|
"md": 1.5,
|
||||||
"sm": 1.1,
|
"sm": 1.1,
|
||||||
"xs": 1,
|
"xs": 1,
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
label: type-ahead-item-group-title;
|
label: type-ahead-item-group-title;
|
||||||
color: ${theme.colors.textWeak};
|
color: ${theme.colors.textWeak};
|
||||||
font-size: ${theme.typography.size.sm};
|
font-size: ${theme.typography.size.sm};
|
||||||
line-height: ${theme.typography.lineHeight.lg};
|
line-height: ${theme.typography.lineHeight.md};
|
||||||
padding: ${theme.spacing.sm};
|
padding: ${theme.spacing.sm};
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||||
@import 'RefreshPicker/RefreshPicker';
|
@import 'RefreshPicker/RefreshPicker';
|
||||||
@import 'Select/Select';
|
@import 'Select/Select';
|
||||||
@import 'Table/TableInputCSV';
|
@import 'TableInputCSV/TableInputCSV';
|
||||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||||
@import 'TimePicker/TimeOfDayPicker';
|
@import 'TimePicker/TimeOfDayPicker';
|
||||||
@import 'Tooltip/Tooltip';
|
@import 'Tooltip/Tooltip';
|
||||||
|
@ -46,8 +46,8 @@ export { QueryField } from './QueryField/QueryField';
|
|||||||
// Renderless
|
// Renderless
|
||||||
export { SetInterval } from './SetInterval/SetInterval';
|
export { SetInterval } from './SetInterval/SetInterval';
|
||||||
|
|
||||||
export { NewTable as Table } from './Table/NewTable';
|
export { Table } from './Table/Table';
|
||||||
export { TableInputCSV } from './Table/TableInputCSV';
|
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
|
||||||
|
|
||||||
// Visualizations
|
// Visualizations
|
||||||
export {
|
export {
|
||||||
@ -62,7 +62,7 @@ export { Gauge } from './Gauge/Gauge';
|
|||||||
export { Graph } from './Graph/Graph';
|
export { Graph } from './Graph/Graph';
|
||||||
export { GraphLegend } from './Graph/GraphLegend';
|
export { GraphLegend } from './Graph/GraphLegend';
|
||||||
export { GraphWithLegend } from './Graph/GraphWithLegend';
|
export { GraphWithLegend } from './Graph/GraphWithLegend';
|
||||||
export { BarGauge } from './BarGauge/BarGauge';
|
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
||||||
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
||||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
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-sm: ${theme.typography.size.sm} !default;
|
||||||
$font-size-xs: ${theme.typography.size.xs} !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-regular: ${theme.typography.weight.regular} !default;
|
||||||
$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
|
$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
|
||||||
|
@ -54,8 +54,8 @@ const theme: GrafanaThemeCommons = {
|
|||||||
lineHeight: {
|
lineHeight: {
|
||||||
xs: 1,
|
xs: 1,
|
||||||
sm: 1.1,
|
sm: 1.1,
|
||||||
md: 4 / 3,
|
md: 1.5,
|
||||||
lg: 1.5,
|
lg: 2,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
decoration: 'none',
|
decoration: 'none',
|
||||||
|
@ -130,8 +130,15 @@ describe('ResultProcessor', () => {
|
|||||||
describe('when calling getTableResult', () => {
|
describe('when calling getTableResult', () => {
|
||||||
it('then it should return correct table result', () => {
|
it('then it should return correct table result', () => {
|
||||||
const { resultProcessor } = testContext();
|
const { resultProcessor } = testContext();
|
||||||
const theResult = resultProcessor.getTableResult();
|
let theResult = resultProcessor.getTableResult();
|
||||||
const resultDataFrame = toDataFrame(
|
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({
|
new TableModel({
|
||||||
columns: [
|
columns: [
|
||||||
{ text: 'value', type: 'number' },
|
{ text: 'value', type: 'number' },
|
||||||
@ -146,8 +153,11 @@ describe('ResultProcessor', () => {
|
|||||||
type: 'table',
|
type: 'table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
expect(theResult.fields[0].name).toEqual('value');
|
||||||
expect(theResult).toEqual(resultDataFrame);
|
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 { ExploreItemState, ExploreMode } from 'app/types/explore';
|
||||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
import { sortLogsResult, refreshIntervalToSortOrder } from 'app/core/utils/explore';
|
import { sortLogsResult, refreshIntervalToSortOrder } from 'app/core/utils/explore';
|
||||||
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
||||||
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
|
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
export class ResultProcessor {
|
export class ResultProcessor {
|
||||||
constructor(
|
constructor(
|
||||||
@ -75,7 +84,17 @@ export class ResultProcessor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mergedTable = mergeTablesIntoModel(new TableModel(), ...tables);
|
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 {
|
getLogsResult(): LogsModel | null {
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { SingleStatBaseOptions } from '@grafana/ui';
|
import { SingleStatBaseOptions, BarGaugeDisplayMode } from '@grafana/ui';
|
||||||
import { standardGaugeFieldOptions } from '../gauge/types';
|
import { standardGaugeFieldOptions } from '../gauge/types';
|
||||||
import { VizOrientation, SelectableValue } from '@grafana/data';
|
import { VizOrientation, SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
||||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
displayMode: BarGaugeDisplayMode;
|
||||||
showUnfilled: boolean;
|
showUnfilled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const displayModes: Array<SelectableValue<string>> = [
|
export const displayModes: Array<SelectableValue<string>> = [
|
||||||
{ value: 'gradient', label: 'Gradient' },
|
{ value: BarGaugeDisplayMode.Gradient, label: 'Gradient' },
|
||||||
{ value: 'lcd', label: 'Retro LCD' },
|
{ value: BarGaugeDisplayMode.Lcd, label: 'Retro LCD' },
|
||||||
{ value: 'basic', label: 'Basic' },
|
{ value: BarGaugeDisplayMode.Basic, label: 'Basic' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const defaults: BarGaugeOptions = {
|
export const defaults: BarGaugeOptions = {
|
||||||
displayMode: 'lcd',
|
displayMode: BarGaugeDisplayMode.Lcd,
|
||||||
orientation: VizOrientation.Horizontal,
|
orientation: VizOrientation.Horizontal,
|
||||||
fieldOptions: standardGaugeFieldOptions,
|
fieldOptions: standardGaugeFieldOptions,
|
||||||
showUnfilled: true,
|
showUnfilled: true,
|
||||||
|
@ -11,9 +11,8 @@ import {
|
|||||||
stringToJsRegex,
|
stringToJsRegex,
|
||||||
unEscapeStringFromRegex,
|
unEscapeStringFromRegex,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { ColumnRender, TableRenderModel } from './types';
|
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
|
||||||
|
|
||||||
export class TableRenderer {
|
export class TableRenderer {
|
||||||
formatters: any[];
|
formatters: any[];
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { Column } from '@grafana/data';
|
import { Column } from '@grafana/data';
|
||||||
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
|
|
||||||
|
|
||||||
export interface TableTransform {
|
export interface TableTransform {
|
||||||
description: string;
|
description: string;
|
||||||
@ -18,3 +17,26 @@ export interface TableRenderModel {
|
|||||||
columns: ColumnRender[];
|
columns: ColumnRender[];
|
||||||
rows: any[][];
|
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
|
// Types
|
||||||
import { Table } from '@grafana/ui';
|
import { Table } from '@grafana/ui';
|
||||||
import { PanelProps } from '@grafana/data';
|
import { PanelProps, applyFieldOverrides } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
interface Props extends PanelProps<Options> {}
|
interface Props extends PanelProps<Options> {}
|
||||||
|
|
||||||
// So that the table does not go all the way to the edge of the panel chrome
|
const paddingBottom = 16;
|
||||||
const paddingBottom = 35;
|
|
||||||
|
|
||||||
export class TablePanel extends Component<Props> {
|
export class TablePanel extends Component<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@ -17,12 +17,19 @@ export class TablePanel extends Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, height, width } = this.props;
|
const { data, height, width, replaceVariables, options } = this.props;
|
||||||
|
|
||||||
if (data.series.length < 1) {
|
if (data.series.length < 1) {
|
||||||
return <div>No Table Data...</div>;
|
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
|
// Types
|
||||||
import { PanelEditorProps } from '@grafana/data';
|
import { PanelEditorProps } from '@grafana/data';
|
||||||
import { Switch, FormField } from '@grafana/ui';
|
import { Switch } from '@grafana/ui';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
|
|
||||||
export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
|
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 });
|
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() {
|
render() {
|
||||||
const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options;
|
const { showHeader } = this.props.options;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="section gf-form-group">
|
<div className="section gf-form-group">
|
||||||
<h5 className="section-heading">Header</h5>
|
<h5 className="section-heading">Header</h5>
|
||||||
<Switch label="Show" labelClass="width-6" checked={showHeader} onChange={this.onToggleShowHeader} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -4,4 +4,7 @@ import { TablePanelEditor } from './TablePanelEditor';
|
|||||||
import { TablePanel } from './TablePanel';
|
import { TablePanel } from './TablePanel';
|
||||||
import { Options, defaults } from './types';
|
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 {
|
export interface Options {
|
||||||
|
fieldOptions: FieldConfigSource;
|
||||||
showHeader: boolean;
|
showHeader: boolean;
|
||||||
fixedHeader: boolean;
|
|
||||||
fixedColumns: number;
|
|
||||||
rotate: boolean;
|
|
||||||
|
|
||||||
styles: ColumnStyle[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaults: Options = {
|
export const defaults: Options = {
|
||||||
|
fieldOptions: {
|
||||||
|
defaults: {},
|
||||||
|
overrides: [],
|
||||||
|
},
|
||||||
showHeader: true,
|
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"
|
prismjs "^1.8.4"
|
||||||
refractor "^2.4.1"
|
refractor "^2.4.1"
|
||||||
|
|
||||||
react-table@7.0.0-rc.4:
|
react-table@latest:
|
||||||
version "7.0.0-rc.4"
|
version "7.0.0-rc.15"
|
||||||
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.4.tgz#88bc61747821f3c3bbbfc7e1a4a088cbe94ed9ee"
|
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a"
|
||||||
integrity sha512-NOYmNmAIvQ9sSZd5xMNSthqiZ/o5h8h28MhFQFSxCu5u3v9J8PNh7x9wYMnk737MTjoKCZWIZT/dMFCPItXzEg==
|
integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw==
|
||||||
|
|
||||||
react-test-renderer@16.9.0:
|
react-test-renderer@16.9.0:
|
||||||
version "16.9.0"
|
version "16.9.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user