Merge pull request #15466 from grafana/bar-gauge-poc
New Bar Gauge panel
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options-gauge": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"options": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "#1F78C1",
|
||||
"index": 5,
|
||||
"value": 96.875
|
||||
},
|
||||
{
|
||||
"color": "#E24D42",
|
||||
"index": 4,
|
||||
"value": 93.75
|
||||
},
|
||||
{
|
||||
"color": "#EF843C",
|
||||
"index": 3,
|
||||
"value": 87.5
|
||||
},
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"index": 2,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"unit": "none",
|
||||
"valueMappings": [
|
||||
{
|
||||
"from": "50",
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
"text": "Hello :) ",
|
||||
"to": "90",
|
||||
"type": 2,
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Horizontal with range variable",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 2,
|
||||
"links": [],
|
||||
"options-gauge": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"options": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Repeat horizontal",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 14,
|
||||
"w": 5,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 4,
|
||||
"links": [],
|
||||
"options-gauge": {
|
||||
"decimals": 0,
|
||||
"maxValue": "200",
|
||||
"minValue": 0,
|
||||
"options": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "max",
|
||||
"suffix": "",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"index": 2,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Vertical",
|
||||
"type": "gauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 17,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Multi series gauges",
|
||||
"uid": "szkuR1umk",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number, text } from '@storybook/addon-knobs';
|
||||
import { BarGauge } from './BarGauge';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
const getKnobs = () => {
|
||||
return {
|
||||
value: number('value', 70),
|
||||
minValue: number('minValue', 0),
|
||||
maxValue: number('maxValue', 100),
|
||||
threshold1Value: number('threshold1Value', 40),
|
||||
threshold1Color: text('threshold1Color', 'orange'),
|
||||
threshold2Value: number('threshold2Value', 60),
|
||||
threshold2Color: text('threshold2Color', 'red'),
|
||||
unit: text('unit', 'ms'),
|
||||
decimals: number('decimals', 1),
|
||||
};
|
||||
};
|
||||
|
||||
const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
|
||||
|
||||
BarGaugeStories.addDecorator(withCenteredStory);
|
||||
|
||||
BarGaugeStories.add('Vertical, with basic thresholds', () => {
|
||||
const {
|
||||
value,
|
||||
minValue,
|
||||
maxValue,
|
||||
threshold1Color,
|
||||
threshold2Color,
|
||||
threshold1Value,
|
||||
threshold2Value,
|
||||
unit,
|
||||
decimals,
|
||||
} = getKnobs();
|
||||
|
||||
return renderComponentWithTheme(BarGauge, {
|
||||
width: 200,
|
||||
height: 400,
|
||||
value: value,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
unit: unit,
|
||||
prefix: '',
|
||||
postfix: '',
|
||||
decimals: decimals,
|
||||
thresholds: [
|
||||
{ index: 0, value: -Infinity, color: 'green' },
|
||||
{ index: 1, value: threshold1Value, color: threshold1Color },
|
||||
{ index: 1, value: threshold2Value, color: threshold2Color },
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BarGauge, Props } from './BarGauge';
|
||||
import { VizOrientation } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||
unit: 'none',
|
||||
height: 300,
|
||||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
theme: getTheme(),
|
||||
orientation: VizOrientation.Horizontal,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<BarGauge {...props} />);
|
||||
const instance = wrapper.instance() as BarGauge;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Get font color', () => {
|
||||
it('should get first threshold color when only one threshold', () => {
|
||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||
|
||||
expect(instance.getValueColors().value).toEqual('#7EB26D');
|
||||
});
|
||||
|
||||
it('should get the threshold color if value is same as a threshold', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 10, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(instance.getValueColors().value).toEqual('#EAB839');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Render BarGauge with basic options', () => {
|
||||
it('should render', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
// Library
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Utils
|
||||
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
|
||||
|
||||
// Types
|
||||
import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
|
||||
|
||||
const BAR_SIZE_RATIO = 0.8;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
height: number;
|
||||
unit: string;
|
||||
width: number;
|
||||
thresholds: Threshold[];
|
||||
valueMappings: ValueMapping[];
|
||||
value: TimeSeriesValue;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
orientation: VizOrientation;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
decimals?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* This visualization is still in POC state, needed more tests & better structure
|
||||
*/
|
||||
export class BarGauge extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
value: 100,
|
||||
unit: 'none',
|
||||
orientation: VizOrientation.Horizontal,
|
||||
thresholds: [],
|
||||
valueMappings: [],
|
||||
};
|
||||
|
||||
getNumericValue(): number {
|
||||
if (Number.isFinite(this.props.value as number)) {
|
||||
return this.props.value as number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getValueColors(): BarColors {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
|
||||
const activeThreshold = getThresholdForValue(thresholds, value);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
return {
|
||||
value: color,
|
||||
border: color,
|
||||
bar: tinycolor(color)
|
||||
.setAlpha(0.3)
|
||||
.toRgbString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: getColorFromHexRgbOrName('gray', theme.type),
|
||||
bar: getColorFromHexRgbOrName('gray', theme.type),
|
||||
border: getColorFromHexRgbOrName('gray', theme.type),
|
||||
};
|
||||
}
|
||||
|
||||
getCellColor(positionValue: TimeSeriesValue): string {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
const activeThreshold = getThresholdForValue(thresholds, positionValue);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value)) {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.15)
|
||||
.toRgbString();
|
||||
} else {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.7)
|
||||
.toRgbString();
|
||||
}
|
||||
}
|
||||
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
getValueStyles(value: string, color: string, width: number): CSSProperties {
|
||||
const guess = width / (value.length * 1.1);
|
||||
const fontSize = Math.min(Math.max(guess, 14), 40);
|
||||
|
||||
return {
|
||||
color: color,
|
||||
fontSize: fontSize + 'px',
|
||||
};
|
||||
}
|
||||
|
||||
renderVerticalBar(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width } = this.props;
|
||||
|
||||
const maxHeight = height * BAR_SIZE_RATIO;
|
||||
const barHeight = Math.max(valuePercent * maxHeight, 0);
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width);
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
};
|
||||
|
||||
const barStyles: CSSProperties = {
|
||||
height: `${barHeight}px`,
|
||||
width: `${width}px`,
|
||||
backgroundColor: colors.bar,
|
||||
borderTop: `1px solid ${colors.border}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
<div style={barStyles} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHorizontalBar(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width } = this.props;
|
||||
|
||||
const maxWidth = width * BAR_SIZE_RATIO;
|
||||
const barWidth = Math.max(valuePercent * maxWidth, 0);
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
|
||||
|
||||
valueStyles.marginLeft = '8px';
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const barStyles = {
|
||||
height: `${height}px`,
|
||||
width: `${barWidth}px`,
|
||||
backgroundColor: colors.bar,
|
||||
borderRight: `1px solid ${colors.border}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={barStyles} />
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHorizontalLCD(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width, maxValue, minValue } = this.props;
|
||||
|
||||
const valueRange = maxValue - minValue;
|
||||
const maxWidth = width * BAR_SIZE_RATIO;
|
||||
const cellSpacing = 4;
|
||||
const cellCount = 30;
|
||||
const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount;
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
|
||||
valueStyles.marginLeft = '8px';
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const cells: JSX.Element[] = [];
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const currentValue = (valueRange / cellCount) * i;
|
||||
const cellColor = this.getCellColor(currentValue);
|
||||
const cellStyles: CSSProperties = {
|
||||
width: `${cellWidth}px`,
|
||||
backgroundColor: cellColor,
|
||||
marginRight: '4px',
|
||||
height: `${height}px`,
|
||||
borderRadius: '2px',
|
||||
};
|
||||
|
||||
cells.push(<div style={cellStyles} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
{cells}
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { maxValue, minValue, orientation, unit, decimals } = this.props;
|
||||
|
||||
const numericValue = this.getNumericValue();
|
||||
const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
|
||||
|
||||
const formatFunc = getValueFormat(unit);
|
||||
const valueFormatted = formatFunc(numericValue, decimals);
|
||||
const vertical = orientation === 'vertical';
|
||||
|
||||
return vertical
|
||||
? this.renderVerticalBar(valueFormatted, valuePercent)
|
||||
: this.renderHorizontalLCD(valueFormatted, valuePercent);
|
||||
}
|
||||
}
|
||||
|
||||
interface BarColors {
|
||||
value: string;
|
||||
bar: string;
|
||||
border: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.bar-gauge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bar-gauge__value {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render BarGauge with basic options should render 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"height": "300px",
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="bar-gauge__value"
|
||||
style={
|
||||
Object {
|
||||
"color": "#7EB26D",
|
||||
"fontSize": "27.272727272727263px",
|
||||
"marginLeft": "8px",
|
||||
}
|
||||
}
|
||||
>
|
||||
25
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { getMappedValue } from '../../utils/valueMappings';
|
||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||
import { Themeable, GrafanaThemeType } from '../../types/theme';
|
||||
import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
|
||||
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
||||
|
||||
type TimeSeriesValue = string | number | null;
|
||||
import { ValueMapping, Threshold, GrafanaThemeType } from '../../types';
|
||||
import { getMappedValue } from '../../utils/valueMappings';
|
||||
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
|
||||
import { Themeable } from '../../index';
|
||||
|
||||
type GaugeValue = string | number | null;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
decimals?: number | null;
|
||||
@@ -51,7 +51,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
formatValue(value: TimeSeriesValue) {
|
||||
formatValue(value: GaugeValue) {
|
||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||
|
||||
if (isNaN(value as number)) {
|
||||
@@ -72,26 +72,16 @@ export class Gauge extends PureComponent<Props> {
|
||||
return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
|
||||
}
|
||||
|
||||
getFontColor(value: TimeSeriesValue) {
|
||||
getFontColor(value: GaugeValue): string {
|
||||
const { thresholds, theme } = this.props;
|
||||
|
||||
if (thresholds.length === 1) {
|
||||
return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
|
||||
const activeThreshold = getThresholdForValue(thresholds, value);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
if (atThreshold) {
|
||||
return getColorFromHexRgbOrName(atThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
|
||||
|
||||
if (belowThreshold.length > 0) {
|
||||
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
|
||||
return getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
return BasicGaugeColor.Red;
|
||||
return '';
|
||||
}
|
||||
|
||||
getFormattedThresholds() {
|
||||
@@ -183,19 +173,15 @@ export class Gauge extends PureComponent<Props> {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className="singlestat-panel">
|
||||
<div
|
||||
style={{
|
||||
height: `${height * 0.9}px`,
|
||||
width: `${Math.min(width, height * 1.3)}px`,
|
||||
top: '10px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: `${Math.min(height, width * 1.3)}px`,
|
||||
width: `${Math.min(width, height * 1.3)}px`,
|
||||
top: '10px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Gauge;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { SingleStatValueInfo, VizOrientation } from '../../types';
|
||||
|
||||
interface RenderProps {
|
||||
vizWidth: number;
|
||||
vizHeight: number;
|
||||
valueInfo: SingleStatValueInfo;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: (renderProps: RenderProps) => JSX.Element | JSX.Element[];
|
||||
height: number;
|
||||
width: number;
|
||||
values: SingleStatValueInfo[];
|
||||
orientation: VizOrientation;
|
||||
}
|
||||
|
||||
const SPACE_BETWEEN = 10;
|
||||
|
||||
export class VizRepeater extends PureComponent<Props> {
|
||||
getOrientation(): VizOrientation {
|
||||
const { orientation, width, height } = this.props;
|
||||
|
||||
if (orientation === VizOrientation.Auto) {
|
||||
if (width > height) {
|
||||
return VizOrientation.Vertical;
|
||||
} else {
|
||||
return VizOrientation.Horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
return orientation;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, height, values, width } = this.props;
|
||||
const orientation = this.getOrientation();
|
||||
|
||||
const itemStyles: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const repeaterStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
let vizHeight = height;
|
||||
let vizWidth = width;
|
||||
|
||||
if (orientation === VizOrientation.Horizontal) {
|
||||
repeaterStyle.flexDirection = 'column';
|
||||
itemStyles.margin = `${SPACE_BETWEEN / 2}px 0`;
|
||||
vizWidth = width;
|
||||
vizHeight = height / values.length - SPACE_BETWEEN;
|
||||
} else {
|
||||
repeaterStyle.flexDirection = 'row';
|
||||
itemStyles.margin = `0 ${SPACE_BETWEEN / 2}px`;
|
||||
vizHeight = height;
|
||||
vizWidth = width / values.length - SPACE_BETWEEN;
|
||||
}
|
||||
|
||||
itemStyles.width = `${vizWidth}px`;
|
||||
itemStyles.height = `${vizHeight}px`;
|
||||
|
||||
return (
|
||||
<div style={repeaterStyle}>
|
||||
{values.map((valueInfo, index) => {
|
||||
return (
|
||||
<div key={index} style={itemStyles}>
|
||||
{children({ vizHeight, vizWidth, valueInfo })}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,3 +11,4 @@
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import 'EmptySearchResult/EmptySearchResult';
|
||||
@import 'FormField/FormField';
|
||||
@import 'BarGauge/BarGauge';
|
||||
|
||||
@@ -19,11 +19,15 @@ export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
export { Switch } from './Switch/Switch';
|
||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||
|
||||
// Visualizations
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { BarGauge } from './BarGauge/BarGauge';
|
||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||
|
||||
@@ -48,10 +48,7 @@ export enum NullValueMode {
|
||||
}
|
||||
|
||||
/** View model projection of many time series */
|
||||
export interface TimeSeriesVMs {
|
||||
[index: number]: TimeSeriesVM;
|
||||
length: number;
|
||||
}
|
||||
export type TimeSeriesVMs = TimeSeriesVM[];
|
||||
|
||||
export interface Column {
|
||||
text: string;
|
||||
@@ -69,3 +66,12 @@ export interface TableData {
|
||||
type: string;
|
||||
columnMap: any;
|
||||
}
|
||||
|
||||
export type SingleStatValue = number | string | null;
|
||||
|
||||
/*
|
||||
* So we can add meta info like tags & series name
|
||||
*/
|
||||
export interface SingleStatValueInfo {
|
||||
value: SingleStatValue;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './datasource';
|
||||
export * from './theme';
|
||||
export * from './threshold';
|
||||
|
||||
@@ -26,10 +26,13 @@ export interface PanelEditorProps<T = any> {
|
||||
onOptionsChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export type PreservePanelOptionsHandler<TOptions = any> = (pluginId: string, prevOptions: any) => Partial<TOptions>;
|
||||
|
||||
export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
preserveOptions?: PreservePanelOptionsHandler<TOptions>;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
@@ -42,6 +45,10 @@ export class ReactPanelPlugin<TOptions = any> {
|
||||
setDefaults(defaults: TOptions) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
setPreserveOptionsHandler(handler: PreservePanelOptionsHandler<TOptions>) {
|
||||
this.preserveOptions = handler;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PanelSize {
|
||||
@@ -58,17 +65,6 @@ export interface PanelMenuItem {
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export enum BasicGaugeColor {
|
||||
Green = '#299c46',
|
||||
Red = '#d44a3a',
|
||||
}
|
||||
|
||||
export enum MappingType {
|
||||
ValueToText = 1,
|
||||
RangeToText = 2,
|
||||
@@ -91,3 +87,9 @@ export interface RangeMap extends BaseMap {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export enum VizOrientation {
|
||||
Auto = 'auto',
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from './processTimeSeries';
|
||||
export * from './singlestat';
|
||||
export * from './valueFormats/valueFormats';
|
||||
export * from './colors';
|
||||
export * from './namedColorsPalette';
|
||||
export * from './thresholds';
|
||||
export * from './string';
|
||||
export * from './deprecationWarning';
|
||||
export { getMappedValue } from './valueMappings';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
|
||||
import { processTimeSeries } from './processTimeSeries';
|
||||
|
||||
export interface SingleStatProcessingOptions {
|
||||
panelData: PanelData;
|
||||
stat: string;
|
||||
}
|
||||
|
||||
//
|
||||
// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
|
||||
//
|
||||
export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
|
||||
const { panelData, stat } = options;
|
||||
|
||||
if (panelData.timeSeries) {
|
||||
const timeSeries = processTimeSeries({
|
||||
timeSeries: panelData.timeSeries,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
return timeSeries.map((series, index) => {
|
||||
const value = stat !== 'name' ? series.stats[stat] : series.label;
|
||||
|
||||
return {
|
||||
value: value,
|
||||
};
|
||||
});
|
||||
} else if (panelData.tableData) {
|
||||
throw { message: 'Panel data not supported' };
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Threshold } from '../types';
|
||||
|
||||
export function getThresholdForValue(
|
||||
thresholds: Threshold[],
|
||||
value: number | null | string | undefined
|
||||
): Threshold | null {
|
||||
if (thresholds.length === 1) {
|
||||
return thresholds[0];
|
||||
}
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
if (atThreshold) {
|
||||
return atThreshold;
|
||||
}
|
||||
|
||||
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
|
||||
if (belowThreshold.length > 0) {
|
||||
const nearestThreshold = belowThreshold.sort((t1: Threshold, t2: Threshold) => t2.value - t1.value)[0];
|
||||
return nearestThreshold;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -192,16 +192,18 @@ func getPanelSort(id string) int {
|
||||
sort = 2
|
||||
case "gauge":
|
||||
sort = 3
|
||||
case "table":
|
||||
case "bargauge":
|
||||
sort = 4
|
||||
case "text":
|
||||
case "table":
|
||||
sort = 5
|
||||
case "heatmap":
|
||||
case "text":
|
||||
sort = 6
|
||||
case "alertlist":
|
||||
case "heatmap":
|
||||
sort = 7
|
||||
case "dashlist":
|
||||
case "alertlist":
|
||||
sort = 8
|
||||
case "dashlist":
|
||||
sort = 9
|
||||
}
|
||||
return sort
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const provideConfig = (component: React.ComponentType<any>) => {
|
||||
|
||||
export const getCurrentThemeName = () =>
|
||||
config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
|
||||
|
||||
export const getCurrentTheme = () => getTheme(getCurrentThemeName());
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
@@ -76,21 +76,26 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
// unmount angular panel
|
||||
this.cleanUpAngularPanel();
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
this.props.panel.changeType(pluginId, fromAngularPanel);
|
||||
}
|
||||
|
||||
if (plugin.exports) {
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
} else {
|
||||
if (!plugin.exports) {
|
||||
try {
|
||||
plugin.exports = await importPluginModule(plugin.module);
|
||||
} catch (e) {
|
||||
plugin = getPanelPluginNotFound(pluginId);
|
||||
}
|
||||
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
}
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
if (fromAngularPanel) {
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
panel.destroy();
|
||||
|
||||
this.props.panel.changeType(pluginId);
|
||||
} else {
|
||||
panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
|
||||
|
||||
// Types
|
||||
import { PanelPlugin, AppNotificationSeverity } from 'app/types';
|
||||
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
@@ -19,15 +22,13 @@ class PanelPluginNotFound extends PureComponent<Props> {
|
||||
const style = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center' as 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div className="alert alert-error" style={{ margin: '0 auto' }}>
|
||||
Panel plugin with id {this.props.pluginId} could not be found
|
||||
</div>
|
||||
<AlertBox severity={AppNotificationSeverity.Error} title={`Panel plugin not found: ${this.props.pluginId}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { PanelModel } from './PanelModel';
|
||||
|
||||
describe('PanelModel', () => {
|
||||
describe('when creating new panel model', () => {
|
||||
@@ -66,7 +65,7 @@ describe('PanelModel', () => {
|
||||
|
||||
describe('when changing panel type', () => {
|
||||
beforeEach(() => {
|
||||
model.changeType('graph', true);
|
||||
model.changeType('graph');
|
||||
model.alert = { id: 2 };
|
||||
});
|
||||
|
||||
@@ -75,12 +74,12 @@ describe('PanelModel', () => {
|
||||
});
|
||||
|
||||
it('should restore table properties when changing back', () => {
|
||||
model.changeType('table', true);
|
||||
model.changeType('table');
|
||||
expect(model.showColumns).toBe(true);
|
||||
});
|
||||
|
||||
it('should remove alert rule when changing type that does not support it', () => {
|
||||
model.changeType('table', true);
|
||||
model.changeType('table');
|
||||
expect(model.alert).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,10 +229,6 @@ export class PanelModel {
|
||||
}, {});
|
||||
}
|
||||
|
||||
private saveCurrentPanelOptions() {
|
||||
this.cachedPluginOptions[this.type] = this.getOptionsToRemember();
|
||||
}
|
||||
|
||||
private restorePanelOptions(pluginId: string) {
|
||||
const prevOptions = this.cachedPluginOptions[pluginId] || {};
|
||||
|
||||
@@ -241,14 +237,11 @@ export class PanelModel {
|
||||
});
|
||||
}
|
||||
|
||||
changeType(pluginId: string, fromAngularPanel: boolean) {
|
||||
this.saveCurrentPanelOptions();
|
||||
this.type = pluginId;
|
||||
changeType(pluginId: string, preserveOptions?: any) {
|
||||
const oldOptions: any = this.getOptionsToRemember();
|
||||
const oldPluginId = this.type;
|
||||
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
if (fromAngularPanel) {
|
||||
this.destroy();
|
||||
}
|
||||
this.type = pluginId;
|
||||
|
||||
// remove panel type specific options
|
||||
for (const key of _.keys(this)) {
|
||||
@@ -259,7 +252,13 @@ export class PanelModel {
|
||||
delete this[key];
|
||||
}
|
||||
|
||||
this.cachedPluginOptions[oldPluginId] = oldOptions;
|
||||
this.restorePanelOptions(pluginId);
|
||||
|
||||
if (preserveOptions && oldOptions) {
|
||||
this.options = this.options || {};
|
||||
Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options));
|
||||
}
|
||||
}
|
||||
|
||||
addQuery(query?: Partial<DataQuery>) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import * as table2Panel from 'app/plugins/panel/table2/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
|
||||
|
||||
const builtInPlugins = {
|
||||
'app/plugins/datasource/graphite/module': graphitePlugin,
|
||||
@@ -58,6 +59,7 @@ const builtInPlugins = {
|
||||
'app/plugins/panel/singlestat/module': singlestatPanel,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
'app/plugins/panel/bargauge/module': barGaugePanel,
|
||||
};
|
||||
|
||||
export default builtInPlugins;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { processSingleStatPanelData } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { BarGauge, VizRepeater } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { BarGaugeOptions } from './types';
|
||||
import { PanelProps } from '@grafana/ui/src/types';
|
||||
|
||||
interface Props extends PanelProps<BarGaugeOptions> {}
|
||||
|
||||
export class BarGaugePanel extends PureComponent<Props> {
|
||||
renderBarGauge(value, width, height) {
|
||||
const { replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
const prefix = replaceVariables(valueOptions.prefix);
|
||||
const suffix = replaceVariables(valueOptions.suffix);
|
||||
|
||||
return (
|
||||
<BarGauge
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
orientation={options.orientation}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
theme={config.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panelData, options, width, height } = this.props;
|
||||
|
||||
const values = processSingleStatPanelData({
|
||||
panelData: panelData,
|
||||
stat: options.valueOptions.stat,
|
||||
});
|
||||
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={options.orientation}>
|
||||
{({ vizHeight, vizWidth, valueInfo }) => this.renderBarGauge(valueInfo.value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
||||
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
|
||||
import { BarGaugeOptions, orientationOptions } from './types';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
thresholds,
|
||||
});
|
||||
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
valueMappings,
|
||||
});
|
||||
|
||||
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
valueOptions,
|
||||
});
|
||||
|
||||
onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
|
||||
onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
|
||||
onOrientationChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
|
||||
|
||||
render() {
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
||||
<PanelOptionsGroup title="Gauge">
|
||||
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
|
||||
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={options.maxValue} />
|
||||
<div className="form-field">
|
||||
<FormLabel width={8}>Orientation</FormLabel>
|
||||
<Select
|
||||
width={12}
|
||||
options={orientationOptions}
|
||||
defaultValue={orientationOptions[0]}
|
||||
onChange={this.onOrientationChange}
|
||||
value={orientationOptions.find(item => item.value === options.orientation)}
|
||||
/>
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="218.21px" height="187.4px" viewBox="0 0 218.21 187.4" style="enable-background:new 0 0 218.21 187.4;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{fill:url(#SVGID_6_);}
|
||||
.st6{fill:url(#SVGID_7_);}
|
||||
.st7{fill:url(#SVGID_8_);}
|
||||
.st8{fill:url(#SVGID_9_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="68.7753" y1="8.4663" x2="68.7753" y2="59.7143">
|
||||
<stop offset="0" style="stop-color:#FAA91F"/>
|
||||
<stop offset="1" style="stop-color:#F37324"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M7.61,50.71V12.52c0-2.66,1.95-4.82,4.36-4.82h113.62c2.41,0,4.36,2.16,4.36,4.82v38.19
|
||||
c0,2.66-1.95,4.82-4.36,4.82H11.97C9.56,55.53,7.61,53.37,7.61,50.71z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="152.8987" y1="19.9183" x2="152.3315" y2="39.7688">
|
||||
<stop offset="0" style="stop-color:#FAA91F"/>
|
||||
<stop offset="1" style="stop-color:#F37324"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M153.89,28.04c0.95,0,1.78,0.18,2.47,0.55c0.7,0.36,1.28,0.86,1.75,1.49c0.47,0.63,0.81,1.37,1.05,2.23
|
||||
c0.23,0.86,0.35,1.79,0.35,2.78c0,1.24-0.17,2.33-0.52,3.29c-0.34,0.96-0.83,1.77-1.46,2.43c-0.63,0.66-1.37,1.16-2.22,1.5
|
||||
c-0.85,0.34-1.78,0.52-2.79,0.52c-1.03,0-1.98-0.17-2.85-0.5c-0.87-0.33-1.62-0.83-2.25-1.49c-0.63-0.66-1.12-1.46-1.47-2.41
|
||||
c-0.35-0.95-0.53-2.03-0.53-3.25c0-1.32,0.17-2.5,0.52-3.55c0.34-1.05,0.78-2.07,1.31-3.04l4.1-7.71h5.56l-4.16,7.35l0.03,0.03
|
||||
c0.1-0.06,0.27-0.11,0.5-0.15C153.5,28.06,153.71,28.04,153.89,28.04z M154.83,35.02c0-1.05-0.21-1.91-0.62-2.57
|
||||
c-0.41-0.66-0.99-0.99-1.72-0.99c-0.69,0-1.26,0.33-1.72,1c-0.46,0.67-0.68,1.52-0.68,2.55c0,1.03,0.22,1.88,0.67,2.55
|
||||
c0.44,0.67,1.01,1,1.7,1c0.71,0,1.28-0.33,1.72-1C154.62,36.9,154.83,36.05,154.83,35.02z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="167.1879" y1="20.3265" x2="166.6207" y2="40.1771">
|
||||
<stop offset="0" style="stop-color:#FAA91F"/>
|
||||
<stop offset="1" style="stop-color:#F37324"/>
|
||||
</linearGradient>
|
||||
<path class="st2" d="M173.57,26.79c0,0.71-0.09,1.37-0.27,1.97c-0.18,0.61-0.43,1.18-0.73,1.73c-0.3,0.55-0.65,1.07-1.03,1.58
|
||||
c-0.38,0.51-0.78,0.99-1.18,1.46l-4.01,4.65h6.95v4.19h-12.72v-4.31l6.53-7.5c0.53-0.61,0.94-1.23,1.24-1.87
|
||||
c0.3-0.64,0.46-1.25,0.46-1.84c0-0.63-0.15-1.16-0.44-1.61c-0.29-0.45-0.75-0.67-1.38-0.67c-0.59,0-1.07,0.24-1.46,0.71
|
||||
c-0.38,0.48-0.62,1.17-0.7,2.08l-4.56-0.43c0.24-2.19,0.99-3.82,2.23-4.9c1.24-1.08,2.79-1.62,4.63-1.62
|
||||
c0.99,0,1.88,0.15,2.67,0.46c0.79,0.3,1.46,0.73,2.02,1.28c0.56,0.55,0.99,1.21,1.29,2C173.41,24.94,173.57,25.82,173.57,26.79z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="90.8472" y1="69.5294" x2="90.8472" y2="118.2775">
|
||||
<stop offset="0" style="stop-color:#E81E25"/>
|
||||
<stop offset="1" style="stop-color:#A91D39"/>
|
||||
</linearGradient>
|
||||
<path class="st3" d="M7.61,115.16V71.24c0-1.08,0.87-1.95,1.95-1.95h162.57c1.08,0,1.95,0.87,1.95,1.95v43.92
|
||||
c0,1.08-0.87,1.95-1.95,1.95H9.56C8.48,117.11,7.61,116.23,7.61,115.16z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="195.8516" y1="81.1631" x2="196.9859" y2="100.4465">
|
||||
<stop offset="0" style="stop-color:#E81E25"/>
|
||||
<stop offset="1" style="stop-color:#A91D39"/>
|
||||
</linearGradient>
|
||||
<path class="st4" d="M196.03,103.72h-5.28l6.35-17.22h-7.38v-4.28h12.66v3.64L196.03,103.72z"/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="208.6078" y1="80.4128" x2="209.7422" y2="99.6962">
|
||||
<stop offset="0" style="stop-color:#E81E25"/>
|
||||
<stop offset="1" style="stop-color:#A91D39"/>
|
||||
</linearGradient>
|
||||
<path class="st5" d="M210.98,89.39c0.95,0,1.78,0.18,2.47,0.55c0.7,0.36,1.28,0.86,1.75,1.49c0.47,0.63,0.81,1.37,1.05,2.23
|
||||
c0.23,0.86,0.35,1.79,0.35,2.78c0,1.24-0.17,2.33-0.52,3.29c-0.34,0.96-0.83,1.77-1.46,2.43c-0.63,0.66-1.37,1.16-2.22,1.5
|
||||
c-0.85,0.34-1.78,0.52-2.79,0.52c-1.03,0-1.99-0.17-2.85-0.5c-0.87-0.33-1.62-0.83-2.25-1.49c-0.63-0.66-1.12-1.46-1.47-2.41
|
||||
c-0.35-0.95-0.53-2.03-0.53-3.25c0-1.32,0.17-2.5,0.52-3.55c0.34-1.05,0.78-2.07,1.31-3.04l4.1-7.71h5.56l-4.16,7.35l0.03,0.03
|
||||
c0.1-0.06,0.27-0.11,0.5-0.15C210.59,89.41,210.8,89.39,210.98,89.39z M211.92,96.37c0-1.05-0.21-1.91-0.62-2.57
|
||||
c-0.41-0.66-0.99-0.99-1.72-0.99c-0.69,0-1.26,0.33-1.72,1c-0.46,0.67-0.68,1.52-0.68,2.55s0.22,1.88,0.67,2.55
|
||||
c0.44,0.67,1.01,1,1.7,1c0.71,0,1.28-0.33,1.72-1C211.7,98.25,211.92,97.4,211.92,96.37z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="49.8893" y1="131.8424" x2="49.8893" y2="178.0907">
|
||||
<stop offset="0" style="stop-color:#04A64D"/>
|
||||
<stop offset="1" style="stop-color:#007E39"/>
|
||||
</linearGradient>
|
||||
<path class="st6" d="M7.61,176.74v-43.92c0-1.08,0.87-1.95,1.95-1.95h80.66c1.08,0,1.95,0.87,1.95,1.95v43.92
|
||||
c0,1.08-0.87,1.95-1.95,1.95H9.56C8.48,178.69,7.61,177.81,7.61,176.74z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="112.038" y1="145.9514" x2="118.2768" y2="172.6079">
|
||||
<stop offset="0" style="stop-color:#04A64D"/>
|
||||
<stop offset="1" style="stop-color:#007E39"/>
|
||||
</linearGradient>
|
||||
<path class="st7" d="M119.69,161.21v4.31h-4.31v-4.31h-7.87v-4.13l6.29-13.06h5.89v13.18h2.19v4.01H119.69z M115.47,148.77h-0.06
|
||||
l-3.68,8.44h3.74V148.77z"/>
|
||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="126.6068" y1="142.5417" x2="132.8455" y2="169.1982">
|
||||
<stop offset="0" style="stop-color:#04A64D"/>
|
||||
<stop offset="1" style="stop-color:#007E39"/>
|
||||
</linearGradient>
|
||||
<path class="st8" d="M136.52,159.24c0,1.09-0.18,2.06-0.53,2.88c-0.35,0.83-0.84,1.53-1.44,2.11c-0.61,0.58-1.32,1.01-2.14,1.31
|
||||
c-0.82,0.29-1.71,0.44-2.66,0.44c-1.76,0-3.27-0.45-4.52-1.35c-1.26-0.9-2.09-2.27-2.49-4.11l4.28-1.09
|
||||
c0.16,0.71,0.45,1.29,0.85,1.73c0.4,0.45,0.96,0.67,1.67,0.67c0.38,0,0.72-0.08,1-0.24c0.28-0.16,0.52-0.37,0.71-0.64
|
||||
s0.33-0.56,0.43-0.9c0.09-0.33,0.14-0.67,0.14-1.02c0-0.93-0.27-1.62-0.8-2.07c-0.54-0.45-1.25-0.67-2.14-0.67h-1.09v-3.58h1.21
|
||||
c0.81,0,1.43-0.24,1.87-0.71c0.43-0.48,0.65-1.14,0.65-1.99c0-0.55-0.16-1.06-0.47-1.55c-0.31-0.49-0.82-0.73-1.5-0.73
|
||||
c-0.59,0-1.04,0.19-1.37,0.58c-0.32,0.39-0.57,0.87-0.73,1.46l-4.16-1.03c0.24-0.93,0.59-1.72,1.05-2.37s0.97-1.18,1.55-1.59
|
||||
c0.58-0.42,1.21-0.72,1.9-0.91c0.69-0.19,1.39-0.29,2.1-0.29c0.79,0,1.55,0.12,2.29,0.36c0.74,0.24,1.4,0.61,1.99,1.09
|
||||
c0.59,0.49,1.06,1.09,1.41,1.82c0.35,0.73,0.53,1.58,0.53,2.55c0,1.26-0.27,2.31-0.8,3.17c-0.54,0.86-1.25,1.43-2.14,1.72v0.09
|
||||
c1.03,0.28,1.85,0.87,2.46,1.76C136.22,157.04,136.52,158.07,136.52,159.24z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,22 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||
import { BarGaugeOptions, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
|
||||
|
||||
reactPanel.setEditor(BarGaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
|
||||
const options: Partial<BarGaugeOptions> = {};
|
||||
|
||||
if (prevOptions.valueOptions) {
|
||||
options.valueOptions = prevOptions.valueOptions;
|
||||
options.thresholds = prevOptions.thresholds;
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Bar Gauge",
|
||||
"id": "bargauge",
|
||||
"state": "alpha",
|
||||
|
||||
"dataFormats": ["time_series"],
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icon_bar_gauge.svg",
|
||||
"large": "img/icon_bar_gauge.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
|
||||
export interface BarGaugeOptions {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
orientation: VizOrientation;
|
||||
valueOptions: SingleStatValueOptions;
|
||||
valueMappings: ValueMapping[];
|
||||
thresholds: Threshold[];
|
||||
}
|
||||
|
||||
export const orientationOptions: SelectOptionItem[] = [
|
||||
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
|
||||
{ value: VizOrientation.Vertical, label: 'Vertical' },
|
||||
];
|
||||
|
||||
export const defaults: BarGaugeOptions = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
orientation: VizOrientation.Horizontal,
|
||||
valueOptions: {
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
decimals: null,
|
||||
},
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
valueMappings: [],
|
||||
};
|
||||
@@ -9,6 +9,8 @@ import { FormField, PanelEditorProps } from '@grafana/ui';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
labelWidth = 8;
|
||||
|
||||
onToggleThresholdLabels = () =>
|
||||
this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
|
||||
|
||||
@@ -28,17 +30,17 @@ export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Gauge">
|
||||
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={minValue} />
|
||||
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={maxValue} />
|
||||
<FormField label="Min value" labelWidth={this.labelWidth} onChange={this.onMinValueChange} value={minValue} />
|
||||
<FormField label="Max value" labelWidth={this.labelWidth} onChange={this.onMaxValueChange} value={maxValue} />
|
||||
<Switch
|
||||
label="Show labels"
|
||||
labelClass="width-8"
|
||||
labelClass={`width-${this.labelWidth}`}
|
||||
checked={showThresholdLabels}
|
||||
onChange={this.onToggleThresholdLabels}
|
||||
/>
|
||||
<Switch
|
||||
label="Show markers"
|
||||
labelClass="width-8"
|
||||
labelClass={`width-${this.labelWidth}`}
|
||||
checked={showThresholdMarkers}
|
||||
onChange={this.onToggleThresholdMarkers}
|
||||
/>
|
||||
|
||||
@@ -1,82 +1,59 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { processTimeSeries, ThemeContext } from '@grafana/ui';
|
||||
import { processSingleStatPanelData } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { Gauge } from '@grafana/ui';
|
||||
import { Gauge, VizRepeater } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { GaugeOptions } from './types';
|
||||
import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
|
||||
import { PanelProps, VizOrientation } from '@grafana/ui/src/types';
|
||||
|
||||
interface Props extends PanelProps<GaugeOptions> {}
|
||||
interface State {
|
||||
value: TimeSeriesValue;
|
||||
}
|
||||
|
||||
export class GaugePanel extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: this.findValue(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.panelData !== prevProps.panelData) {
|
||||
this.setState({ value: this.findValue(this.props) });
|
||||
}
|
||||
}
|
||||
|
||||
findValue(props: Props): number | null {
|
||||
const { panelData, options } = props;
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
renderGauge(value, width, height) {
|
||||
const { replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
if (panelData.timeSeries) {
|
||||
const vmSeries = processTimeSeries({
|
||||
timeSeries: panelData.timeSeries,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
if (vmSeries[0]) {
|
||||
return vmSeries[0].stats[valueOptions.stat];
|
||||
}
|
||||
} else if (panelData.tableData) {
|
||||
return panelData.tableData.rows[0].find(prop => prop > 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height, replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
const { value } = this.state;
|
||||
|
||||
const prefix = replaceVariables(valueOptions.prefix);
|
||||
const suffix = replaceVariables(valueOptions.suffix);
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => (
|
||||
<Gauge
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={options.minValue}
|
||||
maxValue={options.maxValue}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</ThemeContext.Consumer>
|
||||
<Gauge
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={options.minValue}
|
||||
maxValue={options.maxValue}
|
||||
theme={config.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panelData, options, height, width } = this.props;
|
||||
|
||||
const values = processSingleStatPanelData({
|
||||
panelData: panelData,
|
||||
stat: options.valueOptions.stat,
|
||||
});
|
||||
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={VizOrientation.Auto}>
|
||||
{({ vizHeight, vizWidth, valueInfo }) => this.renderGauge(valueInfo.value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
PanelEditorProps,
|
||||
|
||||
@@ -8,3 +8,15 @@ export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
|
||||
|
||||
reactPanel.setEditor(GaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
|
||||
const options: Partial<GaugeOptions> = {};
|
||||
|
||||
if (prevOptions.valueOptions) {
|
||||
options.valueOptions = prevOptions.valueOptions;
|
||||
options.thresholds = prevOptions.thresholds;
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
@@ -31,5 +31,5 @@ export const defaults: GaugeOptions = {
|
||||
unit: 'none',
|
||||
},
|
||||
valueMappings: [],
|
||||
thresholds: [],
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
};
|
||||
|
||||
@@ -96,7 +96,9 @@ button.close {
|
||||
}
|
||||
|
||||
.center-vh {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
|
||||
Reference in New Issue
Block a user