mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: Introduce sparkline cell type (#63182)
This commit is contained in:
@@ -2,8 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { capitalize, uniqueId } from 'lodash';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData } from '@grafana/data';
|
||||
import { isTimeSeries } from '@grafana/data/src/dataframe/utils';
|
||||
import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData, isTimeSeriesFrames } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { AutoSizeInput, clearButtonStyles, Icon, IconButton, Select, useStyles2 } from '@grafana/ui';
|
||||
import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions';
|
||||
@@ -138,7 +137,7 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
|
||||
|
||||
// sometimes we receive results where every value is just "null" when noData occurs
|
||||
const emptyResults = isEmptySeries(series);
|
||||
const isTimeSeriesResults = !emptyResults && isTimeSeries(series);
|
||||
const isTimeSeriesResults = !emptyResults && isTimeSeriesFrames(series);
|
||||
|
||||
return (
|
||||
<div className={styles.expression.results}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ValidateResult } from 'react-hook-form';
|
||||
|
||||
import { DataFrame, ThresholdsConfig, ThresholdsMode } from '@grafana/data';
|
||||
import { isTimeSeries } from '@grafana/data/src/dataframe/utils';
|
||||
import { DataFrame, ThresholdsConfig, ThresholdsMode, isTimeSeriesFrames } from '@grafana/data';
|
||||
import { GraphTresholdsStyleMode } from '@grafana/schema';
|
||||
import { config } from 'app/core/config';
|
||||
import { EvalFunction } from 'app/features/alerting/state/alertDef';
|
||||
@@ -98,7 +97,7 @@ export function errorFromSeries(series: DataFrame[]): Error | undefined {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTimeSeriesResults = isTimeSeries(series);
|
||||
const isTimeSeriesResults = isTimeSeriesFrames(series);
|
||||
|
||||
let error;
|
||||
if (isTimeSeriesResults) {
|
||||
|
||||
@@ -7,9 +7,10 @@ import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Field, GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { TableCellHeight } from '@grafana/schema';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
|
||||
import { getTableStyles } from '@grafana/ui/src/components/Table/styles';
|
||||
import { useTableStyles } from '@grafana/ui/src/components/Table/styles';
|
||||
|
||||
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
|
||||
import { QueryResponse } from '../../service';
|
||||
@@ -51,7 +52,7 @@ export const SearchResultsTable = React.memo(
|
||||
}: SearchResultsProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const columnStyles = useStyles2(getColumnStyles);
|
||||
const tableStyles = useStyles2(getTableStyles);
|
||||
const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Md);
|
||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
const [listEl, setListEl] = useState<FixedSizeList | null>(null);
|
||||
const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TransformerRegistryItem } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { filterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor';
|
||||
import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor';
|
||||
@@ -27,6 +28,7 @@ import { partitionByValuesTransformRegistryItem } from './partitionByValues/Part
|
||||
import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor';
|
||||
import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor';
|
||||
import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor';
|
||||
import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor';
|
||||
|
||||
export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> => {
|
||||
return [
|
||||
@@ -57,5 +59,6 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
|
||||
limitTransformRegistryItem,
|
||||
joinByLabelsTransformRegistryItem,
|
||||
partitionByValuesTransformRegistryItem,
|
||||
...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginState, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
||||
|
||||
import { timeSeriesTableTransformer, TimeSeriesTableTransformerOptions } from './timeSeriesTableTransformer';
|
||||
|
||||
export interface Props extends TransformerUIProps<{}> {}
|
||||
|
||||
export function TimeSeriesTableTransformEditor({ input, options, onChange }: Props) {
|
||||
if (input.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export const timeSeriesTableTransformRegistryItem: TransformerRegistryItem<TimeSeriesTableTransformerOptions> = {
|
||||
id: timeSeriesTableTransformer.id,
|
||||
editor: TimeSeriesTableTransformEditor,
|
||||
transformation: timeSeriesTableTransformer,
|
||||
name: timeSeriesTableTransformer.name,
|
||||
description: timeSeriesTableTransformer.description,
|
||||
state: PluginState.beta,
|
||||
help: ``,
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import { toDataFrame, FieldType, Labels, DataFrame, Field } from '@grafana/data';
|
||||
|
||||
import { timeSeriesToTableTransform } from './timeSeriesTableTransformer';
|
||||
|
||||
describe('timeSeriesTableTransformer', () => {
|
||||
it('Will transform a single query', () => {
|
||||
const series = [
|
||||
getTimeSeries('A', { instance: 'A', pod: 'B' }),
|
||||
getTimeSeries('A', { instance: 'A', pod: 'C' }),
|
||||
getTimeSeries('A', { instance: 'A', pod: 'D' }),
|
||||
];
|
||||
|
||||
const results = timeSeriesToTableTransform({}, series);
|
||||
expect(results).toHaveLength(1);
|
||||
const result = results[0];
|
||||
expect(result.refId).toBe('A');
|
||||
expect(result.fields).toHaveLength(3);
|
||||
expect(result.fields[0].values.toArray()).toEqual(['A', 'A', 'A']);
|
||||
expect(result.fields[1].values.toArray()).toEqual(['B', 'C', 'D']);
|
||||
assertDataFrameField(result.fields[2], series);
|
||||
});
|
||||
|
||||
it('Will pass through non time series frames', () => {
|
||||
const series = [
|
||||
getTable('B', ['foo', 'bar']),
|
||||
getTimeSeries('A', { instance: 'A', pod: 'B' }),
|
||||
getTimeSeries('A', { instance: 'A', pod: 'C' }),
|
||||
getTable('C', ['bar', 'baz', 'bad']),
|
||||
];
|
||||
|
||||
const results = timeSeriesToTableTransform({}, series);
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results[0]).toEqual(series[0]);
|
||||
expect(results[1].refId).toBe('A');
|
||||
expect(results[1].fields).toHaveLength(3);
|
||||
expect(results[1].fields[0].values.toArray()).toEqual(['A', 'A']);
|
||||
expect(results[1].fields[1].values.toArray()).toEqual(['B', 'C']);
|
||||
expect(results[2]).toEqual(series[3]);
|
||||
});
|
||||
|
||||
it('Will group by refId', () => {
|
||||
const series = [
|
||||
getTimeSeries('A', { instance: 'A', pod: 'B' }),
|
||||
getTimeSeries('A', { instance: 'A', pod: 'C' }),
|
||||
getTimeSeries('A', { instance: 'A', pod: 'D' }),
|
||||
getTimeSeries('B', { instance: 'B', pod: 'F', cluster: 'A' }),
|
||||
getTimeSeries('B', { instance: 'B', pod: 'G', cluster: 'B' }),
|
||||
];
|
||||
|
||||
const results = timeSeriesToTableTransform({}, series);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].refId).toBe('A');
|
||||
expect(results[0].fields).toHaveLength(3);
|
||||
expect(results[0].fields[0].values.toArray()).toEqual(['A', 'A', 'A']);
|
||||
expect(results[0].fields[1].values.toArray()).toEqual(['B', 'C', 'D']);
|
||||
assertDataFrameField(results[0].fields[2], series.slice(0, 3));
|
||||
expect(results[1].refId).toBe('B');
|
||||
expect(results[1].fields).toHaveLength(4);
|
||||
expect(results[1].fields[0].values.toArray()).toEqual(['B', 'B']);
|
||||
expect(results[1].fields[1].values.toArray()).toEqual(['F', 'G']);
|
||||
expect(results[1].fields[2].values.toArray()).toEqual(['A', 'B']);
|
||||
assertDataFrameField(results[1].fields[3], series.slice(3, 5));
|
||||
});
|
||||
});
|
||||
|
||||
function assertFieldsEqual(field1: Field, field2: Field) {
|
||||
expect(field1.type).toEqual(field2.type);
|
||||
expect(field1.name).toEqual(field2.name);
|
||||
expect(field1.values.toArray()).toEqual(field2.values.toArray());
|
||||
expect(field1.labels ?? {}).toEqual(field2.labels ?? {});
|
||||
}
|
||||
|
||||
function assertDataFrameField(field: Field, matchesFrames: DataFrame[]) {
|
||||
const frames: DataFrame[] = field.values.toArray();
|
||||
expect(frames).toHaveLength(matchesFrames.length);
|
||||
frames.forEach((frame, idx) => {
|
||||
const matchingFrame = matchesFrames[idx];
|
||||
expect(frame.fields).toHaveLength(matchingFrame.fields.length);
|
||||
frame.fields.forEach((field, fidx) => assertFieldsEqual(field, matchingFrame.fields[fidx]));
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeSeries(refId: string, labels: Labels) {
|
||||
return toDataFrame({
|
||||
refId,
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [10] },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [10],
|
||||
labels,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function getTable(refId: string, fields: string[]) {
|
||||
return toDataFrame({
|
||||
refId,
|
||||
fields: fields.map((f) => ({ name: f, type: FieldType.string, values: ['value'] })),
|
||||
labels: {},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
DataTransformerID,
|
||||
DataTransformerInfo,
|
||||
Field,
|
||||
FieldType,
|
||||
MutableDataFrame,
|
||||
isTimeSeriesFrame,
|
||||
} from '@grafana/data';
|
||||
|
||||
export interface TimeSeriesTableTransformerOptions {}
|
||||
|
||||
export const timeSeriesTableTransformer: DataTransformerInfo<TimeSeriesTableTransformerOptions> = {
|
||||
id: DataTransformerID.timeSeriesTable,
|
||||
name: 'Time series to table transform',
|
||||
description: 'Time series to table rows',
|
||||
defaultOptions: {},
|
||||
|
||||
operator: (options) => (source) =>
|
||||
source.pipe(
|
||||
map((data) => {
|
||||
return timeSeriesToTableTransform(options, data);
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts time series frames to table frames for use with sparkline chart type.
|
||||
*
|
||||
* @remarks
|
||||
* For each refId (queryName) convert all time series frames into a single table frame, adding each series
|
||||
* as values of a "Trend" frame field. This allows "Trend" to be rendered as area chart type.
|
||||
* Any non time series frames are returned as is.
|
||||
*
|
||||
* @param options - Transform options, currently not used
|
||||
* @param data - Array of data frames to transform
|
||||
* @returns Array of transformed data frames
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export function timeSeriesToTableTransform(options: TimeSeriesTableTransformerOptions, data: DataFrame[]): DataFrame[] {
|
||||
// initialize fields from labels for each refId
|
||||
const refId2LabelFields = getLabelFields(data);
|
||||
|
||||
const refId2frameField: Record<string, Field<DataFrame, ArrayVector>> = {};
|
||||
|
||||
const result: DataFrame[] = [];
|
||||
|
||||
for (const frame of data) {
|
||||
if (!isTimeSeriesFrame(frame)) {
|
||||
result.push(frame);
|
||||
continue;
|
||||
}
|
||||
|
||||
const refId = frame.refId ?? '';
|
||||
|
||||
const labelFields = refId2LabelFields[refId] ?? {};
|
||||
// initialize a new frame for this refId with fields per label and a Trend frame field, if it doesn't exist yet
|
||||
let frameField = refId2frameField[refId];
|
||||
if (!frameField) {
|
||||
frameField = {
|
||||
name: 'Trend' + (refId && Object.keys(refId2LabelFields).length > 1 ? ` #${refId}` : ''),
|
||||
type: FieldType.frame,
|
||||
config: {},
|
||||
values: new ArrayVector(),
|
||||
};
|
||||
refId2frameField[refId] = frameField;
|
||||
const table = new MutableDataFrame();
|
||||
for (const label of Object.values(labelFields)) {
|
||||
table.addField(label);
|
||||
}
|
||||
table.addField(frameField);
|
||||
table.refId = refId;
|
||||
result.push(table);
|
||||
}
|
||||
|
||||
// add values to each label based field of this frame
|
||||
const labels = frame.fields[1].labels;
|
||||
for (const labelKey of Object.keys(labelFields)) {
|
||||
const labelValue = labels?.[labelKey] ?? null;
|
||||
labelFields[labelKey].values.add(labelValue);
|
||||
}
|
||||
|
||||
frameField.values.add(frame);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// For each refId, initialize a field for each label name
|
||||
function getLabelFields(frames: DataFrame[]): Record<string, Record<string, Field<string, ArrayVector>>> {
|
||||
// refId -> label name -> field
|
||||
const labelFields: Record<string, Record<string, Field<string, ArrayVector>>> = {};
|
||||
|
||||
for (const frame of frames) {
|
||||
if (!isTimeSeriesFrame(frame)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const refId = frame.refId ?? '';
|
||||
|
||||
if (!labelFields[refId]) {
|
||||
labelFields[refId] = {};
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
if (!field.labels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const labelName of Object.keys(field.labels)) {
|
||||
if (!labelFields[refId][labelName]) {
|
||||
labelFields[refId][labelName] = {
|
||||
name: labelName,
|
||||
type: FieldType.string,
|
||||
config: {},
|
||||
values: new ArrayVector(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labelFields;
|
||||
}
|
||||
@@ -2,11 +2,13 @@ import { merge } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { TableCellOptions } from '@grafana/schema';
|
||||
import { Field, Select, TableCellDisplayMode } from '@grafana/ui';
|
||||
|
||||
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
|
||||
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
|
||||
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
|
||||
|
||||
// The props that any cell type editor are expected
|
||||
// to handle. In this case the generic type should
|
||||
@@ -64,12 +66,21 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
||||
{cellType === TableCellDisplayMode.ColorBackground && (
|
||||
<ColorBackgroundCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||
)}
|
||||
{cellType === TableCellDisplayMode.Sparkline && (
|
||||
<SparklineCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SparklineDisplayModeOption: SelectableValue<TableCellOptions> = {
|
||||
value: { type: TableCellDisplayMode.Sparkline },
|
||||
label: 'Sparkline',
|
||||
};
|
||||
|
||||
const cellDisplayModeOptions: Array<SelectableValue<TableCellOptions>> = [
|
||||
{ value: { type: TableCellDisplayMode.Auto }, label: 'Auto' },
|
||||
...(config.featureToggles.timeSeriesTable ? [SparklineDisplayModeOption] : []),
|
||||
{ value: { type: TableCellDisplayMode.ColorText }, label: 'Colored text' },
|
||||
{ value: { type: TableCellDisplayMode.ColorBackground }, label: 'Colored background' },
|
||||
{ value: { type: TableCellDisplayMode.Gauge }, label: 'Gauge' },
|
||||
|
||||
@@ -56,6 +56,7 @@ export function TablePanel(props: Props) {
|
||||
footerOptions={options.footer}
|
||||
enablePagination={options.footer?.enablePagination}
|
||||
subData={subData}
|
||||
cellHeight={options.cellHeight}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { createFieldConfigRegistry } from '@grafana/data';
|
||||
import { GraphFieldConfig, TableSparklineCellOptions } from '@grafana/schema';
|
||||
import { VerticalGroup, Field, useStyles2 } from '@grafana/ui';
|
||||
import { defaultSparklineCellConfig } from '@grafana/ui/src/components/Table/SparklineCell';
|
||||
|
||||
import { getGraphFieldConfig } from '../../timeseries/config';
|
||||
import { TableCellEditorProps } from '../TableCellOptionEditor';
|
||||
|
||||
type OptionKey = keyof TableSparklineCellOptions;
|
||||
|
||||
const optionIds: Array<keyof GraphFieldConfig> = [
|
||||
'drawStyle',
|
||||
'lineInterpolation',
|
||||
'barAlignment',
|
||||
'lineWidth',
|
||||
'fillOpacity',
|
||||
'gradientMode',
|
||||
'lineStyle',
|
||||
'spanNulls',
|
||||
'showPoints',
|
||||
'pointSize',
|
||||
];
|
||||
|
||||
export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSparklineCellOptions>) => {
|
||||
const { cellOptions, onChange } = props;
|
||||
|
||||
const registry = useMemo(() => {
|
||||
const config = getGraphFieldConfig(defaultSparklineCellConfig);
|
||||
return createFieldConfigRegistry(config, 'ChartCell');
|
||||
}, []);
|
||||
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
const values = { ...defaultSparklineCellConfig, ...cellOptions };
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
{registry.list(optionIds.map((id) => `custom.${id}`)).map((item) => {
|
||||
if (item.showIf && !item.showIf(values)) {
|
||||
return null;
|
||||
}
|
||||
const Editor = item.editor;
|
||||
const path = item.path;
|
||||
|
||||
return (
|
||||
<Field label={item.name} key={item.id} className={style.field}>
|
||||
<Editor
|
||||
onChange={(val) => onChange({ ...cellOptions, [path]: val })}
|
||||
value={(isOptionKey(path, values) ? values[path] : undefined) ?? item.defaultValue}
|
||||
item={item}
|
||||
context={{ data: [] }}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// jumping through hoops to avoid using "any"
|
||||
function isOptionKey(key: string, options: TableSparklineCellOptions): key is OptionKey {
|
||||
return key in options;
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
field: css`
|
||||
width: 100%;
|
||||
|
||||
// @TODO don't show "scheme" option for custom gradient mode.
|
||||
// it needs thresholds to work, which are not supported
|
||||
// for area chart cell right now
|
||||
[title='Use color scheme to define gradient'] {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
});
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
standardEditorsRegistry,
|
||||
identityOverrideProcessor,
|
||||
} from '@grafana/data';
|
||||
import { TableFieldOptions, TableCellOptions, TableCellDisplayMode, defaultTableFieldOptions } from '@grafana/schema';
|
||||
import {
|
||||
TableFieldOptions,
|
||||
TableCellOptions,
|
||||
TableCellDisplayMode,
|
||||
defaultTableFieldOptions,
|
||||
TableCellHeight,
|
||||
} from '@grafana/schema';
|
||||
|
||||
import { PaginationEditor } from './PaginationEditor';
|
||||
import { TableCellOptionEditor } from './TableCellOptionEditor';
|
||||
@@ -108,6 +114,18 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
|
||||
name: 'Show table header',
|
||||
defaultValue: defaultPanelOptions.showHeader,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'cellHeight',
|
||||
name: 'Cell height',
|
||||
defaultValue: defaultPanelOptions.cellHeight,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: TableCellHeight.Sm, label: 'Small' },
|
||||
{ value: TableCellHeight.Md, label: 'Medium' },
|
||||
{ value: TableCellHeight.Lg, label: 'Large' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showRowNums',
|
||||
name: 'Show row numbers',
|
||||
|
||||
@@ -45,6 +45,8 @@ composableKinds: PanelCfg: {
|
||||
// Represents the selected calculations
|
||||
reducer: []
|
||||
}
|
||||
// Controls the height of the rows
|
||||
cellHeight?: ui.TableCellHeight | *"md"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -13,6 +13,10 @@ import * as ui from '@grafana/schema';
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions {
|
||||
/**
|
||||
* Controls the height of the rows
|
||||
*/
|
||||
cellHeight?: ui.TableCellHeight;
|
||||
/**
|
||||
* Controls footer options
|
||||
*/
|
||||
@@ -40,6 +44,7 @@ export interface PanelOptions {
|
||||
}
|
||||
|
||||
export const defaultPanelOptions: Partial<PanelOptions> = {
|
||||
cellHeight: ui.TableCellHeight.Md,
|
||||
footer: {
|
||||
/**
|
||||
* Controls whether the footer should be shown
|
||||
|
||||
Reference in New Issue
Block a user