SparklineCell: Display absolute value (#76125)

pull/76192/head^2
Domas 2 years ago committed by GitHub
parent d72ec22ec2
commit 239bda207e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      devenv/dev-dashboards/panel-table/table_tests_new.json
  2. 1
      docs/sources/developers/kinds/composable/table/panelcfg/schema-reference.md
  3. 5
      docs/sources/panels-visualizations/query-transform-data/transform-data/index.md
  4. 5
      docs/sources/panels-visualizations/visualizations/table/index.md
  5. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  6. 4
      packages/grafana-data/src/dataframe/processDataFrame.ts
  7. 4
      packages/grafana-data/src/transformations/fieldReducer.ts
  8. 5
      packages/grafana-data/src/types/dataFrame.ts
  9. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  10. 1
      packages/grafana-schema/src/common/common.gen.ts
  11. 1
      packages/grafana-schema/src/common/table.cue
  12. 8
      packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx
  13. 49
      packages/grafana-ui/src/components/Table/BarGaugeCell.tsx
  14. 61
      packages/grafana-ui/src/components/Table/SparklineCell.tsx
  15. 53
      packages/grafana-ui/src/components/Table/utils.ts
  16. 7
      pkg/services/featuremgmt/registry.go
  17. 1
      pkg/services/featuremgmt/toggles_gen.csv
  18. 4
      pkg/services/featuremgmt/toggles_gen.go
  19. 3
      public/app/features/transformers/standardTransformers.ts
  20. 55
      public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx
  21. 35
      public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts
  22. 18
      public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts
  23. 9
      public/app/plugins/panel/table/TableCellOptionEditor.tsx
  24. 21
      public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx

@ -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"
}
]
},

@ -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 |

@ -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" %}}

@ -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

@ -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. |

@ -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
*/

@ -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;

@ -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

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

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

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

@ -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}

@ -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;
}
}

@ -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>
);
};

@ -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;
}
}

@ -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",

@ -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

@ -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"

@ -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,
];
};

@ -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> = {

@ -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,
},
],

@ -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;
}

@ -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' },

@ -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');
}, []);

Loading…
Cancel
Save