mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SparklineCell: Display absolute value (#76125)
This commit is contained in:
parent
d72ec22ec2
commit
239bda207e
@ -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
|
||||
|
|
@ -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 & 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…
Reference in New Issue
Block a user