diff --git a/devenv/dev-dashboards/panel-timeline/timeline-modes.json b/devenv/dev-dashboards/panel-timeline/timeline-modes.json index b9d0a2ba597..7b8184df53c 100644 --- a/devenv/dev-dashboards/panel-timeline/timeline-modes.json +++ b/devenv/dev-dashboards/panel-timeline/timeline-modes.json @@ -15,6 +15,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, + "id": 329, "links": [], "panels": [ { @@ -53,6 +54,7 @@ }, "id": 8, "options": { + "alignValue": "left", "colWidth": 0.9, "legend": { "calcs": [], @@ -230,6 +232,7 @@ }, "id": 9, "options": { + "alignValue": "left", "colWidth": 0.9, "legend": { "calcs": [], @@ -268,7 +271,7 @@ "fieldConfig": { "defaults": { "color": { - "mode": "palette-classic" + "mode": "thresholds" }, "custom": { "fillOpacity": 70, @@ -276,12 +279,16 @@ }, "mappings": [], "thresholds": { - "mode": "absolute", + "mode": "percentage", "steps": [ { "color": "green", "value": null }, + { + "color": "#EAB839", + "value": 60 + }, { "color": "red", "value": 80 @@ -364,5 +371,5 @@ "timezone": "utc", "title": "Timeline Modes", "uid": "mIJjFy8Gz", - "version": 21 -} \ No newline at end of file + "version": 12 +} diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts index 341cca53beb..4243f4bed5c 100644 --- a/packages/grafana-data/src/types/fieldColor.ts +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -5,6 +5,7 @@ export enum FieldColorModeId { Thresholds = 'thresholds', PaletteClassic = 'palette-classic', PaletteSaturated = 'palette-saturated', + ContinuousGrYlRd = 'continuous-GrYlRd', Fixed = 'fixed', } diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 6989cdc6d0d..eae9035421b 100755 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -3,7 +3,7 @@ import { PanelProps } from '@grafana/data'; import { useTheme2, ZoomPlugin } from '@grafana/ui'; import { TimelineMode, TimelineOptions } from './types'; import { TimelineChart } from './TimelineChart'; -import { prepareTimelineFields } from './utils'; +import { prepareTimelineFields, prepareTimelineLegendItems } from './utils'; interface TimelinePanelProps extends PanelProps {} @@ -26,6 +26,8 @@ export const StateTimelinePanel: React.FC = ({ options.mergeValues, ]); + const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend), [frames, options.legend]); + if (!frames || warn) { return (
@@ -43,6 +45,7 @@ export const StateTimelinePanel: React.FC = ({ timeZone={timeZone} width={width} height={height} + legendItems={legendItems} {...options} // hardcoded mode={TimelineMode.Changes} diff --git a/public/app/plugins/panel/state-timeline/TimelineChart.tsx b/public/app/plugins/panel/state-timeline/TimelineChart.tsx index 0d729eb4dcc..3fb00b81571 100755 --- a/public/app/plugins/panel/state-timeline/TimelineChart.tsx +++ b/public/app/plugins/panel/state-timeline/TimelineChart.tsx @@ -1,5 +1,16 @@ import React from 'react'; -import { PanelContext, PanelContextRoot, GraphNG, GraphNGProps, BarValueVisibility } from '@grafana/ui'; +import { + PanelContext, + PanelContextRoot, + GraphNG, + GraphNGProps, + BarValueVisibility, + LegendDisplayMode, + UPlotConfigBuilder, + VizLayout, + VizLegend, + VizLegendItem, +} from '@grafana/ui'; import { DataFrame, FieldType, TimeRange } from '@grafana/data'; import { preparePlotConfigBuilder } from './utils'; import { TimelineMode, TimelineValueAlignment } from './types'; @@ -13,6 +24,7 @@ export interface TimelineProps extends Omit { }); }; - renderLegend = () => null; + renderLegend = (config: UPlotConfigBuilder) => { + const { legend, legendItems } = this.props; + + if (!config || !legendItems || !legend || legend.displayMode === LegendDisplayMode.Hidden) { + return null; + } + + return ( + + + + ); + }; render() { return ( diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index e78bdf0a703..00898b28886 100755 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -2,6 +2,7 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/dat import { StateTimelinePanel } from './StateTimelinePanel'; import { TimelineOptions, TimelineFieldConfig, defaultPanelOptions, defaultTimelineFieldConfig } from './types'; import { BarValueVisibility } from '@grafana/ui'; +import { addLegendOptions } from '@grafana/ui/src/options/builder'; export const plugin = new PanelPlugin(StateTimelinePanel) .useFieldConfig({ @@ -11,7 +12,7 @@ export const plugin = new PanelPlugin(Stat byValueSupport: true, }, defaultValue: { - mode: FieldColorModeId.PaletteClassic, + mode: FieldColorModeId.ContinuousGrYlRd, }, }, }, @@ -81,5 +82,5 @@ export const plugin = new PanelPlugin(Stat defaultValue: defaultPanelOptions.rowHeight, }); - //addLegendOptions(builder); + addLegendOptions(builder, false); }); diff --git a/public/app/plugins/panel/state-timeline/plugin.json b/public/app/plugins/panel/state-timeline/plugin.json index 82a6e07ed9e..9710a2c8d36 100755 --- a/public/app/plugins/panel/state-timeline/plugin.json +++ b/public/app/plugins/panel/state-timeline/plugin.json @@ -3,7 +3,7 @@ "name": "State timeline", "id": "state-timeline", - "state": "alpha", + "state": "beta", "info": { "description": "State changes and durations", diff --git a/public/app/plugins/panel/state-timeline/types.ts b/public/app/plugins/panel/state-timeline/types.ts index 4c4e5d6bd46..fc82e4c73a5 100644 --- a/public/app/plugins/panel/state-timeline/types.ts +++ b/public/app/plugins/panel/state-timeline/types.ts @@ -1,12 +1,11 @@ -import { VizLegendOptions, HideableFieldConfig, BarValueVisibility } from '@grafana/ui'; +import { HideableFieldConfig, BarValueVisibility, OptionsWithLegend } from '@grafana/ui'; /** * @alpha */ -export interface TimelineOptions { +export interface TimelineOptions extends OptionsWithLegend { mode: TimelineMode; // not in the saved model! - legend: VizLegendOptions; showValue: BarValueVisibility; rowHeight: number; diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index f7f11282e2d..cb207894793 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -10,8 +10,18 @@ import { FALLBACK_COLOR, FieldType, ArrayVector, + FieldColorModeId, + getValueFormat, + ThresholdsMode, } from '@grafana/data'; -import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui'; +import { + UPlotConfigBuilder, + FIXED_UNIT, + SeriesVisibilityChangeMode, + UPlotConfigPrepFn, + VizLegendOptions, + VizLegendItem, +} from '@grafana/ui'; import { TimelineCoreOptions, getConfig } from './timeline'; import { AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config'; import { measureText } from '@grafana/ui/src/utils/measureText'; @@ -291,3 +301,77 @@ export function prepareTimelineFields( } return { frames }; } + +export function prepareTimelineLegendItems( + frames: DataFrame[] | undefined, + options: VizLegendOptions +): VizLegendItem[] | undefined { + if (!frames || options.displayMode === 'hidden') { + return undefined; + } + + const fields = allNonTimeFields(frames); + if (!fields.length) { + return undefined; + } + + const items: VizLegendItem[] = []; + const first = fields[0].config; + const colorMode = first.color?.mode ?? FieldColorModeId.Fixed; + + // If thresholds are enabled show each step in the legend + if (colorMode === FieldColorModeId.Thresholds && first.thresholds?.steps) { + const steps = first.thresholds.steps; + const disp = getValueFormat( + first.thresholds.mode === ThresholdsMode.Percentage ? 'percent' : first.unit ?? 'fixed' + ); + const fmt = (v: number) => formattedValueToString(disp(v)); + for (let i = 1; i <= steps.length; i++) { + const step = steps[i - 1]; + items.push({ + label: i === 1 ? `< ${fmt(steps[i].value)}` : `${fmt(step.value)}+`, + color: step.color, + yAxis: 1, + }); + } + return items; + } + + // If thresholds are enabled show each step in the legend + if (colorMode.startsWith('continuous')) { + return undefined; // eventually a color bar + } + + let stateColors: Map = new Map(); + + fields.forEach((field) => { + field.values.toArray().forEach((v) => { + let state = field.display!(v); + stateColors.set(state.text, state.color!); + }); + }); + + stateColors.forEach((color, label) => { + if (label.length > 0) { + items.push({ + label: label!, + color, + yAxis: 1, + }); + } + }); + + return items; +} + +function allNonTimeFields(frames: DataFrame[]): Field[] { + const fields: Field[] = []; + for (const frame of frames) { + for (const field of frame.fields) { + if (field.type !== FieldType.time) { + fields.push(field); + } + } + } + return fields; +} diff --git a/public/app/plugins/panel/status-grid/StatusGridPanel.tsx b/public/app/plugins/panel/status-grid/StatusGridPanel.tsx index e91cb44410a..a4de9f152fd 100755 --- a/public/app/plugins/panel/status-grid/StatusGridPanel.tsx +++ b/public/app/plugins/panel/status-grid/StatusGridPanel.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { PanelProps } from '@grafana/data'; import { useTheme2, ZoomPlugin } from '@grafana/ui'; import { StatusPanelOptions } from './types'; import { TimelineChart } from '../state-timeline/TimelineChart'; import { TimelineMode } from '../state-timeline/types'; +import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-timeline/utils'; interface TimelinePanelProps extends PanelProps {} @@ -21,10 +22,14 @@ export const StatusGridPanel: React.FC = ({ }) => { const theme = useTheme2(); - if (!data || !data.series?.length) { + const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, false), [data]); + + const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend), [frames, options.legend]); + + if (!frames || warn) { return (
-

No data found in response

+

{warn ?? 'No data found in response'}

); } @@ -32,12 +37,13 @@ export const StatusGridPanel: React.FC = ({ return ( (StatusGridPanel) .useFieldConfig({ @@ -11,7 +12,7 @@ export const plugin = new PanelPlugin(Sta byValueSupport: true, }, defaultValue: { - mode: FieldColorModeId.PaletteClassic, + mode: FieldColorModeId.Thresholds, }, }, }, @@ -74,5 +75,5 @@ export const plugin = new PanelPlugin(Sta }, }); - //addLegendOptions(builder); + addLegendOptions(builder, false); }); diff --git a/public/app/plugins/panel/status-grid/plugin.json b/public/app/plugins/panel/status-grid/plugin.json index 08046fa495e..175533e63e2 100755 --- a/public/app/plugins/panel/status-grid/plugin.json +++ b/public/app/plugins/panel/status-grid/plugin.json @@ -3,7 +3,7 @@ "name": "Status grid", "id": "status-grid", - "state": "alpha", + "state": "beta", "info": { "description": "Periodic status history",