mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Bar Gauge: Show tile (series name) & refactorings & tests (#16397)
* WIP: began work on adding title support to bar gauge * Feat: BarGauge progress * wip: trying improve text size handling in bar gauge * BarGauge: progress on title & value auto sizing * BarGauge: more auto size handling * bargauge: minor tweaks * Added tests * Refactoring: BarGauge refactoring moving css generation to seperate functions and adding some basic tests * Refactoring VizRepeater and more * Fix: updated and fixed tests
This commit is contained in:
parent
9530906822
commit
566b3d178a
@ -1,61 +1,82 @@
|
|||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { number, text, boolean } from '@storybook/addon-knobs';
|
import { number, text } from '@storybook/addon-knobs';
|
||||||
import { BarGauge } from './BarGauge';
|
import { BarGauge, Props } from './BarGauge';
|
||||||
import { VizOrientation } from '../../types';
|
import { VizOrientation } from '../../types';
|
||||||
import { withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||||
|
|
||||||
const getKnobs = () => {
|
const getKnobs = () => {
|
||||||
return {
|
return {
|
||||||
value: number('value', 70),
|
value: number('value', 70),
|
||||||
|
title: text('title', 'Title'),
|
||||||
minValue: number('minValue', 0),
|
minValue: number('minValue', 0),
|
||||||
maxValue: number('maxValue', 100),
|
maxValue: number('maxValue', 100),
|
||||||
threshold1Value: number('threshold1Value', 40),
|
threshold1Value: number('threshold1Value', 40),
|
||||||
threshold1Color: text('threshold1Color', 'orange'),
|
threshold1Color: text('threshold1Color', 'orange'),
|
||||||
threshold2Value: number('threshold2Value', 60),
|
threshold2Value: number('threshold2Value', 60),
|
||||||
threshold2Color: text('threshold2Color', 'red'),
|
threshold2Color: text('threshold2Color', 'red'),
|
||||||
unit: text('unit', 'ms'),
|
|
||||||
decimals: number('decimals', 1),
|
|
||||||
horizontal: boolean('horizontal', false),
|
|
||||||
lcd: boolean('lcd', false),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
|
const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
|
||||||
|
|
||||||
BarGaugeStories.addDecorator(withHorizontallyCenteredStory);
|
BarGaugeStories.addDecorator(withCenteredStory);
|
||||||
|
|
||||||
BarGaugeStories.add('Simple with basic thresholds', () => {
|
function addBarGaugeStory(name: string, overrides: Partial<Props>) {
|
||||||
|
BarGaugeStories.add(name, () => {
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
|
title,
|
||||||
minValue,
|
minValue,
|
||||||
maxValue,
|
maxValue,
|
||||||
threshold1Color,
|
threshold1Color,
|
||||||
threshold2Color,
|
threshold2Color,
|
||||||
threshold1Value,
|
threshold1Value,
|
||||||
threshold2Value,
|
threshold2Value,
|
||||||
unit,
|
|
||||||
decimals,
|
|
||||||
horizontal,
|
|
||||||
lcd,
|
|
||||||
} = getKnobs();
|
} = getKnobs();
|
||||||
|
|
||||||
return renderComponentWithTheme(BarGauge, {
|
const props: Props = {
|
||||||
|
theme: {} as any,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
value: { text: value.toString(), numeric: value },
|
value: {
|
||||||
|
text: value.toString(),
|
||||||
|
title: title,
|
||||||
|
numeric: value,
|
||||||
|
},
|
||||||
minValue: minValue,
|
minValue: minValue,
|
||||||
maxValue: maxValue,
|
maxValue: maxValue,
|
||||||
unit: unit,
|
orientation: VizOrientation.Vertical,
|
||||||
prefix: '',
|
displayMode: 'basic',
|
||||||
postfix: '',
|
|
||||||
decimals: decimals,
|
|
||||||
orientation: horizontal ? VizOrientation.Horizontal : VizOrientation.Vertical,
|
|
||||||
displayMode: lcd ? 'lcd' : 'simple',
|
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ index: 0, value: -Infinity, color: 'green' },
|
{ index: 0, value: -Infinity, color: 'green' },
|
||||||
{ index: 1, value: threshold1Value, color: threshold1Color },
|
{ index: 1, value: threshold1Value, color: threshold1Color },
|
||||||
{ index: 1, value: threshold2Value, color: threshold2Color },
|
{ index: 1, value: threshold2Value, color: threshold2Color },
|
||||||
],
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, overrides);
|
||||||
|
return renderComponentWithTheme(BarGauge, props);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addBarGaugeStory('Gradient Vertical', {
|
||||||
|
displayMode: 'gradient',
|
||||||
|
orientation: VizOrientation.Vertical,
|
||||||
|
height: 500,
|
||||||
|
width: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
addBarGaugeStory('Gradient Horizontal', {
|
||||||
|
displayMode: 'gradient',
|
||||||
|
orientation: VizOrientation.Horizontal,
|
||||||
|
height: 100,
|
||||||
|
width: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
addBarGaugeStory('LCD Horizontal', {
|
||||||
|
displayMode: 'lcd',
|
||||||
|
orientation: VizOrientation.Vertical,
|
||||||
|
height: 500,
|
||||||
|
width: 100,
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { BarGauge, Props } from './BarGauge';
|
import { BarGauge, Props, getValueColor, getBasicAndGradientStyles, getBarGradient, getTitleStyles } from './BarGauge';
|
||||||
import { VizOrientation } from '../../types';
|
import { VizOrientation, DisplayValue } from '../../types';
|
||||||
import { getTheme } from '../../themes';
|
import { getTheme } from '../../themes';
|
||||||
|
|
||||||
jest.mock('jquery', () => ({
|
// jest.mock('jquery', () => ({
|
||||||
plot: jest.fn(),
|
// plot: jest.fn(),
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const green = '#73BF69';
|
||||||
|
const orange = '#FF9830';
|
||||||
|
// const red = '#BB';
|
||||||
|
|
||||||
|
function getProps(propOverrides?: Partial<Props>): Props {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
displayMode: 'basic',
|
displayMode: 'basic',
|
||||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
thresholds: [
|
||||||
|
{ index: 0, value: -Infinity, color: 'green' },
|
||||||
|
{ index: 1, value: 70, color: 'orange' },
|
||||||
|
{ index: 2, value: 90, color: 'red' },
|
||||||
|
],
|
||||||
height: 300,
|
height: 300,
|
||||||
width: 300,
|
width: 300,
|
||||||
value: {
|
value: {
|
||||||
@ -25,7 +33,11 @@ const setup = (propOverrides?: object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = getProps(propOverrides);
|
||||||
const wrapper = shallow(<BarGauge {...props} />);
|
const wrapper = shallow(<BarGauge {...props} />);
|
||||||
const instance = wrapper.instance() as BarGauge;
|
const instance = wrapper.instance() as BarGauge;
|
||||||
|
|
||||||
@ -35,29 +47,88 @@ const setup = (propOverrides?: object) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Get font color', () => {
|
function getValue(value: number, title?: string): DisplayValue {
|
||||||
it('should get first threshold color when only one threshold', () => {
|
return { numeric: value, text: value.toString(), title: title };
|
||||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
}
|
||||||
|
|
||||||
expect(instance.getValueColors().value).toEqual('#7EB26D');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
describe('BarGauge', () => {
|
||||||
|
describe('Get value color', () => {
|
||||||
it('should get the threshold color if value is same as a threshold', () => {
|
it('should get the threshold color if value is same as a threshold', () => {
|
||||||
const { instance } = setup({
|
const props = getProps({ value: getValue(70) });
|
||||||
thresholds: [
|
expect(getValueColor(props)).toEqual(orange);
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
});
|
||||||
{ index: 1, value: 10, color: '#EAB839' },
|
it('should get the base threshold', () => {
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
const props = getProps({ value: getValue(-10) });
|
||||||
],
|
expect(getValueColor(props)).toEqual(green);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(instance.getValueColors().value).toEqual('#EAB839');
|
describe('Vertical bar without title', () => {
|
||||||
|
it('should not include title height in height', () => {
|
||||||
|
const props = getProps({
|
||||||
|
height: 300,
|
||||||
|
value: getValue(100),
|
||||||
|
orientation: VizOrientation.Vertical,
|
||||||
|
});
|
||||||
|
const styles = getBasicAndGradientStyles(props);
|
||||||
|
expect(styles.bar.height).toBe('270px');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('Render BarGauge with basic options', () => {
|
describe('Vertical bar with title', () => {
|
||||||
|
it('should include title height in height', () => {
|
||||||
|
const props = getProps({
|
||||||
|
height: 300,
|
||||||
|
value: getValue(100, 'ServerA'),
|
||||||
|
orientation: VizOrientation.Vertical,
|
||||||
|
});
|
||||||
|
const styles = getBasicAndGradientStyles(props);
|
||||||
|
expect(styles.bar.height).toBe('249px');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Horizontal bar with title', () => {
|
||||||
|
it('should place above if height > 40', () => {
|
||||||
|
const props = getProps({
|
||||||
|
height: 41,
|
||||||
|
value: getValue(100, 'AA'),
|
||||||
|
orientation: VizOrientation.Horizontal,
|
||||||
|
});
|
||||||
|
const styles = getTitleStyles(props);
|
||||||
|
expect(styles.wrapper.flexDirection).toBe('column');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Horizontal bar with title', () => {
|
||||||
|
it('should place below if height < 40', () => {
|
||||||
|
const props = getProps({
|
||||||
|
height: 30,
|
||||||
|
value: getValue(100, 'AA'),
|
||||||
|
orientation: VizOrientation.Horizontal,
|
||||||
|
});
|
||||||
|
const styles = getTitleStyles(props);
|
||||||
|
expect(styles.wrapper.flexDirection).toBe('row');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Gradient', () => {
|
||||||
|
it('should build gradient based on thresholds', () => {
|
||||||
|
const props = getProps({ orientation: VizOrientation.Vertical, value: getValue(100) });
|
||||||
|
const gradient = getBarGradient(props, 300);
|
||||||
|
expect(gradient).toBe('linear-gradient(0deg, #73BF69, #73BF69 105px, #FF9830 240px, #F2495C)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop gradient if value < threshold', () => {
|
||||||
|
const props = getProps({ orientation: VizOrientation.Vertical, value: getValue(70) });
|
||||||
|
const gradient = getBarGradient(props, 300);
|
||||||
|
expect(gradient).toBe('linear-gradient(0deg, #73BF69, #73BF69 105px, #FF9830)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Render with basic options', () => {
|
||||||
it('should render', () => {
|
it('should render', () => {
|
||||||
const { wrapper } = setup();
|
const { wrapper } = setup();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,11 @@ import { getColorFromHexRgbOrName, getThresholdForValue } from '../../utils';
|
|||||||
// Types
|
// Types
|
||||||
import { DisplayValue, Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
|
import { DisplayValue, Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
|
||||||
|
|
||||||
const BAR_SIZE_RATIO = 0.8;
|
const MIN_VALUE_HEIGHT = 18;
|
||||||
|
const MAX_VALUE_HEIGHT = 50;
|
||||||
|
const MIN_VALUE_WIDTH = 50;
|
||||||
|
const MAX_VALUE_WIDTH = 100;
|
||||||
|
const LINE_HEIGHT = 1.5;
|
||||||
|
|
||||||
export interface Props extends Themeable {
|
export interface Props extends Themeable {
|
||||||
height: number;
|
height: number;
|
||||||
@ -18,6 +22,7 @@ export interface Props extends Themeable {
|
|||||||
maxValue: number;
|
maxValue: number;
|
||||||
minValue: number;
|
minValue: number;
|
||||||
orientation: VizOrientation;
|
orientation: VizOrientation;
|
||||||
|
itemSpacing?: number;
|
||||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
displayMode: 'basic' | 'lcd' | 'gradient';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,9 +37,27 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
displayMode: 'lcd',
|
displayMode: 'lcd',
|
||||||
orientation: VizOrientation.Horizontal,
|
orientation: VizOrientation.Horizontal,
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
|
itemSpacing: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { title } = this.props.value;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return this.renderBarAndValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = getTitleStyles(this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.wrapper}>
|
||||||
|
<div style={styles.title}>{title}</div>
|
||||||
|
{this.renderBarAndValue()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBarAndValue() {
|
||||||
switch (this.props.displayMode) {
|
switch (this.props.displayMode) {
|
||||||
case 'lcd':
|
case 'lcd':
|
||||||
return this.renderRetroBars();
|
return this.renderRetroBars();
|
||||||
@ -45,55 +68,374 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValueColors(): BarColors {
|
renderBasicAndGradientBars(): ReactNode {
|
||||||
const { thresholds, theme, value } = this.props;
|
const { value } = this.props;
|
||||||
|
|
||||||
const activeThreshold = getThresholdForValue(thresholds, value.numeric);
|
const styles = getBasicAndGradientStyles(this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.wrapper}>
|
||||||
|
<div className="bar-gauge__value" style={styles.value}>
|
||||||
|
{value.text}
|
||||||
|
</div>
|
||||||
|
<div style={styles.bar} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellColor(positionValue: TimeSeriesValue): CellColors {
|
||||||
|
const { thresholds, theme, value } = this.props;
|
||||||
|
const activeThreshold = getThresholdForValue(thresholds, positionValue);
|
||||||
|
|
||||||
if (activeThreshold !== null) {
|
if (activeThreshold !== null) {
|
||||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
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.numeric)) {
|
||||||
return {
|
return {
|
||||||
value: color,
|
|
||||||
border: color,
|
|
||||||
background: tinycolor(color)
|
background: tinycolor(color)
|
||||||
.setAlpha(0.15)
|
.setAlpha(0.18)
|
||||||
.toRgbString(),
|
.toRgbString(),
|
||||||
|
border: 'transparent',
|
||||||
|
isLit: false,
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
background: tinycolor(color)
|
||||||
|
.setAlpha(0.95)
|
||||||
|
.toRgbString(),
|
||||||
|
backgroundShade: tinycolor(color)
|
||||||
|
.setAlpha(0.55)
|
||||||
|
.toRgbString(),
|
||||||
|
border: tinycolor(color)
|
||||||
|
.setAlpha(0.9)
|
||||||
|
.toRgbString(),
|
||||||
|
isLit: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: getColorFromHexRgbOrName('gray', theme.type),
|
background: 'gray',
|
||||||
background: getColorFromHexRgbOrName('gray', theme.type),
|
border: 'gray',
|
||||||
border: getColorFromHexRgbOrName('gray', theme.type),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getValueStyles(value: string, color: string, width: number): CSSProperties {
|
renderRetroBars(): ReactNode {
|
||||||
const guess = width / (value.length * 1.1);
|
const { maxValue, minValue, value, itemSpacing } = this.props;
|
||||||
const fontSize = Math.min(Math.max(guess, 14), 40);
|
const {
|
||||||
|
valueHeight,
|
||||||
|
valueWidth,
|
||||||
|
maxBarHeight,
|
||||||
|
maxBarWidth,
|
||||||
|
wrapperWidth,
|
||||||
|
wrapperHeight,
|
||||||
|
} = calculateBarAndValueDimensions(this.props);
|
||||||
|
|
||||||
|
const isVert = isVertical(this.props);
|
||||||
|
const valueRange = maxValue - minValue;
|
||||||
|
const maxSize = isVert ? maxBarHeight : maxBarWidth;
|
||||||
|
const cellSpacing = itemSpacing!;
|
||||||
|
const cellCount = maxSize / 20;
|
||||||
|
const cellSize = (maxSize - cellSpacing * cellCount) / cellCount;
|
||||||
|
const valueColor = getValueColor(this.props);
|
||||||
|
const valueStyles = getValueStyles(value.text, valueColor, valueWidth, valueHeight);
|
||||||
|
|
||||||
|
const containerStyles: CSSProperties = {
|
||||||
|
width: `${wrapperWidth}px`,
|
||||||
|
height: `${wrapperHeight}px`,
|
||||||
|
display: 'flex',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isVert) {
|
||||||
|
containerStyles.flexDirection = 'column-reverse';
|
||||||
|
containerStyles.alignItems = 'center';
|
||||||
|
valueStyles.justifyContent = 'center';
|
||||||
|
} else {
|
||||||
|
containerStyles.flexDirection = 'row';
|
||||||
|
containerStyles.alignItems = 'center';
|
||||||
|
valueStyles.justifyContent = 'flex-end';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells: JSX.Element[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < cellCount; i++) {
|
||||||
|
const currentValue = (valueRange / cellCount) * i;
|
||||||
|
const cellColor = this.getCellColor(currentValue);
|
||||||
|
const cellStyles: CSSProperties = {
|
||||||
|
borderRadius: '2px',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cellColor.isLit) {
|
||||||
|
cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`;
|
||||||
|
} else {
|
||||||
|
cellStyles.backgroundColor = cellColor.background;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVert) {
|
||||||
|
cellStyles.height = `${cellSize}px`;
|
||||||
|
cellStyles.width = `${maxBarWidth}px`;
|
||||||
|
cellStyles.marginTop = `${cellSpacing}px`;
|
||||||
|
} else {
|
||||||
|
cellStyles.width = `${cellSize}px`;
|
||||||
|
cellStyles.height = `${maxBarHeight}px`;
|
||||||
|
cellStyles.marginRight = `${cellSpacing}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.push(<div key={i.toString()} style={cellStyles} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={containerStyles}>
|
||||||
|
{cells}
|
||||||
|
<div className="bar-gauge__value" style={valueStyles}>
|
||||||
|
{value.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CellColors {
|
||||||
|
background: string;
|
||||||
|
backgroundShade?: string;
|
||||||
|
border: string;
|
||||||
|
isLit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TitleDimensions {
|
||||||
|
fontSize: number;
|
||||||
|
placement: 'above' | 'left' | 'below';
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVertical(props: Props) {
|
||||||
|
return props.orientation === VizOrientation.Vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTitleDimensions(props: Props): TitleDimensions {
|
||||||
|
const { title } = props.value;
|
||||||
|
const { height, width } = props;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return { fontSize: 0, width: 0, height: 0, placement: 'above' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVertical(props)) {
|
||||||
|
return {
|
||||||
|
fontSize: 14,
|
||||||
|
width: width,
|
||||||
|
height: 14 * LINE_HEIGHT,
|
||||||
|
placement: 'below',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if height above 40 put text to above bar
|
||||||
|
if (height > 40) {
|
||||||
|
const maxTitleHeightRatio = 0.35;
|
||||||
|
const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
color: color,
|
fontSize: titleHeight / LINE_HEIGHT,
|
||||||
fontSize: fontSize + 'px',
|
width: 0,
|
||||||
|
height: titleHeight,
|
||||||
|
placement: 'above',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// title to left of bar scenario
|
||||||
* Return width or height depending on viz orientation
|
const maxTitleHeightRatio = 0.6;
|
||||||
* */
|
const maxTitleWidthRatio = 0.2;
|
||||||
get size() {
|
const titleHeight = Math.max(height * maxTitleHeightRatio, MIN_VALUE_HEIGHT);
|
||||||
const { height, width } = this.props;
|
|
||||||
return this.isVertical ? height : width;
|
return {
|
||||||
|
fontSize: titleHeight / LINE_HEIGHT,
|
||||||
|
height: 0,
|
||||||
|
width: Math.min(Math.max(width * maxTitleWidthRatio, 50), 200),
|
||||||
|
placement: 'left',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTitleStyles(props: Props): { wrapper: CSSProperties; title: CSSProperties } {
|
||||||
|
const wrapperStyles: CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleDim = calculateTitleDimensions(props);
|
||||||
|
|
||||||
|
const titleStyles: CSSProperties = {
|
||||||
|
fontSize: `${titleDim.fontSize}px`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
alignSelf: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isVertical(props)) {
|
||||||
|
wrapperStyles.flexDirection = 'column-reverse';
|
||||||
|
titleStyles.textAlign = 'center';
|
||||||
|
} else {
|
||||||
|
if (titleDim.placement === 'above') {
|
||||||
|
wrapperStyles.flexDirection = 'column';
|
||||||
|
} else {
|
||||||
|
wrapperStyles.flexDirection = 'row';
|
||||||
|
|
||||||
|
titleStyles.width = `${titleDim.width}px`;
|
||||||
|
titleStyles.textAlign = 'right';
|
||||||
|
titleStyles.marginRight = '10px';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isVertical() {
|
return {
|
||||||
return this.props.orientation === VizOrientation.Vertical;
|
wrapper: wrapperStyles,
|
||||||
|
title: titleStyles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BasicAndGradientStyles {
|
||||||
|
wrapper: CSSProperties;
|
||||||
|
bar: CSSProperties;
|
||||||
|
value: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarAndValueDimensions {
|
||||||
|
valueWidth: number;
|
||||||
|
valueHeight: number;
|
||||||
|
maxBarWidth: number;
|
||||||
|
maxBarHeight: number;
|
||||||
|
wrapperHeight: number;
|
||||||
|
wrapperWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
|
||||||
|
const { height, width } = props;
|
||||||
|
const titleDim = calculateTitleDimensions(props);
|
||||||
|
|
||||||
|
let maxBarHeight = 0;
|
||||||
|
let maxBarWidth = 0;
|
||||||
|
let valueHeight = 0;
|
||||||
|
let valueWidth = 0;
|
||||||
|
let wrapperWidth = 0;
|
||||||
|
let wrapperHeight = 0;
|
||||||
|
|
||||||
|
if (isVertical(props)) {
|
||||||
|
valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT);
|
||||||
|
valueWidth = width;
|
||||||
|
maxBarHeight = height - (titleDim.height + valueHeight);
|
||||||
|
maxBarWidth = width;
|
||||||
|
wrapperWidth = width;
|
||||||
|
wrapperHeight = height - titleDim.height;
|
||||||
|
} else {
|
||||||
|
valueHeight = height - titleDim.height;
|
||||||
|
valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), MIN_VALUE_WIDTH);
|
||||||
|
maxBarHeight = height - titleDim.height;
|
||||||
|
maxBarWidth = width - valueWidth - titleDim.width;
|
||||||
|
|
||||||
|
if (titleDim.placement === 'above') {
|
||||||
|
wrapperWidth = width;
|
||||||
|
wrapperHeight = height - titleDim.height;
|
||||||
|
} else {
|
||||||
|
wrapperWidth = width - titleDim.width;
|
||||||
|
wrapperHeight = height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBarGradient(maxSize: number): string {
|
return {
|
||||||
const { minValue, maxValue, thresholds, value } = this.props;
|
valueWidth,
|
||||||
const cssDirection = this.isVertical ? '0deg' : '90deg';
|
valueHeight,
|
||||||
|
maxBarWidth,
|
||||||
|
maxBarHeight,
|
||||||
|
wrapperHeight,
|
||||||
|
wrapperWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only exported to for unit test
|
||||||
|
*/
|
||||||
|
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
|
||||||
|
const { displayMode, maxValue, minValue, value } = props;
|
||||||
|
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
|
||||||
|
|
||||||
|
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
|
||||||
|
const valueColor = getValueColor(props);
|
||||||
|
const valueStyles = getValueStyles(value.text, valueColor, valueWidth, valueHeight);
|
||||||
|
const isBasic = displayMode === 'basic';
|
||||||
|
|
||||||
|
const wrapperStyles: CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
};
|
||||||
|
|
||||||
|
const barStyles: CSSProperties = {
|
||||||
|
borderRadius: '3px',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isVertical(props)) {
|
||||||
|
const barHeight = Math.max(valuePercent * maxBarHeight, 1);
|
||||||
|
|
||||||
|
// vertical styles
|
||||||
|
wrapperStyles.flexDirection = 'column';
|
||||||
|
wrapperStyles.justifyContent = 'flex-end';
|
||||||
|
|
||||||
|
barStyles.transition = 'height 1s';
|
||||||
|
barStyles.height = `${barHeight}px`;
|
||||||
|
barStyles.width = `${maxBarWidth}px`;
|
||||||
|
|
||||||
|
// value styles centered
|
||||||
|
valueStyles.justifyContent = 'center';
|
||||||
|
|
||||||
|
if (isBasic) {
|
||||||
|
// Basic styles
|
||||||
|
barStyles.background = `${tinycolor(valueColor)
|
||||||
|
.setAlpha(0.25)
|
||||||
|
.toRgbString()}`;
|
||||||
|
barStyles.borderTop = `2px solid ${valueColor}`;
|
||||||
|
} else {
|
||||||
|
// Gradient styles
|
||||||
|
barStyles.background = getBarGradient(props, maxBarHeight);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const barWidth = Math.max(valuePercent * maxBarWidth, 1);
|
||||||
|
|
||||||
|
// Custom styles for horizontal orientation
|
||||||
|
wrapperStyles.flexDirection = 'row-reverse';
|
||||||
|
wrapperStyles.justifyContent = 'flex-end';
|
||||||
|
wrapperStyles.alignItems = 'center';
|
||||||
|
|
||||||
|
barStyles.transition = 'width 1s';
|
||||||
|
barStyles.height = `${maxBarHeight}px`;
|
||||||
|
barStyles.width = `${barWidth}px`;
|
||||||
|
barStyles.marginRight = '10px';
|
||||||
|
|
||||||
|
if (isBasic) {
|
||||||
|
// Basic styles
|
||||||
|
barStyles.background = `${tinycolor(valueColor)
|
||||||
|
.setAlpha(0.25)
|
||||||
|
.toRgbString()}`;
|
||||||
|
barStyles.borderRight = `2px solid ${valueColor}`;
|
||||||
|
} else {
|
||||||
|
// Gradient styles
|
||||||
|
barStyles.background = getBarGradient(props, maxBarWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper: wrapperStyles,
|
||||||
|
bar: barStyles,
|
||||||
|
value: valueStyles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only exported to for unit test
|
||||||
|
*/
|
||||||
|
export function getBarGradient(props: Props, maxSize: number): string {
|
||||||
|
const { minValue, maxValue, thresholds, value } = props;
|
||||||
|
const cssDirection = isVertical(props) ? '0deg' : '90deg';
|
||||||
|
|
||||||
let gradient = '';
|
let gradient = '';
|
||||||
let lastpos = 0;
|
let lastpos = 0;
|
||||||
@ -116,191 +458,64 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return gradient + ')';
|
return gradient + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBasicAndGradientBars(): ReactNode {
|
/**
|
||||||
const { height, width, displayMode, maxValue, minValue, value } = this.props;
|
* Only exported to for unit test
|
||||||
|
*/
|
||||||
|
export function getValueColor(props: Props): string {
|
||||||
|
const { thresholds, theme, value } = props;
|
||||||
|
|
||||||
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
|
const activeThreshold = getThresholdForValue(thresholds, value.numeric);
|
||||||
const maxSize = this.size * BAR_SIZE_RATIO;
|
|
||||||
const barSize = Math.max(valuePercent * maxSize, 0);
|
|
||||||
const colors = this.getValueColors();
|
|
||||||
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
|
|
||||||
const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
|
|
||||||
const isBasic = displayMode === 'basic';
|
|
||||||
|
|
||||||
const containerStyles: CSSProperties = {
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
display: 'flex',
|
|
||||||
};
|
|
||||||
|
|
||||||
const barStyles: CSSProperties = {
|
|
||||||
borderRadius: '3px',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.isVertical) {
|
|
||||||
// Custom styles for vertical orientation
|
|
||||||
containerStyles.flexDirection = 'column';
|
|
||||||
containerStyles.justifyContent = 'flex-end';
|
|
||||||
barStyles.transition = 'height 1s';
|
|
||||||
barStyles.height = `${barSize}px`;
|
|
||||||
barStyles.width = `${width}px`;
|
|
||||||
if (isBasic) {
|
|
||||||
// Basic styles
|
|
||||||
barStyles.background = `${colors.background}`;
|
|
||||||
barStyles.border = `1px solid ${colors.border}`;
|
|
||||||
barStyles.boxShadow = `0 0 4px ${colors.border}`;
|
|
||||||
} else {
|
|
||||||
// Gradient styles
|
|
||||||
barStyles.background = this.getBarGradient(maxSize);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Custom styles for horizontal orientation
|
|
||||||
containerStyles.flexDirection = 'row-reverse';
|
|
||||||
containerStyles.justifyContent = 'flex-end';
|
|
||||||
containerStyles.alignItems = 'center';
|
|
||||||
barStyles.transition = 'width 1s';
|
|
||||||
barStyles.height = `${height}px`;
|
|
||||||
barStyles.width = `${barSize}px`;
|
|
||||||
barStyles.marginRight = '10px';
|
|
||||||
|
|
||||||
if (isBasic) {
|
|
||||||
// Basic styles
|
|
||||||
barStyles.background = `${colors.background}`;
|
|
||||||
barStyles.border = `1px solid ${colors.border}`;
|
|
||||||
barStyles.boxShadow = `0 0 4px ${colors.border}`;
|
|
||||||
} else {
|
|
||||||
// Gradient styles
|
|
||||||
barStyles.background = this.getBarGradient(maxSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={containerStyles}>
|
|
||||||
<div className="bar-gauge__value" style={valueStyles}>
|
|
||||||
{value.text}
|
|
||||||
</div>
|
|
||||||
<div style={barStyles} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCellColor(positionValue: TimeSeriesValue): CellColors {
|
|
||||||
const { thresholds, theme, value } = this.props;
|
|
||||||
const activeThreshold = getThresholdForValue(thresholds, positionValue);
|
|
||||||
|
|
||||||
if (activeThreshold !== null) {
|
if (activeThreshold !== null) {
|
||||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||||
|
|
||||||
// if we are past real value the cell is not "on"
|
|
||||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
|
||||||
return {
|
|
||||||
background: tinycolor(color)
|
|
||||||
.setAlpha(0.15)
|
|
||||||
.toRgbString(),
|
|
||||||
border: 'transparent',
|
|
||||||
isLit: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
background: tinycolor(color)
|
|
||||||
.setAlpha(0.85)
|
|
||||||
.toRgbString(),
|
|
||||||
backgroundShade: tinycolor(color)
|
|
||||||
.setAlpha(0.55)
|
|
||||||
.toRgbString(),
|
|
||||||
border: tinycolor(color)
|
|
||||||
.setAlpha(0.9)
|
|
||||||
.toRgbString(),
|
|
||||||
isLit: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return getColorFromHexRgbOrName('gray', theme.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only exported to for unit test
|
||||||
|
*/
|
||||||
|
function getValueStyles(value: string, color: string, width: number, height: number): CSSProperties {
|
||||||
|
const heightFont = height / LINE_HEIGHT;
|
||||||
|
const guess = width / (value.length * 1.1);
|
||||||
|
const fontSize = Math.min(Math.max(guess, 14), heightFont);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: 'gray',
|
color: color,
|
||||||
border: 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRetroBars(): ReactNode {
|
|
||||||
const { height, width, maxValue, minValue, value } = this.props;
|
|
||||||
|
|
||||||
const valueRange = maxValue - minValue;
|
|
||||||
const maxSize = this.size * BAR_SIZE_RATIO;
|
|
||||||
const cellSpacing = 5;
|
|
||||||
const cellCount = maxSize / 20;
|
|
||||||
const cellSize = (maxSize - cellSpacing * cellCount) / cellCount;
|
|
||||||
const colors = this.getValueColors();
|
|
||||||
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
|
|
||||||
const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
|
|
||||||
|
|
||||||
const containerStyles: CSSProperties = {
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
width: `${width}px`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: fontSize.toFixed(2) + 'px',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isVertical) {
|
|
||||||
containerStyles.flexDirection = 'column-reverse';
|
|
||||||
containerStyles.alignItems = 'center';
|
|
||||||
valueStyles.marginBottom = '20px';
|
|
||||||
} else {
|
|
||||||
containerStyles.flexDirection = 'row';
|
|
||||||
containerStyles.alignItems = 'center';
|
|
||||||
valueStyles.marginLeft = '20px';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cells: JSX.Element[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < cellCount; i++) {
|
|
||||||
const currentValue = (valueRange / cellCount) * i;
|
|
||||||
const cellColor = this.getCellColor(currentValue);
|
|
||||||
const cellStyles: CSSProperties = {
|
|
||||||
borderRadius: '2px',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cellColor.isLit) {
|
|
||||||
cellStyles.boxShadow = `0 0 4px ${cellColor.border}`;
|
|
||||||
cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`;
|
|
||||||
} else {
|
|
||||||
cellStyles.backgroundColor = cellColor.background;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isVertical) {
|
|
||||||
cellStyles.height = `${cellSize}px`;
|
|
||||||
cellStyles.width = `${width}px`;
|
|
||||||
cellStyles.marginTop = `${cellSpacing}px`;
|
|
||||||
} else {
|
|
||||||
cellStyles.width = `${cellSize}px`;
|
|
||||||
cellStyles.height = `${height}px`;
|
|
||||||
cellStyles.marginRight = `${cellSpacing}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
cells.push(<div style={cellStyles} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={containerStyles}>
|
|
||||||
{cells}
|
|
||||||
<div className="bar-gauge__value" style={valueStyles}>
|
|
||||||
{value.text}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BarColors {
|
// let canvasElement: HTMLCanvasElement | null = null;
|
||||||
value: string;
|
//
|
||||||
background: string;
|
// interface TextDimensions {
|
||||||
border: string;
|
// width: number;
|
||||||
}
|
// height: number;
|
||||||
|
// }
|
||||||
interface CellColors {
|
//
|
||||||
background: string;
|
// /**
|
||||||
backgroundShade?: string;
|
// * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||||
border: string;
|
// *
|
||||||
isLit?: boolean;
|
// * @param {String} text The text to be rendered.
|
||||||
}
|
// * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
|
||||||
|
// *
|
||||||
|
// * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||||
|
// */
|
||||||
|
// function getTextWidth(text: string): number {
|
||||||
|
// // re-use canvas object for better performance
|
||||||
|
// canvasElement = canvasElement || document.createElement('canvas');
|
||||||
|
// const context = canvasElement.getContext('2d');
|
||||||
|
// if (context) {
|
||||||
|
// context.font = 'normal 16px Roboto';
|
||||||
|
// const metrics = context.measureText(text);
|
||||||
|
// return metrics.width;
|
||||||
|
// }
|
||||||
|
// return 16;
|
||||||
|
// }
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Render BarGauge with basic options should render 1`] = `
|
exports[`BarGauge Render with basic options should render 1`] = `
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"flexDirection": "row-reverse",
|
"flexDirection": "row-reverse",
|
||||||
"height": "300px",
|
|
||||||
"justifyContent": "flex-end",
|
"justifyContent": "flex-end",
|
||||||
"width": "300px",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -17,8 +15,12 @@ exports[`Render BarGauge with basic options should render 1`] = `
|
|||||||
className="bar-gauge__value"
|
className="bar-gauge__value"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"color": "#7EB26D",
|
"alignItems": "center",
|
||||||
"fontSize": "27.27272727272727px",
|
"color": "#73BF69",
|
||||||
|
"display": "flex",
|
||||||
|
"fontSize": "27.27px",
|
||||||
|
"height": "300px",
|
||||||
|
"width": "60px",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -27,10 +29,9 @@ exports[`Render BarGauge with basic options should render 1`] = `
|
|||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"background": "rgba(126, 178, 109, 0.15)",
|
"background": "rgba(115, 191, 105, 0.25)",
|
||||||
"border": "1px solid #7EB26D",
|
|
||||||
"borderRadius": "3px",
|
"borderRadius": "3px",
|
||||||
"boxShadow": "0 0 4px #7EB26D",
|
"borderRight": "2px solid #73BF69",
|
||||||
"height": "300px",
|
"height": "300px",
|
||||||
"marginRight": "10px",
|
"marginRight": "10px",
|
||||||
"transition": "width 1s",
|
"transition": "width 1s",
|
||||||
|
@ -31,7 +31,7 @@ export interface SingleStatValueOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GetSingleStatDisplayValueOptions {
|
export interface GetSingleStatDisplayValueOptions {
|
||||||
data: SeriesData[];
|
data?: SeriesData[];
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
@ -55,6 +55,7 @@ export const getSingleStatDisplayValues = (options: GetSingleStatDisplayValueOpt
|
|||||||
|
|
||||||
const values: DisplayValue[] = [];
|
const values: DisplayValue[] = [];
|
||||||
|
|
||||||
|
if (data) {
|
||||||
for (const series of data) {
|
for (const series of data) {
|
||||||
if (stat === 'name') {
|
if (stat === 'name') {
|
||||||
values.push(display(series.name));
|
values.push(display(series.name));
|
||||||
@ -71,11 +72,14 @@ export const getSingleStatDisplayValues = (options: GetSingleStatDisplayValueOpt
|
|||||||
stats: [stat], // The stats to calculate
|
stats: [stat], // The stats to calculate
|
||||||
nullValueMode: NullValueMode.Null,
|
nullValueMode: NullValueMode.Null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayValue = display(stats[stat]);
|
const displayValue = display(stats[stat]);
|
||||||
|
displayValue.title = series.name;
|
||||||
values.push(displayValue);
|
values.push(displayValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
values.push({
|
values.push({
|
||||||
|
@ -1,23 +1,47 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { VizOrientation } from '../../types';
|
import { VizOrientation } from '../../types';
|
||||||
|
|
||||||
interface RenderProps<T> {
|
|
||||||
vizWidth: number;
|
|
||||||
vizHeight: number;
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
children: (renderProps: RenderProps<T>) => JSX.Element | JSX.Element[];
|
renderValue: (value: T, width: number, height: number) => JSX.Element;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
values: T[];
|
source: any; // If this changes, new values will be requested
|
||||||
|
getValues: () => T[];
|
||||||
|
renderCounter: number; // force update of values & render
|
||||||
orientation: VizOrientation;
|
orientation: VizOrientation;
|
||||||
|
itemSpacing?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPACE_BETWEEN = 10;
|
interface DefaultProps {
|
||||||
|
itemSpacing: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropsWithDefaults<T> = Props<T> & DefaultProps;
|
||||||
|
|
||||||
|
interface State<T> {
|
||||||
|
values: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VizRepeater<T> extends PureComponent<Props<T>, State<T>> {
|
||||||
|
static defaultProps: DefaultProps = {
|
||||||
|
itemSpacing: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props<T>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
values: props.getValues(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props<T>) {
|
||||||
|
const { renderCounter, source } = this.props;
|
||||||
|
if (renderCounter !== prevProps.renderCounter || source !== prevProps.source) {
|
||||||
|
this.setState({ values: this.props.getValues() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class VizRepeater<T> extends PureComponent<Props<T>> {
|
|
||||||
getOrientation(): VizOrientation {
|
getOrientation(): VizOrientation {
|
||||||
const { orientation, width, height } = this.props;
|
const { orientation, width, height } = this.props;
|
||||||
|
|
||||||
@ -33,7 +57,8 @@ export class VizRepeater<T> extends PureComponent<Props<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, height, values, width } = this.props;
|
const { renderValue, height, width, itemSpacing } = this.props as PropsWithDefaults<T>;
|
||||||
|
const { values } = this.state;
|
||||||
const orientation = this.getOrientation();
|
const orientation = this.getOrientation();
|
||||||
|
|
||||||
const itemStyles: React.CSSProperties = {
|
const itemStyles: React.CSSProperties = {
|
||||||
@ -49,14 +74,14 @@ export class VizRepeater<T> extends PureComponent<Props<T>> {
|
|||||||
|
|
||||||
if (orientation === VizOrientation.Horizontal) {
|
if (orientation === VizOrientation.Horizontal) {
|
||||||
repeaterStyle.flexDirection = 'column';
|
repeaterStyle.flexDirection = 'column';
|
||||||
itemStyles.margin = `${SPACE_BETWEEN / 2}px 0`;
|
itemStyles.margin = `${itemSpacing / 2}px 0`;
|
||||||
vizWidth = width;
|
vizWidth = width;
|
||||||
vizHeight = height / values.length - SPACE_BETWEEN;
|
vizHeight = height / values.length - itemSpacing;
|
||||||
} else {
|
} else {
|
||||||
repeaterStyle.flexDirection = 'row';
|
repeaterStyle.flexDirection = 'row';
|
||||||
itemStyles.margin = `0 ${SPACE_BETWEEN / 2}px`;
|
itemStyles.margin = `0 ${itemSpacing / 2}px`;
|
||||||
vizHeight = height;
|
vizHeight = height;
|
||||||
vizWidth = width / values.length - SPACE_BETWEEN;
|
vizWidth = width / values.length - itemSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
itemStyles.width = `${vizWidth}px`;
|
itemStyles.width = `${vizWidth}px`;
|
||||||
@ -67,7 +92,7 @@ export class VizRepeater<T> extends PureComponent<Props<T>> {
|
|||||||
{values.map((value, index) => {
|
{values.map((value, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={index} style={itemStyles}>
|
<div key={index} style={itemStyles}>
|
||||||
{children({ vizHeight, vizWidth, value })}
|
{renderValue(value, vizWidth, vizHeight)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { DisplayValue, PanelProps, BarGauge, getSingleStatDisplayValues } from '@grafana/ui';
|
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { BarGauge, VizRepeater, getSingleStatDisplayValues } from '@grafana/ui/src/components';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { BarGaugeOptions } from './types';
|
import { BarGaugeOptions } from './types';
|
||||||
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
import { PanelProps, DisplayValue } from '@grafana/ui/src/types';
|
||||||
|
|
||||||
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||||
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
||||||
@ -21,12 +23,13 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
|||||||
orientation={options.orientation}
|
orientation={options.orientation}
|
||||||
thresholds={options.thresholds}
|
thresholds={options.thresholds}
|
||||||
theme={config.theme}
|
theme={config.theme}
|
||||||
|
itemSpacing={this.getItemSpacing()}
|
||||||
displayMode={options.displayMode}
|
displayMode={options.displayMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getProcessedValues = (): DisplayValue[] => {
|
getValues = (): DisplayValue[] => {
|
||||||
return getSingleStatDisplayValues({
|
return getSingleStatDisplayValues({
|
||||||
valueMappings: this.props.options.valueMappings,
|
valueMappings: this.props.options.valueMappings,
|
||||||
thresholds: this.props.options.thresholds,
|
thresholds: this.props.options.thresholds,
|
||||||
@ -37,16 +40,25 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getItemSpacing(): number {
|
||||||
|
if (this.props.options.displayMode === 'lcd') {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { height, width, options, data, renderCounter } = this.props;
|
const { height, width, options, data, renderCounter } = this.props;
|
||||||
return (
|
return (
|
||||||
<ProcessedValuesRepeater
|
<VizRepeater
|
||||||
getProcessedValues={this.getProcessedValues}
|
source={data}
|
||||||
|
getValues={this.getValues}
|
||||||
renderValue={this.renderValue}
|
renderValue={this.renderValue}
|
||||||
|
renderCounter={renderCounter}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
source={data}
|
itemSpacing={this.getItemSpacing()}
|
||||||
renderCounter={renderCounter}
|
|
||||||
orientation={options.orientation}
|
orientation={options.orientation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -9,8 +9,7 @@ import { Gauge } from '@grafana/ui';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
import { DisplayValue, PanelProps, getSingleStatDisplayValues } from '@grafana/ui';
|
import { DisplayValue, PanelProps, getSingleStatDisplayValues, VizRepeater } from '@grafana/ui';
|
||||||
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
|
||||||
|
|
||||||
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||||
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
||||||
@ -31,7 +30,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getProcessedValues = (): DisplayValue[] => {
|
getValues = (): DisplayValue[] => {
|
||||||
return getSingleStatDisplayValues({
|
return getSingleStatDisplayValues({
|
||||||
valueMappings: this.props.options.valueMappings,
|
valueMappings: this.props.options.valueMappings,
|
||||||
thresholds: this.props.options.thresholds,
|
thresholds: this.props.options.thresholds,
|
||||||
@ -45,8 +44,8 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
|||||||
render() {
|
render() {
|
||||||
const { height, width, options, data, renderCounter } = this.props;
|
const { height, width, options, data, renderCounter } = this.props;
|
||||||
return (
|
return (
|
||||||
<ProcessedValuesRepeater
|
<VizRepeater
|
||||||
getProcessedValues={this.getProcessedValues}
|
getValues={this.getValues}
|
||||||
renderValue={this.renderValue}
|
renderValue={this.renderValue}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { VizOrientation } from '@grafana/ui';
|
|
||||||
import { VizRepeater } from '@grafana/ui';
|
|
||||||
|
|
||||||
export interface Props<T> {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
orientation: VizOrientation;
|
|
||||||
source: any; // If this changes, the values will be processed
|
|
||||||
renderCounter: number; // change to force processing
|
|
||||||
|
|
||||||
getProcessedValues: () => T[];
|
|
||||||
renderValue: (value: T, width: number, height: number) => JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State<T> {
|
|
||||||
values: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is essentially a cache of processed values. This checks for changes
|
|
||||||
* to the source and then saves the processed values in the State
|
|
||||||
*/
|
|
||||||
export class ProcessedValuesRepeater<T> extends PureComponent<Props<T>, State<T>> {
|
|
||||||
constructor(props: Props<T>) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
values: props.getProcessedValues(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props<T>) {
|
|
||||||
const { renderCounter, source } = this.props;
|
|
||||||
if (renderCounter !== prevProps.renderCounter || source !== prevProps.source) {
|
|
||||||
this.setState({ values: this.props.getProcessedValues() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { orientation, height, width, renderValue } = this.props;
|
|
||||||
const { values } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VizRepeater height={height} width={width} values={values} orientation={orientation}>
|
|
||||||
{({ vizHeight, vizWidth, value }) => renderValue(value, vizWidth, vizHeight)}
|
|
||||||
</VizRepeater>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,11 +6,11 @@ import { config } from 'app/core/config';
|
|||||||
import { getFlotPairs } from '@grafana/ui/src/utils/flotPairs';
|
import { getFlotPairs } from '@grafana/ui/src/utils/flotPairs';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
import { VizRepeater } from '@grafana/ui/src/components';
|
||||||
|
import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { SingleStatOptions } from './types';
|
import { SingleStatOptions } from './types';
|
||||||
import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
|
|
||||||
import {
|
import {
|
||||||
DisplayValue,
|
DisplayValue,
|
||||||
PanelProps,
|
PanelProps,
|
||||||
@ -34,7 +34,7 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
|
|||||||
return <BigValue {...value} width={width} height={height} theme={config.theme} />;
|
return <BigValue {...value} width={width} height={height} theme={config.theme} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
getProcessedValues = (): SingleStatDisplay[] => {
|
getValues = (): SingleStatDisplay[] => {
|
||||||
const { data, replaceVariables, options, timeRange } = this.props;
|
const { data, replaceVariables, options, timeRange } = this.props;
|
||||||
const { valueOptions, valueMappings } = options;
|
const { valueOptions, valueMappings } = options;
|
||||||
|
|
||||||
@ -127,8 +127,8 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
|
|||||||
render() {
|
render() {
|
||||||
const { height, width, options, data, renderCounter } = this.props;
|
const { height, width, options, data, renderCounter } = this.props;
|
||||||
return (
|
return (
|
||||||
<ProcessedValuesRepeater
|
<VizRepeater
|
||||||
getProcessedValues={this.getProcessedValues}
|
getValues={this.getValues}
|
||||||
renderValue={this.renderValue}
|
renderValue={this.renderValue}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
Loading…
Reference in New Issue
Block a user