added orientation option
This commit is contained in:
@@ -21,6 +21,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
value: 25,
|
value: 25,
|
||||||
decimals: 0,
|
decimals: 0,
|
||||||
theme: getTheme(),
|
theme: getTheme(),
|
||||||
|
orientation: 'horizontal',
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface Props extends Themeable {
|
|||||||
value: TimeSeriesValue;
|
value: TimeSeriesValue;
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
minValue: number;
|
minValue: number;
|
||||||
|
orientation: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
@@ -28,6 +29,7 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
minValue: 0,
|
minValue: 0,
|
||||||
value: 100,
|
value: 100,
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
|
orientation: 'horizontal',
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
valueMappings: [],
|
valueMappings: [],
|
||||||
};
|
};
|
||||||
@@ -75,29 +77,19 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderHorizontalBar(valueStyles: React.CSSProperties, valueFormatted: string, barStyles: React.CSSProperties) {
|
||||||
const { height, width, maxValue, minValue, unit, decimals } = this.props;
|
const { height, width } = this.props;
|
||||||
|
|
||||||
const numericValue = this.getNumericValue();
|
const containerStyles = {
|
||||||
const barMaxHeight = height * 0.8; // 20% for value & name
|
|
||||||
const valuePercent = numericValue / (maxValue - minValue);
|
|
||||||
const barHeight = Math.max(valuePercent * barMaxHeight, 0);
|
|
||||||
|
|
||||||
const formatFunc = getValueFormat(unit);
|
|
||||||
const valueFormatted = formatFunc(numericValue, decimals);
|
|
||||||
const colors = this.getColors();
|
|
||||||
|
|
||||||
const containerStyles = { width: `${width}px`, height: `${height}px` };
|
|
||||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value);
|
|
||||||
const barStyles = {
|
|
||||||
height: `${barHeight}px`,
|
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
backgroundColor: colors.bar,
|
height: `${height}px`,
|
||||||
borderTop: `1px solid ${colors.border}`,
|
display: 'flex',
|
||||||
};
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bar-gauge" style={containerStyles}>
|
<div style={containerStyles}>
|
||||||
<div className="bar-gauge__value" style={valueStyles}>
|
<div className="bar-gauge__value" style={valueStyles}>
|
||||||
{valueFormatted}
|
{valueFormatted}
|
||||||
</div>
|
</div>
|
||||||
@@ -105,6 +97,70 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderVerticalBar(valueFormatted: string, barStyles: React.CSSProperties) {
|
||||||
|
const { height, width } = this.props;
|
||||||
|
const colors = this.getColors();
|
||||||
|
|
||||||
|
const containerStyles = {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '8px',
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
const valueStyles = this.getValueStyles(valueFormatted, colors.value);
|
||||||
|
|
||||||
|
Object.assign(valueStyles, { marginLeft: '8px' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={containerStyles}>
|
||||||
|
<div style={barStyles} />
|
||||||
|
<div className="bar-gauge__value" style={valueStyles}>
|
||||||
|
{valueFormatted}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { height, width, maxValue, minValue, orientation, unit, decimals } = this.props;
|
||||||
|
|
||||||
|
const numericValue = this.getNumericValue();
|
||||||
|
const barMaxHeight = height * 0.8; // 20% for value & name
|
||||||
|
const barMaxWidth = width * 0.8;
|
||||||
|
const valuePercent = numericValue / (maxValue - minValue);
|
||||||
|
const barHeight = Math.max(valuePercent * barMaxHeight, 0);
|
||||||
|
const barWidth = Math.max(valuePercent * barMaxWidth, 0);
|
||||||
|
|
||||||
|
const formatFunc = getValueFormat(unit);
|
||||||
|
const valueFormatted = formatFunc(numericValue, decimals);
|
||||||
|
const colors = this.getColors();
|
||||||
|
const valueStyles = this.getValueStyles(valueFormatted, colors.value);
|
||||||
|
const vertical = orientation === 'vertical';
|
||||||
|
|
||||||
|
const horizontalBarStyles = {
|
||||||
|
height: `${barHeight}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
backgroundColor: colors.bar,
|
||||||
|
borderTop: `1px solid ${colors.border}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const verticalBarStyles = {
|
||||||
|
height: `${height}px`,
|
||||||
|
width: `${barWidth}px`,
|
||||||
|
backgroundColor: colors.bar,
|
||||||
|
borderRight: `1px solid ${colors.border}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const barStyles = vertical ? verticalBarStyles : horizontalBarStyles;
|
||||||
|
|
||||||
|
return vertical
|
||||||
|
? this.renderVerticalBar(valueFormatted, barStyles)
|
||||||
|
: this.renderHorizontalBar(valueStyles, valueFormatted, barStyles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BarColors {
|
interface BarColors {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { TimeSeriesVMs } from '../../types';
|
import { TimeSeriesVMs } from '../../types';
|
||||||
|
|
||||||
interface RenderProps {
|
interface RenderProps {
|
||||||
@@ -12,45 +12,55 @@ interface Props {
|
|||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
timeSeries: TimeSeriesVMs;
|
timeSeries: TimeSeriesVMs;
|
||||||
|
orientation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VizRepeater extends Component<Props> {
|
export class VizRepeater extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { children, height, timeSeries, width } = this.props;
|
const { children, orientation, height, timeSeries, width } = this.props;
|
||||||
|
|
||||||
const singleStatWidth = 1 / timeSeries.length * 100;
|
const vizContainerWidth = 1 / timeSeries.length * 100;
|
||||||
const singleStatHeight = 1 / timeSeries.length * 100;
|
const vizContainerHeight = 1 / timeSeries.length * 100;
|
||||||
const repeatingGaugeWidth = Math.floor(width / timeSeries.length) - 10; // make Gauge slightly smaller than panel.
|
const repeatingVizWidth = Math.floor(width / timeSeries.length) - 10; // make Gauge slightly smaller than panel.
|
||||||
const repeatingGaugeHeight = Math.floor(height / timeSeries.length) - 10;
|
const repeatingVizHeight = Math.floor(height / timeSeries.length) - 10;
|
||||||
|
|
||||||
const horizontalPanels = {
|
const horizontalVisualization = {
|
||||||
display: 'inline-block',
|
|
||||||
height: height,
|
height: height,
|
||||||
width: `${singleStatWidth}%`,
|
width: `${vizContainerWidth}%`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const verticalPanels = {
|
const verticalVisualization = {
|
||||||
display: 'block',
|
|
||||||
width: width,
|
width: width,
|
||||||
height: `${singleStatHeight}%`,
|
height: `${vizContainerHeight}%`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const repeaterStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: orientation === 'vertical' || height > width ? 'column' : 'row',
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
let vizContainerStyle = {};
|
let vizContainerStyle = {};
|
||||||
let vizWidth = width;
|
let vizWidth = width;
|
||||||
let vizHeight = height;
|
let vizHeight = height;
|
||||||
|
|
||||||
if (width > height) {
|
if (orientation === 'horizontal' || width > height) {
|
||||||
vizContainerStyle = horizontalPanels;
|
vizContainerStyle = horizontalVisualization;
|
||||||
vizWidth = repeatingGaugeWidth;
|
vizWidth = repeatingVizWidth;
|
||||||
} else if (height > width) {
|
|
||||||
vizContainerStyle = verticalPanels;
|
|
||||||
vizHeight = repeatingGaugeHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return children({
|
if (orientation === 'vertical' || height > width) {
|
||||||
vizHeight,
|
vizContainerStyle = verticalVisualization;
|
||||||
vizWidth,
|
vizHeight = repeatingVizHeight;
|
||||||
vizContainerStyle,
|
}
|
||||||
});
|
|
||||||
|
return (
|
||||||
|
<div style={repeaterStyle}>
|
||||||
|
{children({
|
||||||
|
vizHeight,
|
||||||
|
vizWidth,
|
||||||
|
vizContainerStyle,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,59 +2,78 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { processTimeSeries, ThemeContext } from '@grafana/ui';
|
import { processTimeSeries } from '@grafana/ui';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { BarGauge } from '@grafana/ui';
|
import { BarGauge, VizRepeater } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { BarGaugeOptions } from './types';
|
import { BarGaugeOptions } from './types';
|
||||||
import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
|
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
|
||||||
|
|
||||||
interface Props extends PanelProps<BarGaugeOptions> {}
|
interface Props extends PanelProps<BarGaugeOptions> {}
|
||||||
|
|
||||||
export class BarGaugePanel extends PureComponent<Props> {
|
export class BarGaugePanel extends PureComponent<Props> {
|
||||||
render() {
|
renderBarGauge(value, width, height) {
|
||||||
const { panelData, width, height, onInterpolate, options } = this.props;
|
const { onInterpolate, options } = this.props;
|
||||||
const { valueOptions } = options;
|
const { valueOptions } = options;
|
||||||
|
|
||||||
const prefix = onInterpolate(valueOptions.prefix);
|
const prefix = onInterpolate(valueOptions.prefix);
|
||||||
const suffix = onInterpolate(valueOptions.suffix);
|
const suffix = onInterpolate(valueOptions.suffix);
|
||||||
|
|
||||||
let value: TimeSeriesValue;
|
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 { stat } = options.valueOptions;
|
||||||
|
|
||||||
if (panelData.timeSeries) {
|
if (panelData.timeSeries) {
|
||||||
const vmSeries = processTimeSeries({
|
const timeSeries = processTimeSeries({
|
||||||
timeSeries: panelData.timeSeries,
|
timeSeries: panelData.timeSeries,
|
||||||
nullValueMode: NullValueMode.Null,
|
nullValueMode: NullValueMode.Null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (vmSeries[0]) {
|
if (timeSeries.length > 1) {
|
||||||
value = vmSeries[0].stats[valueOptions.stat];
|
return (
|
||||||
} else {
|
<VizRepeater height={height} width={width} timeSeries={timeSeries} orientation={options.orientation}>
|
||||||
value = null;
|
{({ vizHeight, vizWidth, vizContainerStyle }) => {
|
||||||
|
return timeSeries.map((series, index) => {
|
||||||
|
const value = stat !== 'name' ? series.stats[stat] : series.label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} style={vizContainerStyle}>
|
||||||
|
{this.renderBarGauge(value, vizWidth, vizHeight)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
</VizRepeater>
|
||||||
|
);
|
||||||
|
} else if (timeSeries.length > 0) {
|
||||||
|
const value = timeSeries[0].stats[options.valueOptions.stat];
|
||||||
|
return this.renderBarGauge(value, width, height);
|
||||||
}
|
}
|
||||||
} else if (panelData.tableData) {
|
} else if (panelData.tableData) {
|
||||||
value = panelData.tableData.rows[0].find(prop => prop > 0);
|
const value = panelData.tableData.rows[0].find(prop => prop > 0);
|
||||||
|
|
||||||
|
return this.renderBarGauge(value, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className="singlestat-panel">No time series data available</div>;
|
||||||
<ThemeContext.Consumer>
|
|
||||||
{theme => (
|
|
||||||
<BarGauge
|
|
||||||
value={value}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
prefix={prefix}
|
|
||||||
suffix={suffix}
|
|
||||||
unit={valueOptions.unit}
|
|
||||||
decimals={valueOptions.decimals}
|
|
||||||
thresholds={options.thresholds}
|
|
||||||
valueMappings={options.valueMappings}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ThemeContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEd
|
|||||||
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PanelEditorProps, Threshold, ValueMapping } from '@grafana/ui';
|
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
|
||||||
import { BarGaugeOptions } from './types';
|
import { BarGaugeOptions, orientationOptions } from './types';
|
||||||
import { SingleStatValueOptions } from '../gauge/types';
|
import { SingleStatValueOptions } from '../gauge/types';
|
||||||
|
|
||||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||||
@@ -31,6 +31,7 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
|||||||
|
|
||||||
onMinValueChange = ({ target }) => this.props.onChange({ ...this.props.options, minValue: target.value });
|
onMinValueChange = ({ target }) => this.props.onChange({ ...this.props.options, minValue: target.value });
|
||||||
onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
|
onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
|
||||||
|
onOrientationChange = ({ value }) => this.props.onChange({ ...this.props.options, orientation: value });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
@@ -42,6 +43,16 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
|||||||
<PanelOptionsGroup title="Gauge">
|
<PanelOptionsGroup title="Gauge">
|
||||||
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
|
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
|
||||||
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={options.maxValue} />
|
<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>
|
</PanelOptionsGroup>
|
||||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
import { Threshold, SelectOptionItem, ValueMapping } from '@grafana/ui';
|
||||||
import { SingleStatValueOptions } from '../gauge/types';
|
import { SingleStatValueOptions } from '../gauge/types';
|
||||||
|
|
||||||
export interface BarGaugeOptions {
|
export interface BarGaugeOptions {
|
||||||
minValue: number;
|
minValue: number;
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
|
orientation: string;
|
||||||
valueOptions: SingleStatValueOptions;
|
valueOptions: SingleStatValueOptions;
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrientationModes {
|
||||||
|
Vertical = 'vertical',
|
||||||
|
Horizontal = 'horizontal',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orientationOptions: SelectOptionItem[] = [
|
||||||
|
{ value: OrientationModes.Horizontal, label: 'Horizontal' },
|
||||||
|
{ value: OrientationModes.Vertical, label: 'Vertical' },
|
||||||
|
];
|
||||||
|
|
||||||
export const defaults: BarGaugeOptions = {
|
export const defaults: BarGaugeOptions = {
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
|
orientation: OrientationModes.Horizontal,
|
||||||
valueOptions: {
|
valueOptions: {
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
stat: 'avg',
|
stat: 'avg',
|
||||||
@@ -19,6 +31,6 @@ export const defaults: BarGaugeOptions = {
|
|||||||
suffix: '',
|
suffix: '',
|
||||||
decimals: null,
|
decimals: null,
|
||||||
},
|
},
|
||||||
thresholds: [{ index: 1, value: 80, color: 'red' }, { index: 0, value: -Infinity, color: 'green' }],
|
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||||
valueMappings: [],
|
valueMappings: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user