Timeline: split "periodic" mode into its own panel (#34171)

This commit is contained in:
Ryan McKinley 2021-05-17 13:00:04 -07:00 committed by GitHub
parent c630393cf4
commit de5cd4a7d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 434 additions and 56 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

@ -1,7 +1,7 @@
{ {
"type": "panel", "type": "panel",
"name": "Timeline", "name": "State timeline",
"id": "timeline", "id": "state-timeline",
"state": "alpha", "state": "alpha",

View File

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

View 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,
]
`);
});
});

View File

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

View File

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

View 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

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

View 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"
}
}
}

View 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,
};