mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge with master
This commit is contained in:
@@ -45,7 +45,7 @@ $arrowSize: 15px;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
top: 0;
|
||||
left: calc(100% -$arrowSize);
|
||||
left: calc(100%-#{$arrowSize});
|
||||
}
|
||||
|
||||
&[data-placement^='right'] {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormField, Props } from './FormField';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
label: 'Test',
|
||||
labelWidth: 11,
|
||||
@@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => {
|
||||
return shallow(<FormField {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
describe('FormField', () => {
|
||||
it('should render component with default inputEl', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render component with custom inputEl', () => {
|
||||
const wrapper = setup({
|
||||
inputEl: (
|
||||
<>
|
||||
<span>Input</span>
|
||||
<button>Ok</button>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
labelWidth?: number;
|
||||
inputWidth?: number;
|
||||
inputEl?: React.ReactNode;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@@ -12,14 +13,18 @@ const defaultProps = {
|
||||
inputWidth: 12,
|
||||
};
|
||||
|
||||
const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
|
||||
/**
|
||||
* Default form field including label used in Grafana UI. Default input element is simple <input />. You can also pass
|
||||
* custom inputEl if required in which case inputWidth and inputProps are ignored.
|
||||
*/
|
||||
export const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => {
|
||||
return (
|
||||
<div className="form-field">
|
||||
<FormLabel width={labelWidth}>{label}</FormLabel>
|
||||
<input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
|
||||
{inputEl || <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FormField.displayName = 'FormField';
|
||||
FormField.defaultProps = defaultProps;
|
||||
export { FormField };
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
exports[`FormField should render component with custom inputEl 1`] = `
|
||||
<div
|
||||
className="form-field"
|
||||
>
|
||||
<Component
|
||||
width={11}
|
||||
>
|
||||
Test
|
||||
</Component>
|
||||
<span>
|
||||
Input
|
||||
</span>
|
||||
<button>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FormField should render component with default inputEl 1`] = `
|
||||
<div
|
||||
className="form-field"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
import { SecretFormField } from './SecretFormField';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
|
||||
const SecretFormFieldStories = storiesOf('UI/SecretFormField/SecretFormField', module);
|
||||
|
||||
SecretFormFieldStories.addDecorator(withCenteredStory);
|
||||
const getSecretFormFieldKnobs = () => {
|
||||
return {
|
||||
isConfigured: boolean('Set configured state', false),
|
||||
};
|
||||
};
|
||||
|
||||
SecretFormFieldStories.add('default', () => {
|
||||
const knobs = getSecretFormFieldKnobs();
|
||||
return (
|
||||
<UseState initialState="Input value">
|
||||
{(value, setValue) => (
|
||||
<SecretFormField
|
||||
label={'Secret field'}
|
||||
labelWidth={10}
|
||||
value={value}
|
||||
isConfigured={knobs.isConfigured}
|
||||
onChange={e => setValue(e.currentTarget.value)}
|
||||
onReset={() => {
|
||||
action('Value was reset')('');
|
||||
setValue('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { omit } from 'lodash';
|
||||
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
|
||||
import { FormField } from '..';
|
||||
|
||||
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
// Function to use when reset is clicked. Means you have to reset the input value yourself as this is uncontrolled
|
||||
// component (or do something else if required).
|
||||
onReset: () => void;
|
||||
isConfigured: boolean;
|
||||
|
||||
label?: string;
|
||||
labelWidth?: number;
|
||||
inputWidth?: number;
|
||||
// Placeholder of the input field when in non configured state.
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
inputWidth: 12,
|
||||
placeholder: 'Password',
|
||||
label: 'Password',
|
||||
};
|
||||
|
||||
/**
|
||||
* Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
|
||||
* a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
|
||||
* form field. This is used for passwords or anything that is encrypted on the server and is later returned encrypted
|
||||
* to the user (like datasource passwords).
|
||||
*/
|
||||
export const SecretFormField: FunctionComponent<Props> = ({
|
||||
label,
|
||||
labelWidth,
|
||||
inputWidth,
|
||||
onReset,
|
||||
isConfigured,
|
||||
placeholder,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
return (
|
||||
<FormField
|
||||
label={label!}
|
||||
labelWidth={labelWidth}
|
||||
inputEl={
|
||||
isConfigured ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className={`gf-form-input width-${inputWidth! - 2}`}
|
||||
disabled={true}
|
||||
value="configured"
|
||||
{...omit(inputProps, 'value')}
|
||||
/>
|
||||
<button className="btn btn-secondary gf-form-btn" onClick={onReset}>
|
||||
reset
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type="password"
|
||||
className={`gf-form-input width-${inputWidth}`}
|
||||
placeholder={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SecretFormField.defaultProps = defaultProps;
|
||||
SecretFormField.displayName = 'SecretFormField';
|
||||
@@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
|
||||
const suffix = (rowId + 1).toString();
|
||||
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
|
||||
}),
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CellMeasurerCache,
|
||||
CellMeasurer,
|
||||
GridCellProps,
|
||||
Index,
|
||||
} from 'react-virtualized';
|
||||
import { Themeable } from '../../types/theme';
|
||||
|
||||
@@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index';
|
||||
export interface Props extends Themeable {
|
||||
data: TableData;
|
||||
|
||||
minColumnWidth: number;
|
||||
showHeader: boolean;
|
||||
fixedHeader: boolean;
|
||||
fixedColumns: number;
|
||||
@@ -46,6 +48,7 @@ interface State {
|
||||
|
||||
interface ColumnRenderInfo {
|
||||
header: string;
|
||||
width: number;
|
||||
builder: TableCellBuilder;
|
||||
}
|
||||
|
||||
@@ -64,6 +67,7 @@ export class Table extends Component<Props, State> {
|
||||
fixedHeader: true,
|
||||
fixedColumns: 0,
|
||||
rotate: false,
|
||||
minColumnWidth: 150,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -76,7 +80,7 @@ export class Table extends Component<Props, State> {
|
||||
this.renderer = this.initColumns(props);
|
||||
this.measurer = new CellMeasurerCache({
|
||||
defaultHeight: 30,
|
||||
defaultWidth: 150,
|
||||
fixedWidth: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +114,8 @@ export class Table extends Component<Props, State> {
|
||||
|
||||
/** Given the configuration, setup how each column gets rendered */
|
||||
initColumns(props: Props): ColumnRenderInfo[] {
|
||||
const { styles, data } = props;
|
||||
const { styles, data, width, minColumnWidth } = props;
|
||||
const columnWidth = Math.max(width / data.columns.length, minColumnWidth);
|
||||
|
||||
return data.columns.map((col, index) => {
|
||||
let title = col.text;
|
||||
@@ -131,6 +136,7 @@ export class Table extends Component<Props, State> {
|
||||
|
||||
return {
|
||||
header: title,
|
||||
width: columnWidth,
|
||||
builder: getCellBuilder(col, style, this.props),
|
||||
};
|
||||
});
|
||||
@@ -228,6 +234,10 @@ export class Table extends Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
getColumnWidth = (col: Index): number => {
|
||||
return this.renderer[col.index].width;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
|
||||
const { data } = this.state;
|
||||
@@ -269,7 +279,7 @@ export class Table extends Component<Props, State> {
|
||||
rowCount={rowCount}
|
||||
overscanColumnCount={8}
|
||||
overscanRowCount={8}
|
||||
columnWidth={this.measurer.columnWidth}
|
||||
columnWidth={this.getColumnWidth}
|
||||
deferredMeasurementCache={this.measurer}
|
||||
cellRenderer={this.cellRenderer}
|
||||
rowHeight={this.measurer.rowHeight}
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`Render should render with base threshold 1`] = `
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||
// Forms
|
||||
export { FormLabel } from './FormLabel/FormLabel';
|
||||
export { FormField } from './FormField/FormField';
|
||||
export { SecretFormField } from './SecretFormFied/SecretFormField';
|
||||
|
||||
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
|
||||
@@ -51,27 +51,13 @@ export enum NullValueMode {
|
||||
export type TimeSeriesVMs = TimeSeriesVM[];
|
||||
|
||||
export interface Column {
|
||||
text: string;
|
||||
title?: string;
|
||||
type?: string;
|
||||
sort?: boolean;
|
||||
desc?: boolean;
|
||||
filterable?: boolean;
|
||||
text: string; // The column name
|
||||
type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove?
|
||||
filterable?: boolean; // currently only set by elasticsearch, and used in the table panel
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
columns: Column[];
|
||||
rows: any[];
|
||||
type: string;
|
||||
columnMap: any;
|
||||
}
|
||||
|
||||
export type SingleStatValue = number | string | null;
|
||||
|
||||
/*
|
||||
* So we can add meta info like tags & series name
|
||||
*/
|
||||
export interface SingleStatValueInfo {
|
||||
value: SingleStatValue;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ComponentClass } from 'react';
|
||||
import { TimeSeries, LoadingState, TableData } from './data';
|
||||
import { LoadingState, TableData } from './data';
|
||||
import { TimeRange } from './time';
|
||||
import { ScopedVars } from './datasource';
|
||||
|
||||
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
panelData: PanelData;
|
||||
data?: TableData[];
|
||||
timeRange: TimeRange;
|
||||
loading: LoadingState;
|
||||
options: T;
|
||||
@@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
|
||||
replaceVariables: InterpolateFunction;
|
||||
}
|
||||
|
||||
export interface PanelData {
|
||||
timeSeries?: TimeSeries[];
|
||||
tableData?: TableData;
|
||||
}
|
||||
|
||||
export interface PanelEditorProps<T = any> {
|
||||
options: T;
|
||||
onOptionsChange: (options: T) => void;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
|
||||
Object {
|
||||
"columnMap": Object {},
|
||||
"columns": Array [
|
||||
Object {
|
||||
"text": "Column 1",
|
||||
@@ -31,13 +30,11 @@ Object {
|
||||
null,
|
||||
],
|
||||
],
|
||||
"type": "table",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`processTableData basic processing should read header and two rows 1`] = `
|
||||
Object {
|
||||
"columnMap": Object {},
|
||||
"columns": Array [
|
||||
Object {
|
||||
"text": "a",
|
||||
@@ -61,6 +58,5 @@ Object {
|
||||
6,
|
||||
],
|
||||
],
|
||||
"type": "table",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './processTimeSeries';
|
||||
export * from './singlestat';
|
||||
export * from './processTableData';
|
||||
export * from './valueFormats/valueFormats';
|
||||
export * from './colors';
|
||||
export * from './namedColorsPalette';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseCSV } from './processTableData';
|
||||
import { parseCSV, toTableData } from './processTableData';
|
||||
|
||||
describe('processTableData', () => {
|
||||
describe('basic processing', () => {
|
||||
@@ -18,3 +18,41 @@ describe('processTableData', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTableData', () => {
|
||||
it('converts timeseries to table skipping nulls', () => {
|
||||
const input1 = {
|
||||
target: 'Field Name',
|
||||
datapoints: [[100, 1], [200, 2]],
|
||||
};
|
||||
const input2 = {
|
||||
// without target
|
||||
target: '',
|
||||
datapoints: [[100, 1], [200, 2]],
|
||||
};
|
||||
const data = toTableData([null, input1, input2, null, null]);
|
||||
expect(data.length).toBe(2);
|
||||
expect(data[0].columns[0].text).toBe(input1.target);
|
||||
expect(data[0].rows).toBe(input1.datapoints);
|
||||
|
||||
// Default name
|
||||
expect(data[1].columns[0].text).toEqual('Value');
|
||||
});
|
||||
|
||||
it('keeps tableData unchanged', () => {
|
||||
const input = {
|
||||
columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
|
||||
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
|
||||
};
|
||||
const data = toTableData([null, input, null, null]);
|
||||
expect(data.length).toBe(1);
|
||||
expect(data[0]).toBe(input);
|
||||
});
|
||||
|
||||
it('supports null values OK', () => {
|
||||
expect(toTableData([null, null, null, null])).toEqual([]);
|
||||
expect(toTableData(undefined)).toEqual([]);
|
||||
expect(toTableData((null as unknown) as any[])).toEqual([]);
|
||||
expect(toTableData([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
|
||||
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
||||
|
||||
// Types
|
||||
import { TableData, Column } from '../types';
|
||||
import { TableData, Column, TimeSeries } from '../types';
|
||||
|
||||
// Subset of all parse options
|
||||
export interface TableParseOptions {
|
||||
@@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
|
||||
return {
|
||||
columns,
|
||||
rows: fixedRows,
|
||||
type: table.type,
|
||||
columnMap: table.columnMap,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
|
||||
return {
|
||||
columns: [],
|
||||
rows: [],
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
|
||||
return matchRowSizes({
|
||||
columns: makeColumns(header),
|
||||
rows: results.data,
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
});
|
||||
}
|
||||
|
||||
function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
text: timeSeries.target || 'Value',
|
||||
unit: timeSeries.unit,
|
||||
},
|
||||
{
|
||||
text: 'Time',
|
||||
type: 'time',
|
||||
unit: 'dateTimeAsIso',
|
||||
},
|
||||
],
|
||||
rows: timeSeries.datapoints,
|
||||
};
|
||||
}
|
||||
|
||||
export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
|
||||
|
||||
export const toTableData = (results?: any[]): TableData[] => {
|
||||
if (!results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results
|
||||
.filter(d => !!d)
|
||||
.map(data => {
|
||||
if (data.hasOwnProperty('columns')) {
|
||||
return data as TableData;
|
||||
}
|
||||
if (data.hasOwnProperty('datapoints')) {
|
||||
return convertTimeSeriesToTableData(data);
|
||||
}
|
||||
// TODO, try to convert JSON to table?
|
||||
console.warn('Can not convert', data);
|
||||
throw new Error('Unsupported data format');
|
||||
});
|
||||
};
|
||||
|
||||
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
|
||||
if (isNumber(sortIndex)) {
|
||||
const copy = {
|
||||
|
||||
@@ -4,24 +4,43 @@ import isNumber from 'lodash/isNumber';
|
||||
import { colors } from './colors';
|
||||
|
||||
// Types
|
||||
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
||||
import { getFlotPairs } from './flotPairs';
|
||||
import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
|
||||
|
||||
interface Options {
|
||||
timeSeries: TimeSeries[];
|
||||
data: TableData[];
|
||||
xColumn?: number; // Time (or null to guess)
|
||||
yColumn?: number; // Value (or null to guess)
|
||||
nullValueMode: NullValueMode;
|
||||
}
|
||||
|
||||
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
|
||||
const vmSeries = timeSeries.map((item, index) => {
|
||||
// NOTE: this should move to processTableData.ts
|
||||
// I left it as is so the merge changes are more clear.
|
||||
export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs {
|
||||
const vmSeries = data.map((item, index) => {
|
||||
if (!isNumber(xColumn)) {
|
||||
xColumn = 1; // Default timeseries colum. TODO, find first time field!
|
||||
}
|
||||
if (!isNumber(yColumn)) {
|
||||
yColumn = 0; // TODO, find first non-time field
|
||||
}
|
||||
|
||||
// TODO? either % or throw error?
|
||||
if (xColumn >= item.columns.length) {
|
||||
throw new Error('invalid colum: ' + xColumn);
|
||||
}
|
||||
if (yColumn >= item.columns.length) {
|
||||
throw new Error('invalid colum: ' + yColumn);
|
||||
}
|
||||
|
||||
const colorIndex = index % colors.length;
|
||||
const label = item.target;
|
||||
const label = item.columns[yColumn].text;
|
||||
|
||||
// Use external calculator just to make sure it works :)
|
||||
const result = getFlotPairs({
|
||||
rows: item.datapoints,
|
||||
xIndex: 1,
|
||||
yIndex: 0,
|
||||
rows: item.rows,
|
||||
xIndex: xColumn,
|
||||
yIndex: yColumn,
|
||||
nullValueMode,
|
||||
});
|
||||
|
||||
@@ -50,9 +69,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
|
||||
let previousValue = 0;
|
||||
let previousDeltaUp = true;
|
||||
|
||||
for (let i = 0; i < item.datapoints.length; i++) {
|
||||
currentValue = item.datapoints[i][0];
|
||||
currentTime = item.datapoints[i][1];
|
||||
for (let i = 0; i < item.rows.length; i++) {
|
||||
currentValue = item.rows[i][yColumn];
|
||||
currentTime = item.rows[i][xColumn];
|
||||
|
||||
if (typeof currentTime !== 'number') {
|
||||
continue;
|
||||
@@ -103,7 +122,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
|
||||
if (previousValue > currentValue) {
|
||||
// counter reset
|
||||
previousDeltaUp = false;
|
||||
if (i === item.datapoints.length - 1) {
|
||||
if (i === item.rows.length - 1) {
|
||||
// reset on last
|
||||
delta += currentValue;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
|
||||
import { processTimeSeries } from './processTimeSeries';
|
||||
|
||||
export interface SingleStatProcessingOptions {
|
||||
panelData: PanelData;
|
||||
stat: string;
|
||||
}
|
||||
|
||||
//
|
||||
// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
|
||||
//
|
||||
export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
|
||||
const { panelData, stat } = options;
|
||||
|
||||
if (panelData.timeSeries) {
|
||||
const timeSeries = processTimeSeries({
|
||||
timeSeries: panelData.timeSeries,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
return timeSeries.map((series, index) => {
|
||||
const value = stat !== 'name' ? series.stats[stat] : series.label;
|
||||
|
||||
return {
|
||||
value: value,
|
||||
};
|
||||
});
|
||||
} else if (panelData.tableData) {
|
||||
throw { message: 'Panel data not supported' };
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
interface StateHolderProps<T> {
|
||||
initialState: T;
|
||||
children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element;
|
||||
children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode;
|
||||
}
|
||||
|
||||
export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {
|
||||
|
||||
Reference in New Issue
Block a user