mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -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",
|
||||
"type": "timeline"
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
@ -204,7 +204,7 @@
|
||||
}
|
||||
],
|
||||
"title": "State changes with boolean values",
|
||||
"type": "timeline"
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
@ -298,7 +298,7 @@
|
||||
}
|
||||
],
|
||||
"title": "State changes with nulls",
|
||||
"type": "timeline"
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
@ -388,8 +388,8 @@
|
||||
"stringInput": ""
|
||||
}
|
||||
],
|
||||
"title": "\"Periodic samples\" Mode",
|
||||
"type": "timeline"
|
||||
"title": "Status map",
|
||||
"type": "status-grid"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
|
@ -188,7 +188,7 @@
|
||||
}
|
||||
],
|
||||
"title": "\"State changes\" Mode",
|
||||
"type": "timeline"
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
@ -253,7 +253,7 @@
|
||||
}
|
||||
],
|
||||
"title": "\"State changes\" Mode (strings & booleans)",
|
||||
"type": "timeline"
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
@ -331,8 +331,8 @@
|
||||
"stringInput": ""
|
||||
}
|
||||
],
|
||||
"title": "\"Periodic samples\" Mode",
|
||||
"type": "timeline"
|
||||
"title": "Status map view",
|
||||
"type": "status-grid"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
|
@ -155,7 +155,8 @@ export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined
|
||||
}
|
||||
|
||||
// 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 ?? {};
|
||||
if (frame.name) {
|
||||
@ -285,7 +286,7 @@ export function join(tables: AlignedData[], nullModes?: number[][]) {
|
||||
let yVal = ys[i];
|
||||
let alignedIdx = xIdxs.get(xs[i]);
|
||||
|
||||
if (yVal == null) {
|
||||
if (yVal === null) {
|
||||
if (nullMode !== NULL_REMOVE) {
|
||||
yVals[alignedIdx] = yVal;
|
||||
|
||||
|
@ -274,20 +274,22 @@ func getPanelSort(id string) int {
|
||||
sort = 7
|
||||
case "piechart":
|
||||
sort = 8
|
||||
case "timeline":
|
||||
case "state-timeline":
|
||||
sort = 9
|
||||
case "heatmap":
|
||||
sort = 10
|
||||
case "graph":
|
||||
case "status-grid":
|
||||
sort = 11
|
||||
case "text":
|
||||
case "graph":
|
||||
sort = 12
|
||||
case "alertlist":
|
||||
case "text":
|
||||
sort = 13
|
||||
case "dashlist":
|
||||
case "alertlist":
|
||||
sort = 14
|
||||
case "news":
|
||||
case "dashlist":
|
||||
sort = 15
|
||||
case "news":
|
||||
sort = 16
|
||||
}
|
||||
return sort
|
||||
}
|
||||
|
@ -431,7 +431,8 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
|
||||
"table",
|
||||
"table-old",
|
||||
"text",
|
||||
"timeline",
|
||||
"state-timeline",
|
||||
"status-grid",
|
||||
"timeseries",
|
||||
"welcome",
|
||||
"xychart",
|
||||
|
@ -42,7 +42,8 @@ const alertmanagerPlugin = async () =>
|
||||
|
||||
import * as textPanel from 'app/plugins/panel/text/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 xyChartPanel from 'app/plugins/panel/xychart/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/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/xychart/module': xyChartPanel,
|
||||
'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 { TimelinePanel } from './TimelinePanel';
|
||||
import { TimelineOptions, TimelineFieldConfig, TimelineMode, defaultTimelineFieldConfig } from './types';
|
||||
import { StateTimelinePanel } from './StateTimelinePanel';
|
||||
import { TimelineOptions, TimelineFieldConfig, defaultPanelOptions, defaultTimelineFieldConfig } from './types';
|
||||
import { BarValueVisibility } from '@grafana/ui';
|
||||
|
||||
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(TimelinePanel)
|
||||
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(StateTimelinePanel)
|
||||
.useFieldConfig({
|
||||
standardOptions: {
|
||||
[FieldConfigProperty.Color]: {
|
||||
@ -41,17 +41,6 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
||||
})
|
||||
.setPanelOptions((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({
|
||||
path: 'showValue',
|
||||
name: 'Show values',
|
||||
@ -62,7 +51,7 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
||||
{ value: BarValueVisibility.Never, label: 'Never' },
|
||||
],
|
||||
},
|
||||
defaultValue: BarValueVisibility.Always,
|
||||
defaultValue: defaultPanelOptions.showValue,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'alignValue',
|
||||
@ -74,29 +63,22 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
||||
{ value: 'right', label: 'Right' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'center',
|
||||
showIf: ({ mode }) => mode === TimelineMode.Changes,
|
||||
defaultValue: defaultPanelOptions.alignValue,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'mergeValues',
|
||||
name: 'Merge equal consecutive values',
|
||||
defaultValue: defaultPanelOptions.mergeValues,
|
||||
})
|
||||
.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,
|
||||
},
|
||||
showIf: ({ mode }) => mode === TimelineMode.Samples,
|
||||
defaultValue: defaultPanelOptions.rowHeight,
|
||||
});
|
||||
|
||||
//addLegendOptions(builder);
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Timeline",
|
||||
"id": "timeline",
|
||||
"name": "State timeline",
|
||||
"id": "state-timeline",
|
||||
|
||||
"state": "alpha",
|
||||
|
@ -4,12 +4,14 @@ import { VizLegendOptions, HideableFieldConfig, BarValueVisibility } from '@graf
|
||||
* @alpha
|
||||
*/
|
||||
export interface TimelineOptions {
|
||||
mode: TimelineMode;
|
||||
mode: TimelineMode; // not in the saved model!
|
||||
|
||||
legend: VizLegendOptions;
|
||||
showValue: BarValueVisibility;
|
||||
rowHeight: number;
|
||||
colWidth?: number;
|
||||
alignValue: TimelineValueAlignment;
|
||||
mergeValues?: boolean;
|
||||
}
|
||||
|
||||
export type TimelineValueAlignment = 'center' | 'left' | 'right';
|
||||
@ -22,6 +24,16 @@ export interface TimelineFieldConfig extends HideableFieldConfig {
|
||||
fillOpacity?: number; // 100
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const defaultPanelOptions: Partial<TimelineOptions> = {
|
||||
showValue: BarValueVisibility.Always,
|
||||
mergeValues: true,
|
||||
alignValue: 'left',
|
||||
rowHeight: 0.9,
|
||||
};
|
||||
|
||||
/**
|
||||
* @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,
|
||||
Field,
|
||||
FALLBACK_COLOR,
|
||||
FieldType,
|
||||
ArrayVector,
|
||||
} from '@grafana/data';
|
||||
import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui';
|
||||
import { TimelineCoreOptions, getConfig } from './timeline';
|
||||
@ -193,3 +195,96 @@ export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
||||
}
|
||||
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 { PanelProps } from '@grafana/data';
|
||||
import { useTheme2, ZoomPlugin } from '@grafana/ui';
|
||||
import { TimelineOptions } from './types';
|
||||
import { TimelineChart } from './TimelineChart';
|
||||
import { StatusPanelOptions } from './types';
|
||||
import { TimelineChart } from '../state-timeline/TimelineChart';
|
||||
import { TimelineMode } from '../state-timeline/types';
|
||||
|
||||
interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
|
||||
interface TimelinePanelProps extends PanelProps<StatusPanelOptions> {}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
export const StatusGridPanel: React.FC<TimelinePanelProps> = ({
|
||||
data,
|
||||
timeRange,
|
||||
timeZone,
|
||||
@ -38,6 +39,9 @@ export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
width={width}
|
||||
height={height}
|
||||
{...options}
|
||||
// hardcoded
|
||||
mode={TimelineMode.Samples}
|
||||
alignValue="center"
|
||||
>
|
||||
{(config) => <ZoomPlugin config={config} onZoom={onChangeTimeRange} />}
|
||||
</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