SparklineCell: Display absolute value (#76125)

This commit is contained in:
Domas 2023-10-13 11:00:42 +03:00 committed by GitHub
parent d72ec22ec2
commit 239bda207e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 253 additions and 111 deletions

View File

@ -393,6 +393,10 @@
"value": {
"mode": "continuous-GrYlRd"
}
},
{
"id": "unit",
"value": "r/sec"
}
]
},
@ -538,6 +542,10 @@
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "unit",
"value": "r/sec"
}
]
},
@ -563,6 +571,10 @@
"fixedColor": "purple",
"mode": "fixed"
}
},
{
"id": "unit",
"value": "ms"
}
]
},

View File

@ -170,6 +170,7 @@ It extends [GraphFieldConfig](#graphfieldconfig).
| `fillOpacity` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* |
| `gradientMode` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*<br/>TODO docs<br/>Possible values are: `none`, `opacity`, `hue`, `scheme`. |
| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*<br/>TODO docs |
| `hideValue` | boolean | No | | |
| `lineColor` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* |
| `lineInterpolation` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*<br/>TODO docs<br/>Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. |
| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*<br/>TODO docs |

View File

@ -995,11 +995,10 @@ Here is the result after adding a Limit transformation with a value of '3':
### Time series to table transform
> **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature.
> Modify Grafana [configuration file][] to enable the `timeSeriesTable` [feature toggle][] to use it.
Use this transformation to convert time series result into a table, converting time series data frame into a "Trend" field. "Trend" field can then be rendered using [sparkline cell type][], producing an inline sparkline for each table row. If there are multiple time series queries, each will result in a separate table data frame. These can be joined using join or merge transforms to produce a single table with multiple sparklines per row.
For each generated "Trend" field value calculation function can be selected. Default is "last non null value". This value will be displayed next to the sparkline and used for sorting table rows.
### Format Time
{{% admonition type="note" %}}

View File

@ -148,12 +148,9 @@ If you have a field value that is an image URL or a base64 encoded image you can
### Sparkline
> **Note:** This cell type is available in Grafana 9.5+ as an opt-in beta feature.
> Modify Grafana [configuration file][] to enable the `timeSeriesTable` [feature toggle][] to use it.
Shows value rendered as a sparkline. Requires [time series to table][] data transform.
{{< figure src="/static/img/docs/tables/sparkline.png" max-width="500px" caption="Sparkline" class="docs-image--no-shadow" >}}
{{< figure src="/static/img/docs/tables/sparkline2.png" max-width="500px" caption="Sparkline" class="docs-image--no-shadow" >}}
## Cell value inspect

View File

@ -104,7 +104,6 @@ Experimental features might be changed or removed without prior notice.
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries |
| `individualCookiePreferences` | Support overriding cookie preferences per user |
| `timeSeriesTable` | Enable time series table transformer & sparkline cell type |
| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation |
| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. |
| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. |

View File

@ -23,6 +23,7 @@ import {
PanelData,
LoadingState,
GraphSeriesValue,
DataFrameWithValue,
} from '../types/index';
import { arrayToDataFrame } from './ArrayDataFrame';
@ -303,6 +304,9 @@ export const isTableData = (data: unknown): data is DataFrame => Boolean(data &&
export const isDataFrame = (data: unknown): data is DataFrame => Boolean(data && data.hasOwnProperty('fields'));
export const isDataFrameWithValue = (data: unknown): data is DataFrameWithValue =>
Boolean(isDataFrame(data) && data.hasOwnProperty('value'));
/**
* Inspect any object and return the results as a DataFrame
*/

View File

@ -30,6 +30,10 @@ export enum ReducerID {
uniqueValues = 'uniqueValues',
}
export function isReducerID(id: string): id is ReducerID {
return Object.keys(ReducerID).includes(id);
}
// Internal function
type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;

View File

@ -247,6 +247,11 @@ export interface DataFrame extends QueryResultBase {
length: number;
}
// Data frame that include aggregate value, for use by timeSeriesTableTransformer / chart cell type
export interface DataFrameWithValue extends DataFrame {
value: number | null;
}
/**
* @public
* Like a field, but properties are optional and values may be a simple array

View File

@ -65,7 +65,6 @@ export interface FeatureToggles {
individualCookiePreferences?: boolean;
gcomOnlyExternalOrgRoleSync?: boolean;
prometheusMetricEncyclopedia?: boolean;
timeSeriesTable?: boolean;
influxdbBackendMigration?: boolean;
clientTokenRotation?: boolean;
prometheusDataplane?: boolean;

View File

@ -756,6 +756,7 @@ export interface TableBarGaugeCellOptions {
* Sparkline cell options
*/
export interface TableSparklineCellOptions extends GraphFieldConfig {
hideValue?: boolean;
type: TableCellDisplayMode.Sparkline;
}

View File

@ -59,6 +59,7 @@ TableBarGaugeCellOptions: {
TableSparklineCellOptions: {
GraphFieldConfig
type: TableCellDisplayMode & "sparkline"
hideValue?: bool
} @cuetsy(kind="interface")
// Colored background cell options

View File

@ -1,7 +1,7 @@
import { difference } from 'lodash';
import React, { PureComponent } from 'react';
import { fieldReducers, SelectableValue } from '@grafana/data';
import { fieldReducers, SelectableValue, FieldReducerInfo } from '@grafana/data';
import { Select } from '../Select/Select';
@ -15,6 +15,7 @@ export interface Props {
width?: number;
menuPlacement?: 'auto' | 'bottom' | 'top';
inputId?: string;
filterOptions?: (ext: FieldReducerInfo) => boolean;
}
export class StatsPicker extends PureComponent<Props> {
@ -63,9 +64,10 @@ export class StatsPicker extends PureComponent<Props> {
};
render() {
const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement, width, inputId } = this.props;
const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement, width, inputId, filterOptions } =
this.props;
const select = fieldReducers.selectOptions(stats);
const select = fieldReducers.selectOptions(stats, filterOptions);
return (
<Select
value={select.current}

View File

@ -1,22 +1,14 @@
import { isFunction } from 'lodash';
import React from 'react';
import {
ThresholdsConfig,
ThresholdsMode,
VizOrientation,
getFieldConfigWithMinMax,
DisplayValueAlignmentFactors,
Field,
DisplayValue,
} from '@grafana/data';
import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data';
import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema';
import { BarGauge } from '../BarGauge/BarGauge';
import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu';
import { TableCellProps } from './types';
import { getCellOptions } from './utils';
import { getAlignmentFactor, getCellOptions } from './utils';
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
@ -102,40 +94,3 @@ export const BarGaugeCell = (props: TableCellProps) => {
</div>
);
};
/**
* Getting gauge values to align is very tricky without looking at all values and passing them through display processor. For very large tables that
* could pretty expensive. So this is kind of a compromise. We look at the first 1000 rows and cache the longest value.
* If we have a cached value we just check if the current value is longer and update the alignmentFactor. This can obviously still lead to
* unaligned gauges but it should a lot less common.
**/
function getAlignmentFactor(field: Field, displayValue: DisplayValue, rowIndex: number): DisplayValueAlignmentFactors {
let alignmentFactor = field.state?.alignmentFactors;
if (alignmentFactor) {
// check if current alignmentFactor is still the longest
if (alignmentFactor.text.length < displayValue.text.length) {
alignmentFactor.text = displayValue.text;
}
return alignmentFactor;
} else {
// look at the next 100 rows
alignmentFactor = { ...displayValue };
const maxIndex = Math.min(field.values.length, rowIndex + 1000);
for (let i = rowIndex + 1; i < maxIndex; i++) {
const nextDisplayValue = field.display!(field.values[i]);
if (nextDisplayValue.text.length > alignmentFactor.text.length) {
alignmentFactor.text = displayValue.text;
}
}
if (field.state) {
field.state.alignmentFactors = alignmentFactor;
} else {
field.state = { alignmentFactors: alignmentFactor };
}
return alignmentFactor;
}
}

View File

@ -1,6 +1,14 @@
import React from 'react';
import { FieldType, FieldConfig, getMinMaxAndDelta, FieldSparkline, isDataFrame, Field } from '@grafana/data';
import {
FieldType,
FieldConfig,
getMinMaxAndDelta,
FieldSparkline,
isDataFrame,
Field,
isDataFrameWithValue,
} from '@grafana/data';
import {
BarAlignment,
GraphDrawStyle,
@ -12,12 +20,16 @@ import {
VisibilityMode,
} from '@grafana/schema';
import { useTheme2 } from '../../themes';
import { measureText } from '../../utils';
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
import { Sparkline } from '../Sparkline/Sparkline';
import { TableCellProps } from './types';
import { getCellOptions } from './utils';
import { getAlignmentFactor, getCellOptions } from './utils';
export const defaultSparklineCellConfig: GraphFieldConfig = {
export const defaultSparklineCellConfig: TableSparklineCellOptions = {
type: TableCellDisplayMode.Sparkline,
drawStyle: GraphDrawStyle.Line,
lineInterpolation: LineInterpolation.Smooth,
lineWidth: 1,
@ -26,11 +38,13 @@ export const defaultSparklineCellConfig: GraphFieldConfig = {
pointSize: 2,
barAlignment: BarAlignment.Center,
showPoints: VisibilityMode.Never,
hideValue: false,
};
export const SparklineCell = (props: TableCellProps) => {
const { field, innerWidth, tableStyles, cell, cellProps, timeRange } = props;
const sparkline = getSparkline(cell.value);
const theme = useTheme2();
if (!sparkline) {
return (
@ -70,15 +84,42 @@ export const SparklineCell = (props: TableCellProps) => {
},
};
const hideValue = field.config.custom?.cellOptions?.hideValue;
let valueWidth = 0;
let valueElement: React.ReactNode = null;
if (!hideValue) {
const value = isDataFrameWithValue(cell.value) ? cell.value.value : null;
const displayValue = field.display!(value);
const alignmentFactor = getAlignmentFactor(field, displayValue, cell.row.index);
valueWidth =
measureText(`${alignmentFactor.prefix ?? ''}${alignmentFactor.text}${alignmentFactor.suffix ?? ''}`, 16).width +
theme.spacing.gridSize;
valueElement = (
<FormattedValueDisplay
style={{
width: `${valueWidth - theme.spacing.gridSize}px`,
textAlign: 'right',
marginRight: theme.spacing(1),
}}
value={displayValue}
/>
);
}
return (
<div {...cellProps} className={tableStyles.cellContainer}>
<Sparkline
width={innerWidth}
height={tableStyles.cellHeightInner}
sparkline={sparkline}
config={config}
theme={tableStyles.theme}
/>
{valueElement}
<div>
<Sparkline
width={innerWidth - valueWidth}
height={tableStyles.cellHeightInner}
sparkline={sparkline}
config={config}
theme={tableStyles.theme}
/>
</div>
</div>
);
};

View File

@ -15,7 +15,10 @@ import {
reduceField,
GrafanaTheme2,
isDataFrame,
isDataFrameWithValue,
isTimeSeriesFrame,
DisplayValueAlignmentFactors,
DisplayValue,
} from '@grafana/data';
import {
BarGaugeDisplayMode,
@ -115,6 +118,7 @@ export function getColumns(
const selectSortType = (type: FieldType) => {
switch (type) {
case FieldType.number:
case FieldType.frame:
return 'number';
case FieldType.time:
return 'basic';
@ -131,9 +135,7 @@ export function getColumns(
id: fieldIndex.toString(),
field: field,
Header: fieldTableOptions.hideHeader ? '' : getFieldDisplayName(field, data),
accessor: (_row, i) => {
return field.values[i];
},
accessor: (_row, i) => field.values[i],
sortType: selectSortType(field.type),
width: fieldTableOptions.width,
minWidth: fieldTableOptions.minWidth ?? columnMinWidth,
@ -305,6 +307,10 @@ export function sortNumber(rowA: Row, rowB: Row, id: string) {
}
function toNumber(value: any): number {
if (isDataFrameWithValue(value)) {
return value.value ?? Number.NEGATIVE_INFINITY;
}
if (value === null || value === undefined || value === '' || isNaN(value)) {
return Number.NEGATIVE_INFINITY;
}
@ -486,3 +492,44 @@ function addMissingColumnIndex(columns: Array<{ id: string; field?: Field } | un
// Recurse
addMissingColumnIndex(columns);
}
/**
* Getting gauge or sparkline values to align is very tricky without looking at all values and passing them through display processor.
* For very large tables that could pretty expensive. So this is kind of a compromise. We look at the first 1000 rows and cache the longest value.
* If we have a cached value we just check if the current value is longer and update the alignmentFactor. This can obviously still lead to
* unaligned gauges but it should a lot less common.
**/
export function getAlignmentFactor(
field: Field,
displayValue: DisplayValue,
rowIndex: number
): DisplayValueAlignmentFactors {
let alignmentFactor = field.state?.alignmentFactors;
if (alignmentFactor) {
// check if current alignmentFactor is still the longest
if (alignmentFactor.text.length < displayValue.text.length) {
alignmentFactor.text = displayValue.text;
}
return alignmentFactor;
} else {
// look at the next 1000 rows
alignmentFactor = { ...displayValue };
const maxIndex = Math.min(field.values.length, rowIndex + 1000);
for (let i = rowIndex + 1; i < maxIndex; i++) {
const nextDisplayValue = field.display!(field.values[i]);
if (nextDisplayValue.text.length > alignmentFactor.text.length) {
alignmentFactor.text = displayValue.text;
}
}
if (field.state) {
field.state.alignmentFactors = alignmentFactor;
} else {
field.state = { alignmentFactors: alignmentFactor };
}
return alignmentFactor;
}
}

View File

@ -324,13 +324,6 @@ var (
FrontendOnly: true,
Owner: grafanaObservabilityMetricsSquad,
},
{
Name: "timeSeriesTable",
Description: "Enable time series table transformer & sparkline cell type",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: appO11ySquad,
},
{
Name: "influxdbBackendMigration",
Description: "Query InfluxDB InfluxQL without the proxy",

View File

@ -46,7 +46,6 @@ lokiQuerySplittingConfig,experimental,@grafana/observability-logs,false,false,fa
individualCookiePreferences,experimental,@grafana/backend-platform,false,false,false,false
gcomOnlyExternalOrgRoleSync,GA,@grafana/grafana-authnz-team,false,false,false,false
prometheusMetricEncyclopedia,GA,@grafana/observability-metrics,false,false,false,true
timeSeriesTable,experimental,@grafana/app-o11y,false,false,false,true
influxdbBackendMigration,preview,@grafana/observability-metrics,false,false,false,true
clientTokenRotation,experimental,@grafana/grafana-authnz-team,false,false,false,false
prometheusDataplane,GA,@grafana/observability-metrics,false,false,false,false

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
46 individualCookiePreferences experimental @grafana/backend-platform false false false false
47 gcomOnlyExternalOrgRoleSync GA @grafana/grafana-authnz-team false false false false
48 prometheusMetricEncyclopedia GA @grafana/observability-metrics false false false true
timeSeriesTable experimental @grafana/app-o11y false false false true
49 influxdbBackendMigration preview @grafana/observability-metrics false false false true
50 clientTokenRotation experimental @grafana/grafana-authnz-team false false false false
51 prometheusDataplane GA @grafana/observability-metrics false false false false

View File

@ -195,10 +195,6 @@ const (
// Adds the metrics explorer component to the Prometheus query builder as an option in metric select
FlagPrometheusMetricEncyclopedia = "prometheusMetricEncyclopedia"
// FlagTimeSeriesTable
// Enable time series table transformer &amp; sparkline cell type
FlagTimeSeriesTable = "timeSeriesTable"
// FlagInfluxdbBackendMigration
// Query InfluxDB InfluxQL without the proxy
FlagInfluxdbBackendMigration = "influxdbBackendMigration"

View File

@ -1,5 +1,4 @@
import { TransformerRegistryItem } from '@grafana/data';
import { config } from '@grafana/runtime';
import { filterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor';
import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor';
@ -61,6 +60,6 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
joinByLabelsTransformRegistryItem,
partitionByValuesTransformRegistryItem,
formatTimeTransformerRegistryItem,
...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []),
timeSeriesTableTransformRegistryItem,
];
};

View File

@ -1,17 +1,56 @@
import React from 'react';
import React, { useCallback } from 'react';
import { PluginState, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
import { PluginState, TransformerRegistryItem, TransformerUIProps, ReducerID, isReducerID } from '@grafana/data';
import { InlineFieldRow, InlineField, StatsPicker } from '@grafana/ui';
import { timeSeriesTableTransformer, TimeSeriesTableTransformerOptions } from './timeSeriesTableTransformer';
export interface Props extends TransformerUIProps<{}> {}
export function TimeSeriesTableTransformEditor({
input,
options,
onChange,
}: TransformerUIProps<TimeSeriesTableTransformerOptions>) {
const refIds: string[] = input.reduce<string[]>((acc, frame) => {
if (frame.refId && !acc.includes(frame.refId)) {
return [...acc, frame.refId];
}
return acc;
}, []);
export function TimeSeriesTableTransformEditor({ input, options, onChange }: Props) {
if (input.length === 0) {
return null;
}
const onSelectStat = useCallback(
(refId: string, stats: string[]) => {
const reducerID = stats[0];
if (reducerID && isReducerID(reducerID)) {
onChange({
refIdToStat: {
...options.refIdToStat,
[refId]: reducerID,
},
});
}
},
[onChange, options]
);
return <div></div>;
return (
<>
{refIds.map((refId) => {
return (
<div key={refId}>
<InlineFieldRow>
<InlineField label={`Trend ${refIds.length > 1 ? ` #${refId}` : ''} value`}>
<StatsPicker
stats={[options.refIdToStat?.[refId] ?? ReducerID.lastNotNull]}
onChange={onSelectStat.bind(null, refId)}
filterOptions={(ext) => ext.id !== ReducerID.allValues && ext.id !== ReducerID.uniqueValues}
/>
</InlineField>
</InlineFieldRow>
</div>
);
})}
</>
);
}
export const timeSeriesTableTransformRegistryItem: TransformerRegistryItem<TimeSeriesTableTransformerOptions> = {

View File

@ -1,4 +1,4 @@
import { toDataFrame, FieldType, Labels, DataFrame, Field } from '@grafana/data';
import { toDataFrame, FieldType, Labels, DataFrame, Field, ReducerID } from '@grafana/data';
import { timeSeriesToTableTransform } from './timeSeriesTableTransformer';
@ -61,6 +61,35 @@ describe('timeSeriesTableTransformer', () => {
expect(results[1].fields[2].values).toEqual(['A', 'B']);
assertDataFrameField(results[1].fields[3], series.slice(3, 5));
});
it('Will include last value by deault', () => {
const series = [
getTimeSeries('A', { instance: 'A', pod: 'B' }, [4, 2, 3]),
getTimeSeries('A', { instance: 'A', pod: 'C' }, [3, 4, 5]),
];
const results = timeSeriesToTableTransform({}, series);
expect(results[0].fields[2].values[0].value).toEqual(3);
expect(results[0].fields[2].values[1].value).toEqual(5);
});
it('Will calculate average value if configured', () => {
const series = [
getTimeSeries('A', { instance: 'A', pod: 'B' }, [4, 2, 3]),
getTimeSeries('B', { instance: 'A', pod: 'C' }, [3, 4, 5]),
];
const results = timeSeriesToTableTransform(
{
refIdToStat: {
B: ReducerID.mean,
},
},
series
);
expect(results[0].fields[2].values[0].value).toEqual(3);
expect(results[1].fields[2].values[0].value).toEqual(4);
});
});
function assertFieldsEqual(field1: Field, field2: Field) {
@ -80,7 +109,7 @@ function assertDataFrameField(field: Field, matchesFrames: DataFrame[]) {
});
}
function getTimeSeries(refId: string, labels: Labels) {
function getTimeSeries(refId: string, labels: Labels, values: number[] = [10]) {
return toDataFrame({
refId,
fields: [
@ -88,7 +117,7 @@ function getTimeSeries(refId: string, labels: Labels) {
{
name: 'Value',
type: FieldType.number,
values: [10],
values,
labels,
},
],

View File

@ -2,15 +2,20 @@ import { map } from 'rxjs/operators';
import {
DataFrame,
DataFrameWithValue,
DataTransformerID,
DataTransformerInfo,
Field,
FieldType,
MutableDataFrame,
isTimeSeriesFrame,
ReducerID,
reduceField,
} from '@grafana/data';
export interface TimeSeriesTableTransformerOptions {}
export interface TimeSeriesTableTransformerOptions {
refIdToStat?: Record<string, ReducerID>;
}
export const timeSeriesTableTransformer: DataTransformerInfo<TimeSeriesTableTransformerOptions> = {
id: DataTransformerID.timeSeriesTable,
@ -44,7 +49,7 @@ export function timeSeriesToTableTransform(options: TimeSeriesTableTransformerOp
// initialize fields from labels for each refId
const refId2LabelFields = getLabelFields(data);
const refId2frameField: Record<string, Field<DataFrame>> = {};
const refId2frameField: Record<string, Field<DataFrameWithValue>> = {};
const result: DataFrame[] = [];
@ -83,8 +88,13 @@ export function timeSeriesToTableTransform(options: TimeSeriesTableTransformerOp
const labelValue = labels?.[labelKey] ?? null;
labelFields[labelKey].values.push(labelValue!);
}
frameField.values.push(frame);
const reducerId = options.refIdToStat?.[refId] ?? ReducerID.lastNotNull;
const valueField = frame.fields.find((f) => f.type === FieldType.number);
const value = (valueField && reduceField({ field: valueField, reducers: [reducerId] })[reducerId]) || null;
frameField.values.push({
...frame,
value,
});
}
return result;
}

View File

@ -3,7 +3,7 @@ import { merge } from 'lodash';
import React, { useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { TableCellOptions } from '@grafana/schema';
import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui';
@ -77,14 +77,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
);
};
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.Sparkline }, label: 'Sparkline' },
{ value: { type: TableCellDisplayMode.ColorText }, label: 'Colored text' },
{ value: { type: TableCellDisplayMode.ColorBackground }, label: 'Colored background' },
{ value: { type: TableCellDisplayMode.Gauge }, label: 'Gauge' },

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { createFieldConfigRegistry } from '@grafana/data';
import { createFieldConfigRegistry, SetFieldConfigOptionsArgs } 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';
@ -11,7 +11,8 @@ import { TableCellEditorProps } from '../TableCellOptionEditor';
type OptionKey = keyof TableSparklineCellOptions;
const optionIds: Array<keyof GraphFieldConfig> = [
const optionIds: Array<keyof TableSparklineCellOptions> = [
'hideValue',
'drawStyle',
'lineInterpolation',
'barAlignment',
@ -24,11 +25,25 @@ const optionIds: Array<keyof GraphFieldConfig> = [
'pointSize',
];
function getChartCellConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs<GraphFieldConfig> {
const graphFieldConfig = getGraphFieldConfig(cfg);
return {
...graphFieldConfig,
useCustomConfig: (builder) => {
graphFieldConfig.useCustomConfig?.(builder);
builder.addBooleanSwitch({
path: 'hideValue',
name: 'Hide value',
});
},
};
}
export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSparklineCellOptions>) => {
const { cellOptions, onChange } = props;
const registry = useMemo(() => {
const config = getGraphFieldConfig(defaultSparklineCellConfig);
const config = getChartCellConfig(defaultSparklineCellConfig);
return createFieldConfigRegistry(config, 'ChartCell');
}, []);