mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Timeline: split "periodic" mode into its own panel (#34171)
This commit is contained in:
parent
c630393cf4
commit
de5cd4a7d3
@ -110,7 +110,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "State changes strings",
|
"title": "State changes strings",
|
||||||
"type": "timeline"
|
"type": "state-timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"datasource": null,
|
"datasource": null,
|
||||||
@ -204,7 +204,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "State changes with boolean values",
|
"title": "State changes with boolean values",
|
||||||
"type": "timeline"
|
"type": "state-timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"datasource": null,
|
"datasource": null,
|
||||||
@ -298,7 +298,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "State changes with nulls",
|
"title": "State changes with nulls",
|
||||||
"type": "timeline"
|
"type": "state-timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"datasource": null,
|
"datasource": null,
|
||||||
@ -388,8 +388,8 @@
|
|||||||
"stringInput": ""
|
"stringInput": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "\"Periodic samples\" Mode",
|
"title": "Status map",
|
||||||
"type": "timeline"
|
"type": "status-grid"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refresh": false,
|
"refresh": false,
|
||||||
|
@ -188,7 +188,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "\"State changes\" Mode",
|
"title": "\"State changes\" Mode",
|
||||||
"type": "timeline"
|
"type": "state-timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"datasource": null,
|
"datasource": null,
|
||||||
@ -253,7 +253,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "\"State changes\" Mode (strings & booleans)",
|
"title": "\"State changes\" Mode (strings & booleans)",
|
||||||
"type": "timeline"
|
"type": "state-timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"datasource": null,
|
"datasource": null,
|
||||||
@ -331,8 +331,8 @@
|
|||||||
"stringInput": ""
|
"stringInput": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "\"Periodic samples\" Mode",
|
"title": "Status map view",
|
||||||
"type": "timeline"
|
"type": "status-grid"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refresh": false,
|
"refresh": false,
|
||||||
|
@ -155,7 +155,8 @@ export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Support the standard graph span nulls field config
|
// Support the standard graph span nulls field config
|
||||||
nullModesFrame.push(field.config.custom?.spanNulls === true ? NULL_REMOVE : NULL_EXPAND);
|
let spanNulls = field.config.custom?.spanNulls;
|
||||||
|
nullModesFrame.push(spanNulls === true ? NULL_REMOVE : spanNulls === -1 ? NULL_RETAIN : NULL_EXPAND);
|
||||||
|
|
||||||
let labels = field.labels ?? {};
|
let labels = field.labels ?? {};
|
||||||
if (frame.name) {
|
if (frame.name) {
|
||||||
@ -285,7 +286,7 @@ export function join(tables: AlignedData[], nullModes?: number[][]) {
|
|||||||
let yVal = ys[i];
|
let yVal = ys[i];
|
||||||
let alignedIdx = xIdxs.get(xs[i]);
|
let alignedIdx = xIdxs.get(xs[i]);
|
||||||
|
|
||||||
if (yVal == null) {
|
if (yVal === null) {
|
||||||
if (nullMode !== NULL_REMOVE) {
|
if (nullMode !== NULL_REMOVE) {
|
||||||
yVals[alignedIdx] = yVal;
|
yVals[alignedIdx] = yVal;
|
||||||
|
|
||||||
|
@ -274,20 +274,22 @@ func getPanelSort(id string) int {
|
|||||||
sort = 7
|
sort = 7
|
||||||
case "piechart":
|
case "piechart":
|
||||||
sort = 8
|
sort = 8
|
||||||
case "timeline":
|
case "state-timeline":
|
||||||
sort = 9
|
sort = 9
|
||||||
case "heatmap":
|
case "heatmap":
|
||||||
sort = 10
|
sort = 10
|
||||||
case "graph":
|
case "status-grid":
|
||||||
sort = 11
|
sort = 11
|
||||||
case "text":
|
case "graph":
|
||||||
sort = 12
|
sort = 12
|
||||||
case "alertlist":
|
case "text":
|
||||||
sort = 13
|
sort = 13
|
||||||
case "dashlist":
|
case "alertlist":
|
||||||
sort = 14
|
sort = 14
|
||||||
case "news":
|
case "dashlist":
|
||||||
sort = 15
|
sort = 15
|
||||||
|
case "news":
|
||||||
|
sort = 16
|
||||||
}
|
}
|
||||||
return sort
|
return sort
|
||||||
}
|
}
|
||||||
|
@ -431,7 +431,8 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
|
|||||||
"table",
|
"table",
|
||||||
"table-old",
|
"table-old",
|
||||||
"text",
|
"text",
|
||||||
"timeline",
|
"state-timeline",
|
||||||
|
"status-grid",
|
||||||
"timeseries",
|
"timeseries",
|
||||||
"welcome",
|
"welcome",
|
||||||
"xychart",
|
"xychart",
|
||||||
|
@ -42,7 +42,8 @@ const alertmanagerPlugin = async () =>
|
|||||||
|
|
||||||
import * as textPanel from 'app/plugins/panel/text/module';
|
import * as textPanel from 'app/plugins/panel/text/module';
|
||||||
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
||||||
import * as timelinePanel from 'app/plugins/panel/timeline/module';
|
import * as stateTimelinePanel from 'app/plugins/panel/state-timeline/module';
|
||||||
|
import * as statusGridPanel from 'app/plugins/panel/status-grid/module';
|
||||||
import * as graphPanel from 'app/plugins/panel/graph/module';
|
import * as graphPanel from 'app/plugins/panel/graph/module';
|
||||||
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
|
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
|
||||||
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
||||||
@ -90,7 +91,8 @@ const builtInPlugins: any = {
|
|||||||
|
|
||||||
'app/plugins/panel/text/module': textPanel,
|
'app/plugins/panel/text/module': textPanel,
|
||||||
'app/plugins/panel/timeseries/module': timeseriesPanel,
|
'app/plugins/panel/timeseries/module': timeseriesPanel,
|
||||||
'app/plugins/panel/timeline/module': timelinePanel,
|
'app/plugins/panel/state-timeline/module': stateTimelinePanel,
|
||||||
|
'app/plugins/panel/status-grid/module': statusGridPanel,
|
||||||
'app/plugins/panel/graph/module': graphPanel,
|
'app/plugins/panel/graph/module': graphPanel,
|
||||||
'app/plugins/panel/xychart/module': xyChartPanel,
|
'app/plugins/panel/xychart/module': xyChartPanel,
|
||||||
'app/plugins/panel/dashlist/module': dashListPanel,
|
'app/plugins/panel/dashlist/module': dashListPanel,
|
||||||
|
53
public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
Executable file
53
public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { PanelProps } from '@grafana/data';
|
||||||
|
import { useTheme2, ZoomPlugin } from '@grafana/ui';
|
||||||
|
import { TimelineMode, TimelineOptions } from './types';
|
||||||
|
import { TimelineChart } from './TimelineChart';
|
||||||
|
import { prepareTimelineFields } from './utils';
|
||||||
|
|
||||||
|
interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||||
|
data,
|
||||||
|
timeRange,
|
||||||
|
timeZone,
|
||||||
|
options,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onChangeTimeRange,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme2();
|
||||||
|
|
||||||
|
const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, options.mergeValues ?? true), [
|
||||||
|
data,
|
||||||
|
options.mergeValues,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!frames || warn) {
|
||||||
|
return (
|
||||||
|
<div className="panel-empty">
|
||||||
|
<p>{warn ?? 'No data found in response'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimelineChart
|
||||||
|
theme={theme}
|
||||||
|
frames={frames}
|
||||||
|
structureRev={data.structureRev}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={timeZone}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
{...options}
|
||||||
|
// hardcoded
|
||||||
|
mode={TimelineMode.Changes}
|
||||||
|
>
|
||||||
|
{(config) => <ZoomPlugin config={config} onZoom={onChangeTimeRange} />}
|
||||||
|
</TimelineChart>
|
||||||
|
);
|
||||||
|
};
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@ -1,9 +1,9 @@
|
|||||||
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
|
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
|
||||||
import { TimelinePanel } from './TimelinePanel';
|
import { StateTimelinePanel } from './StateTimelinePanel';
|
||||||
import { TimelineOptions, TimelineFieldConfig, TimelineMode, defaultTimelineFieldConfig } from './types';
|
import { TimelineOptions, TimelineFieldConfig, defaultPanelOptions, defaultTimelineFieldConfig } from './types';
|
||||||
import { BarValueVisibility } from '@grafana/ui';
|
import { BarValueVisibility } from '@grafana/ui';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(TimelinePanel)
|
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(StateTimelinePanel)
|
||||||
.useFieldConfig({
|
.useFieldConfig({
|
||||||
standardOptions: {
|
standardOptions: {
|
||||||
[FieldConfigProperty.Color]: {
|
[FieldConfigProperty.Color]: {
|
||||||
@ -41,17 +41,6 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
|||||||
})
|
})
|
||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder) => {
|
||||||
builder
|
builder
|
||||||
.addRadio({
|
|
||||||
path: 'mode',
|
|
||||||
name: 'Mode',
|
|
||||||
defaultValue: TimelineMode.Changes,
|
|
||||||
settings: {
|
|
||||||
options: [
|
|
||||||
{ label: 'State changes', value: TimelineMode.Changes },
|
|
||||||
{ label: 'Periodic samples', value: TimelineMode.Samples },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'showValue',
|
path: 'showValue',
|
||||||
name: 'Show values',
|
name: 'Show values',
|
||||||
@ -62,7 +51,7 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
|||||||
{ value: BarValueVisibility.Never, label: 'Never' },
|
{ value: BarValueVisibility.Never, label: 'Never' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
defaultValue: BarValueVisibility.Always,
|
defaultValue: defaultPanelOptions.showValue,
|
||||||
})
|
})
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'alignValue',
|
path: 'alignValue',
|
||||||
@ -74,29 +63,22 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
|||||||
{ value: 'right', label: 'Right' },
|
{ value: 'right', label: 'Right' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
defaultValue: 'center',
|
defaultValue: defaultPanelOptions.alignValue,
|
||||||
showIf: ({ mode }) => mode === TimelineMode.Changes,
|
})
|
||||||
|
.addBooleanSwitch({
|
||||||
|
path: 'mergeValues',
|
||||||
|
name: 'Merge equal consecutive values',
|
||||||
|
defaultValue: defaultPanelOptions.mergeValues,
|
||||||
})
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
path: 'rowHeight',
|
path: 'rowHeight',
|
||||||
name: 'Row height',
|
name: 'Row height',
|
||||||
defaultValue: 0.9,
|
|
||||||
settings: {
|
settings: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
},
|
},
|
||||||
})
|
defaultValue: defaultPanelOptions.rowHeight,
|
||||||
.addSliderInput({
|
|
||||||
path: 'colWidth',
|
|
||||||
name: 'Column width',
|
|
||||||
defaultValue: 0.9,
|
|
||||||
settings: {
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
showIf: ({ mode }) => mode === TimelineMode.Samples,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//addLegendOptions(builder);
|
//addLegendOptions(builder);
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "panel",
|
"type": "panel",
|
||||||
"name": "Timeline",
|
"name": "State timeline",
|
||||||
"id": "timeline",
|
"id": "state-timeline",
|
||||||
|
|
||||||
"state": "alpha",
|
"state": "alpha",
|
||||||
|
|
@ -4,12 +4,14 @@ import { VizLegendOptions, HideableFieldConfig, BarValueVisibility } from '@graf
|
|||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface TimelineOptions {
|
export interface TimelineOptions {
|
||||||
mode: TimelineMode;
|
mode: TimelineMode; // not in the saved model!
|
||||||
|
|
||||||
legend: VizLegendOptions;
|
legend: VizLegendOptions;
|
||||||
showValue: BarValueVisibility;
|
showValue: BarValueVisibility;
|
||||||
rowHeight: number;
|
rowHeight: number;
|
||||||
colWidth?: number;
|
colWidth?: number;
|
||||||
alignValue: TimelineValueAlignment;
|
alignValue: TimelineValueAlignment;
|
||||||
|
mergeValues?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TimelineValueAlignment = 'center' | 'left' | 'right';
|
export type TimelineValueAlignment = 'center' | 'left' | 'right';
|
||||||
@ -22,6 +24,16 @@ export interface TimelineFieldConfig extends HideableFieldConfig {
|
|||||||
fillOpacity?: number; // 100
|
fillOpacity?: number; // 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const defaultPanelOptions: Partial<TimelineOptions> = {
|
||||||
|
showValue: BarValueVisibility.Always,
|
||||||
|
mergeValues: true,
|
||||||
|
alignValue: 'left',
|
||||||
|
rowHeight: 0.9,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
60
public/app/plugins/panel/state-timeline/utils.test.ts
Normal file
60
public/app/plugins/panel/state-timeline/utils.test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { FieldType, toDataFrame } from '@grafana/data';
|
||||||
|
import { prepareTimelineFields } from './utils';
|
||||||
|
|
||||||
|
describe('prepare timeline graph', () => {
|
||||||
|
it('errors with no time fields', () => {
|
||||||
|
const frames = [
|
||||||
|
toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'a', values: [1, 2, 3] },
|
||||||
|
{ name: 'b', values: ['a', 'b', 'c'] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const info = prepareTimelineFields(frames, true);
|
||||||
|
expect(info.warn).toEqual('Data does not have a time field');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a number, string, or boolean value', () => {
|
||||||
|
const frames = [
|
||||||
|
toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'a', type: FieldType.time, values: [1, 2, 3] },
|
||||||
|
{ name: 'b', type: FieldType.other, values: [{}, {}, {}] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const info = prepareTimelineFields(frames, true);
|
||||||
|
expect(info.warn).toEqual('No graphable fields');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will merge duplicate values', () => {
|
||||||
|
const frames = [
|
||||||
|
toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'a', type: FieldType.time, values: [1, 2, 3, 4, 5, 6, 7] },
|
||||||
|
{ name: 'b', values: [1, 1, undefined, 1, 2, 2, null, 2, 3] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const info = prepareTimelineFields(frames, true);
|
||||||
|
expect(info.warn).toBeUndefined();
|
||||||
|
|
||||||
|
const out = info.frames![0];
|
||||||
|
|
||||||
|
const field = out.fields.find((f) => f.name === 'b');
|
||||||
|
expect(field?.values.toArray()).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
2,
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -8,6 +8,8 @@ import {
|
|||||||
outerJoinDataFrames,
|
outerJoinDataFrames,
|
||||||
Field,
|
Field,
|
||||||
FALLBACK_COLOR,
|
FALLBACK_COLOR,
|
||||||
|
FieldType,
|
||||||
|
ArrayVector,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui';
|
import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui';
|
||||||
import { TimelineCoreOptions, getConfig } from './timeline';
|
import { TimelineCoreOptions, getConfig } from './timeline';
|
||||||
@ -193,3 +195,96 @@ export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
|||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If any sequential duplicate values exist, this will return a new array
|
||||||
|
* with the future values set to undefined.
|
||||||
|
*
|
||||||
|
* in: 1, 1,undefined, 1,2, 2,null,2,3
|
||||||
|
* out: 1,undefined,undefined,undefined,2,undefined,null,2,3
|
||||||
|
*/
|
||||||
|
export function unsetSameFutureValues(values: any[]): any[] | undefined {
|
||||||
|
let prevVal = values[0];
|
||||||
|
let clone: any[] | undefined = undefined;
|
||||||
|
|
||||||
|
for (let i = 1; i < values.length; i++) {
|
||||||
|
let value = values[i];
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
prevVal = null;
|
||||||
|
} else {
|
||||||
|
if (value === prevVal) {
|
||||||
|
if (!clone) {
|
||||||
|
clone = [...values];
|
||||||
|
}
|
||||||
|
clone[i] = undefined;
|
||||||
|
} else if (value != null) {
|
||||||
|
prevVal = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will return a set of frames with only graphable values included
|
||||||
|
export function prepareTimelineFields(
|
||||||
|
series: DataFrame[] | undefined,
|
||||||
|
mergeValues: boolean
|
||||||
|
): { frames?: DataFrame[]; warn?: string } {
|
||||||
|
if (!series?.length) {
|
||||||
|
return { warn: 'No data in response' };
|
||||||
|
}
|
||||||
|
let hasTimeseries = false;
|
||||||
|
const frames: DataFrame[] = [];
|
||||||
|
for (let frame of series) {
|
||||||
|
let isTimeseries = false;
|
||||||
|
let changed = false;
|
||||||
|
const fields: Field[] = [];
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
switch (field.type) {
|
||||||
|
case FieldType.time:
|
||||||
|
isTimeseries = true;
|
||||||
|
hasTimeseries = true;
|
||||||
|
fields.push(field);
|
||||||
|
break;
|
||||||
|
case FieldType.number:
|
||||||
|
case FieldType.boolean:
|
||||||
|
case FieldType.string:
|
||||||
|
if (mergeValues) {
|
||||||
|
let merged = unsetSameFutureValues(field.values.toArray());
|
||||||
|
if (merged) {
|
||||||
|
fields.push({
|
||||||
|
...field,
|
||||||
|
values: new ArrayVector(merged),
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.push(field);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isTimeseries && fields.length > 1) {
|
||||||
|
hasTimeseries = true;
|
||||||
|
if (changed) {
|
||||||
|
frames.push({
|
||||||
|
...frame,
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frames.push(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTimeseries) {
|
||||||
|
return { warn: 'Data does not have a time field' };
|
||||||
|
}
|
||||||
|
if (!frames.length) {
|
||||||
|
return { warn: 'No graphable fields' };
|
||||||
|
}
|
||||||
|
return { frames };
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PanelProps } from '@grafana/data';
|
import { PanelProps } from '@grafana/data';
|
||||||
import { useTheme2, ZoomPlugin } from '@grafana/ui';
|
import { useTheme2, ZoomPlugin } from '@grafana/ui';
|
||||||
import { TimelineOptions } from './types';
|
import { StatusPanelOptions } from './types';
|
||||||
import { TimelineChart } from './TimelineChart';
|
import { TimelineChart } from '../state-timeline/TimelineChart';
|
||||||
|
import { TimelineMode } from '../state-timeline/types';
|
||||||
|
|
||||||
interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
|
interface TimelinePanelProps extends PanelProps<StatusPanelOptions> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
export const StatusGridPanel: React.FC<TimelinePanelProps> = ({
|
||||||
data,
|
data,
|
||||||
timeRange,
|
timeRange,
|
||||||
timeZone,
|
timeZone,
|
||||||
@ -38,6 +39,9 @@ export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
{...options}
|
{...options}
|
||||||
|
// hardcoded
|
||||||
|
mode={TimelineMode.Samples}
|
||||||
|
alignValue="center"
|
||||||
>
|
>
|
||||||
{(config) => <ZoomPlugin config={config} onZoom={onChangeTimeRange} />}
|
{(config) => <ZoomPlugin config={config} onZoom={onChangeTimeRange} />}
|
||||||
</TimelineChart>
|
</TimelineChart>
|
42
public/app/plugins/panel/status-grid/img/status.svg
Normal file
42
public/app/plugins/panel/status-grid/img/status.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 80.09 80.09">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:#3865ab;}.cls-2{fill:#84aff1;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}</style>
|
||||||
|
<linearGradient id="linear-gradient" y1="19.02" x2="66.08" y2="19.02" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#f2cc0c" />
|
||||||
|
<stop offset="1" stop-color="#ff9830" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="linear-gradient-2" y1="54.06" x2="66.08" y2="54.06" xlink:href="#linear-gradient" />
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_2" data-name="Layer 2">
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<rect class="cls-1" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="28.03" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="42.05" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-3" x="56.06" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="70.08" y="14.02" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="28.03" y="28.03" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="42.05" y="28.03" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="56.06" y="28.03" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-3" x="70.08" y="28.03" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" y="42.05" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="42.05" y="42.05" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="56.06" y="42.05" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="70.08" y="42.05" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-3" x="14.02" y="56.06" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="14.02" y="70.08" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="28.03" y="70.08" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-3" x="42.05" y="70.08" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="14.02" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" y="28.03" width="10.02" height="10.02" rx="1" transform="translate(38.05 28.03) rotate(90)" />
|
||||||
|
<rect class="cls-1" x="70.08" y="56.06" width="10.02" height="10.02" rx="1" transform="translate(136.16 -14.02) rotate(90)" />
|
||||||
|
<rect class="cls-1" x="70.08" width="10.02" height="10.02" rx="1" transform="translate(80.09 -70.08) rotate(90)" />
|
||||||
|
<path class="cls-1" d="M9,24H1a1,1,0,0,1-1-1V15a1,1,0,0,1,1-1H9a1,1,0,0,1,1,1v8A1,1,0,0,1,9,24Zm15-1V15a1,1,0,0,0-1-1H15a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,24,23Zm14,0V15a1,1,0,0,0-1-1H29a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,38.05,23Zm28,0V15a1,1,0,0,0-1-1h-8a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,66.08,23Zm-15,1h-8a1,1,0,0,1-1-1V15a1,1,0,0,1,1-1h8a1,1,0,0,1,1,1v8A1,1,0,0,1,51.06,24Z" />
|
||||||
|
<rect class="cls-1" x="14.02" y="28.03" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="28.03" y="56.06" width="10.02" height="10.02" rx="1" />
|
||||||
|
<path class="cls-1" d="M37.05,52.06H29a1,1,0,0,1-1-1v-8a1,1,0,0,1,1-1h8a1,1,0,0,1,1,1v8A1,1,0,0,1,37.05,52.06Zm-14,0H15a1,1,0,0,1-1-1v-8a1,1,0,0,1,1-1h8a1,1,0,0,1,1,1v8A1,1,0,0,1,23,52.06Zm-13,13v-8a1,1,0,0,0-1-1H1a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1H9A1,1,0,0,0,10,65.08Zm42,0v-8a1,1,0,0,0-1-1h-8a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,52.06,65.08Zm14,0v-8a1,1,0,0,0-1-1h-8a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,66.08,65.08Z" />
|
||||||
|
<rect class="cls-1" y="70.08" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="56.06" y="70.08" width="10.02" height="10.02" rx="1" />
|
||||||
|
<rect class="cls-1" x="70.08" y="70.08" width="10.02" height="10.02" rx="1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
78
public/app/plugins/panel/status-grid/module.tsx
Executable file
78
public/app/plugins/panel/status-grid/module.tsx
Executable file
@ -0,0 +1,78 @@
|
|||||||
|
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
|
||||||
|
import { StatusGridPanel } from './StatusGridPanel';
|
||||||
|
import { StatusPanelOptions, StatusFieldConfig, defaultStatusFieldConfig } from './types';
|
||||||
|
import { BarValueVisibility } from '@grafana/ui';
|
||||||
|
|
||||||
|
export const plugin = new PanelPlugin<StatusPanelOptions, StatusFieldConfig>(StatusGridPanel)
|
||||||
|
.useFieldConfig({
|
||||||
|
standardOptions: {
|
||||||
|
[FieldConfigProperty.Color]: {
|
||||||
|
settings: {
|
||||||
|
byValueSupport: true,
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
mode: FieldColorModeId.PaletteClassic,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useCustomConfig: (builder) => {
|
||||||
|
builder
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'lineWidth',
|
||||||
|
name: 'Line width',
|
||||||
|
defaultValue: defaultStatusFieldConfig.lineWidth,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'fillOpacity',
|
||||||
|
name: 'Fill opacity',
|
||||||
|
defaultValue: defaultStatusFieldConfig.fillOpacity,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.setPanelOptions((builder) => {
|
||||||
|
builder
|
||||||
|
.addRadio({
|
||||||
|
path: 'showValue',
|
||||||
|
name: 'Show values',
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
//{ value: BarValueVisibility.Auto, label: 'Auto' },
|
||||||
|
{ value: BarValueVisibility.Always, label: 'Always' },
|
||||||
|
{ value: BarValueVisibility.Never, label: 'Never' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: BarValueVisibility.Always,
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'rowHeight',
|
||||||
|
name: 'Row height',
|
||||||
|
defaultValue: 0.9,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.01,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'colWidth',
|
||||||
|
name: 'Column width',
|
||||||
|
defaultValue: 0.9,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.01,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//addLegendOptions(builder);
|
||||||
|
});
|
19
public/app/plugins/panel/status-grid/plugin.json
Executable file
19
public/app/plugins/panel/status-grid/plugin.json
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "panel",
|
||||||
|
"name": "Status grid",
|
||||||
|
"id": "status-grid",
|
||||||
|
|
||||||
|
"state": "alpha",
|
||||||
|
|
||||||
|
"info": {
|
||||||
|
"description": "System status map",
|
||||||
|
"author": {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "https://grafana.com"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/status.svg",
|
||||||
|
"large": "img/status.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
public/app/plugins/panel/status-grid/types.ts
Normal file
27
public/app/plugins/panel/status-grid/types.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { VizLegendOptions, HideableFieldConfig, BarValueVisibility } from '@grafana/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export interface StatusPanelOptions {
|
||||||
|
legend: VizLegendOptions;
|
||||||
|
showValue: BarValueVisibility;
|
||||||
|
rowHeight: number;
|
||||||
|
colWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export interface StatusFieldConfig extends HideableFieldConfig {
|
||||||
|
lineWidth?: number; // 0
|
||||||
|
fillOpacity?: number; // 100
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const defaultStatusFieldConfig: StatusFieldConfig = {
|
||||||
|
lineWidth: 1,
|
||||||
|
fillOpacity: 70,
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user