mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Feat: Singlestat panel react progress & refactorings (#16039)
* big value component * big value component * editor for font and sparkline * less logging * remove sparkline from storybook * add display value link wrapper * follow tooltip * follow tooltip * merge master * Just minor refactoring * use series after last merge * Refactoring: moving shared singlestat stuff to grafana-ui * Refactor: Moved final getSingleStatDisplayValues func
This commit is contained in:
committed by
Torkel Ödegaard
parent
1d955a8762
commit
c8b2102500
@@ -0,0 +1,37 @@
|
|||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { number, text } from '@storybook/addon-knobs';
|
||||||
|
import { BigValue } from './BigValue';
|
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||||
|
|
||||||
|
const getKnobs = () => {
|
||||||
|
return {
|
||||||
|
value: text('value', 'Hello'),
|
||||||
|
valueFontSize: number('valueFontSize', 120),
|
||||||
|
prefix: text('prefix', ''),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const BigValueStories = storiesOf('UI/BigValue', module);
|
||||||
|
|
||||||
|
BigValueStories.addDecorator(withCenteredStory);
|
||||||
|
|
||||||
|
BigValueStories.add('Singlestat viz', () => {
|
||||||
|
const { value, prefix, valueFontSize } = getKnobs();
|
||||||
|
|
||||||
|
return renderComponentWithTheme(BigValue, {
|
||||||
|
width: 300,
|
||||||
|
height: 250,
|
||||||
|
value: {
|
||||||
|
text: value,
|
||||||
|
numeric: NaN,
|
||||||
|
fontSize: valueFontSize + '%',
|
||||||
|
},
|
||||||
|
prefix: prefix
|
||||||
|
? {
|
||||||
|
text: prefix,
|
||||||
|
numeric: NaN,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { BigValue, Props } from './BigValue';
|
||||||
|
import { getTheme } from '../../themes/index';
|
||||||
|
|
||||||
|
jest.mock('jquery', () => ({
|
||||||
|
plot: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
height: 300,
|
||||||
|
width: 300,
|
||||||
|
value: {
|
||||||
|
text: '25',
|
||||||
|
numeric: 25,
|
||||||
|
},
|
||||||
|
theme: getTheme(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<BigValue {...props} />);
|
||||||
|
const instance = wrapper.instance() as BigValue;
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
wrapper,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render BarGauge with basic options', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
expect(wrapper).toBeDefined();
|
||||||
|
// expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
134
packages/grafana-ui/src/components/BigValue/BigValue.tsx
Normal file
134
packages/grafana-ui/src/components/BigValue/BigValue.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// Library
|
||||||
|
import React, { PureComponent, ReactNode, CSSProperties } from 'react';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { getColorFromHexRgbOrName } from '../../utils';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { Themeable, DisplayValue } from '../../types';
|
||||||
|
|
||||||
|
export interface BigValueSparkline {
|
||||||
|
data: any[][]; // [[number,number]]
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
full: boolean; // full height
|
||||||
|
fillColor: string;
|
||||||
|
lineColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props extends Themeable {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
value: DisplayValue;
|
||||||
|
prefix?: DisplayValue;
|
||||||
|
suffix?: DisplayValue;
|
||||||
|
sparkline?: BigValueSparkline;
|
||||||
|
backgroundColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This visualization is still in POC state, needed more tests & better structure
|
||||||
|
*/
|
||||||
|
export class BigValue extends PureComponent<Props> {
|
||||||
|
canvasElement: any;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const { sparkline, theme } = this.props;
|
||||||
|
|
||||||
|
if (sparkline && this.canvasElement) {
|
||||||
|
const { data, minX, maxX, fillColor, lineColor } = sparkline;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
legend: { show: false },
|
||||||
|
series: {
|
||||||
|
lines: {
|
||||||
|
show: true,
|
||||||
|
fill: 1,
|
||||||
|
zero: false,
|
||||||
|
lineWidth: 1,
|
||||||
|
fillColor: getColorFromHexRgbOrName(fillColor, theme.type),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxes: { show: false },
|
||||||
|
xaxis: {
|
||||||
|
show: false,
|
||||||
|
min: minX,
|
||||||
|
max: maxX,
|
||||||
|
},
|
||||||
|
grid: { hoverable: false, show: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const plotSeries = {
|
||||||
|
data,
|
||||||
|
color: getColorFromHexRgbOrName(lineColor, theme.type),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
$.plot(this.canvasElement, [plotSeries], options);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('sparkline rendering error', err, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderText = (value?: DisplayValue, padding?: string): ReactNode => {
|
||||||
|
if (!value || !value.text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const css: CSSProperties = {};
|
||||||
|
if (padding) {
|
||||||
|
css.padding = padding;
|
||||||
|
}
|
||||||
|
if (value.color) {
|
||||||
|
css.color = value.color;
|
||||||
|
}
|
||||||
|
if (value.fontSize) {
|
||||||
|
css.fontSize = value.fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span style={css}>{value.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { height, width, value, prefix, suffix, sparkline, backgroundColor } = this.props;
|
||||||
|
|
||||||
|
const plotCss: CSSProperties = {};
|
||||||
|
plotCss.position = 'absolute';
|
||||||
|
|
||||||
|
if (sparkline) {
|
||||||
|
if (sparkline.full) {
|
||||||
|
plotCss.bottom = '5px';
|
||||||
|
plotCss.left = '-5px';
|
||||||
|
plotCss.width = width - 10 + 'px';
|
||||||
|
const dynamicHeightMargin = height <= 100 ? 5 : Math.round(height / 100) * 15 + 5;
|
||||||
|
plotCss.height = height - dynamicHeightMargin + 'px';
|
||||||
|
} else {
|
||||||
|
plotCss.bottom = '0px';
|
||||||
|
plotCss.left = '-5px';
|
||||||
|
plotCss.width = width - 10 + 'px';
|
||||||
|
plotCss.height = Math.floor(height * 0.25) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="big-value" style={{ width, height, backgroundColor }}>
|
||||||
|
<span className="big-value__value">
|
||||||
|
{this.renderText(prefix, '0px 2px 0px 0px')}
|
||||||
|
{this.renderText(value)}
|
||||||
|
{this.renderText(suffix)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{sparkline && <div style={plotCss} ref={element => (this.canvasElement = element)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/grafana-ui/src/components/BigValue/_BigValue.scss
Normal file
15
packages/grafana-ui/src/components/BigValue/_BigValue.scss
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.big-value {
|
||||||
|
position: relative;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-value__value {
|
||||||
|
line-height: 1;
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 3em;
|
||||||
|
font-weight: $font-weight-semi-bold;
|
||||||
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent, ChangeEvent } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { FormField, FormLabel, PanelOptionsGroup, StatsPicker, UnitPicker, StatID } from '@grafana/ui';
|
import {
|
||||||
|
FormField,
|
||||||
|
FormLabel,
|
||||||
|
PanelOptionsGroup,
|
||||||
|
StatsPicker,
|
||||||
|
UnitPicker,
|
||||||
|
StatID,
|
||||||
|
SelectOptionItem,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { SingleStatValueOptions } from './types';
|
import { SingleStatValueOptions } from './shared';
|
||||||
|
|
||||||
const labelWidth = 6;
|
const labelWidth = 6;
|
||||||
|
|
||||||
@@ -15,15 +23,15 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SingleStatValueEditor extends PureComponent<Props> {
|
export class SingleStatValueEditor extends PureComponent<Props> {
|
||||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
onUnitChange = (unit: SelectOptionItem) => this.props.onChange({ ...this.props.options, unit: unit.value });
|
||||||
|
|
||||||
onStatsChange = stats => {
|
onStatsChange = (stats: string[]) => {
|
||||||
const stat = stats[0] || StatID.mean;
|
const stat = stats[0] || StatID.mean;
|
||||||
this.props.onChange({ ...this.props.options, stat });
|
this.props.onChange({ ...this.props.options, stat });
|
||||||
};
|
};
|
||||||
|
|
||||||
onDecimalChange = event => {
|
onDecimalChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!isNaN(event.target.value)) {
|
if (!isNaN(parseInt(event.target.value, 10))) {
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.options,
|
...this.props.options,
|
||||||
decimals: parseInt(event.target.value, 10),
|
decimals: parseInt(event.target.value, 10),
|
||||||
@@ -36,14 +44,16 @@ export class SingleStatValueEditor extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
|
onPrefixChange = (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
|
this.props.onChange({ ...this.props.options, prefix: event.target.value });
|
||||||
|
onSuffixChange = (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
this.props.onChange({ ...this.props.options, suffix: event.target.value });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { stat, unit, decimals, prefix, suffix } = this.props.options;
|
const { stat, unit, decimals, prefix, suffix } = this.props.options;
|
||||||
|
|
||||||
let decimalsString = '';
|
let decimalsString = '';
|
||||||
if (Number.isFinite(decimals)) {
|
if (decimals !== null && decimals !== undefined && Number.isFinite(decimals as number)) {
|
||||||
decimalsString = decimals.toString();
|
decimalsString = decimals.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
122
packages/grafana-ui/src/components/SingleStatShared/shared.ts
Normal file
122
packages/grafana-ui/src/components/SingleStatShared/shared.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import {
|
||||||
|
ValueMapping,
|
||||||
|
Threshold,
|
||||||
|
VizOrientation,
|
||||||
|
PanelModel,
|
||||||
|
DisplayValue,
|
||||||
|
FieldType,
|
||||||
|
NullValueMode,
|
||||||
|
GrafanaTheme,
|
||||||
|
SeriesData,
|
||||||
|
InterpolateFunction,
|
||||||
|
} from '../../types';
|
||||||
|
import { getStatsCalculators, calculateStats } from '../../utils/statsCalculator';
|
||||||
|
import { getDisplayProcessor } from '../../utils/displayValue';
|
||||||
|
export { SingleStatValueEditor } from './SingleStatValueEditor';
|
||||||
|
|
||||||
|
export interface SingleStatBaseOptions {
|
||||||
|
valueMappings: ValueMapping[];
|
||||||
|
thresholds: Threshold[];
|
||||||
|
valueOptions: SingleStatValueOptions;
|
||||||
|
orientation: VizOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleStatValueOptions {
|
||||||
|
unit: string;
|
||||||
|
suffix: string;
|
||||||
|
stat: string;
|
||||||
|
prefix: string;
|
||||||
|
decimals?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSingleStatDisplayValueOptions {
|
||||||
|
data: SeriesData[];
|
||||||
|
theme: GrafanaTheme;
|
||||||
|
valueMappings: ValueMapping[];
|
||||||
|
thresholds: Threshold[];
|
||||||
|
valueOptions: SingleStatValueOptions;
|
||||||
|
replaceVariables: InterpolateFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSingleStatDisplayValues = (options: GetSingleStatDisplayValueOptions): DisplayValue[] => {
|
||||||
|
const { data, replaceVariables, valueOptions } = options;
|
||||||
|
const { unit, decimals, stat } = valueOptions;
|
||||||
|
|
||||||
|
const display = getDisplayProcessor({
|
||||||
|
unit,
|
||||||
|
decimals,
|
||||||
|
mappings: options.valueMappings,
|
||||||
|
thresholds: options.thresholds,
|
||||||
|
prefix: replaceVariables(valueOptions.prefix),
|
||||||
|
suffix: replaceVariables(valueOptions.suffix),
|
||||||
|
theme: options.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
const values: DisplayValue[] = [];
|
||||||
|
|
||||||
|
for (const series of data) {
|
||||||
|
if (stat === 'name') {
|
||||||
|
values.push(display(series.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < series.fields.length; i++) {
|
||||||
|
const column = series.fields[i];
|
||||||
|
|
||||||
|
// Show all fields that are not 'time'
|
||||||
|
if (column.type === FieldType.number) {
|
||||||
|
const stats = calculateStats({
|
||||||
|
series,
|
||||||
|
fieldIndex: i,
|
||||||
|
stats: [stat], // The stats to calculate
|
||||||
|
nullValueMode: NullValueMode.Null,
|
||||||
|
});
|
||||||
|
const displayValue = display(stats[stat]);
|
||||||
|
values.push(displayValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.length === 0) {
|
||||||
|
values.push({
|
||||||
|
numeric: 0,
|
||||||
|
text: 'No data',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
|
||||||
|
|
||||||
|
export const sharedSingleStatOptionsCheck = (
|
||||||
|
options: Partial<SingleStatBaseOptions> | any,
|
||||||
|
prevPluginId: string,
|
||||||
|
prevOptions: any
|
||||||
|
) => {
|
||||||
|
for (const k of optionsToKeep) {
|
||||||
|
if (prevOptions.hasOwnProperty(k)) {
|
||||||
|
options[k] = cloneDeep(prevOptions[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseOptions>) => {
|
||||||
|
const options = panel.options;
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
// This happens on the first load or when migrating from angular
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.valueOptions) {
|
||||||
|
// 6.1 renamed some stats, This makes sure they are up to date
|
||||||
|
// avg -> mean, current -> last, total -> sum
|
||||||
|
const { valueOptions } = options;
|
||||||
|
if (valueOptions && valueOptions.stat) {
|
||||||
|
valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import 'CustomScrollbar/CustomScrollbar';
|
@import 'CustomScrollbar/CustomScrollbar';
|
||||||
|
@import 'BigValue/BigValue';
|
||||||
@import 'DeleteButton/DeleteButton';
|
@import 'DeleteButton/DeleteButton';
|
||||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||||
@import 'Table/Table';
|
@import 'Table/Table';
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ export { StatsPicker } from './StatsPicker/StatsPicker';
|
|||||||
export { Input, InputStatus } from './Input/Input';
|
export { Input, InputStatus } from './Input/Input';
|
||||||
|
|
||||||
// Visualizations
|
// Visualizations
|
||||||
|
export { BigValue } from './BigValue/BigValue';
|
||||||
export { Gauge } from './Gauge/Gauge';
|
export { Gauge } from './Gauge/Gauge';
|
||||||
export { Graph } from './Graph/Graph';
|
export { Graph } from './Graph/Graph';
|
||||||
export { BarGauge } from './BarGauge/BarGauge';
|
export { BarGauge } from './BarGauge/BarGauge';
|
||||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||||
|
export * from './SingleStatShared/shared';
|
||||||
|
|
||||||
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface DisplayValue {
|
|||||||
numeric: number; // Use isNaN to check if it is a real number
|
numeric: number; // Use isNaN to check if it is a real number
|
||||||
color?: string; // color based on configs or Threshold
|
color?: string; // color based on configs or Threshold
|
||||||
title?: string;
|
title?: string;
|
||||||
|
fontSize?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DecimalInfo {
|
export interface DecimalInfo {
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui';
|
import { DisplayValue, PanelProps, BarGauge, getSingleStatDisplayValues } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { BarGaugeOptions } from './types';
|
import { BarGaugeOptions } from './types';
|
||||||
import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
|
|
||||||
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
||||||
|
|
||||||
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||||
@@ -28,7 +27,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getProcessedValues = (): DisplayValue[] => {
|
getProcessedValues = (): DisplayValue[] => {
|
||||||
return getSingleStatValues(this.props);
|
return getSingleStatDisplayValues({
|
||||||
|
valueMappings: this.props.options.valueMappings,
|
||||||
|
thresholds: this.props.options.thresholds,
|
||||||
|
valueOptions: this.props.options.valueOptions,
|
||||||
|
data: this.props.data,
|
||||||
|
theme: config.theme,
|
||||||
|
replaceVariables: this.props.replaceVariables,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -2,13 +2,19 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
import {
|
||||||
|
ThresholdsEditor,
|
||||||
|
ValueMappingsEditor,
|
||||||
|
PanelOptionsGrid,
|
||||||
|
PanelOptionsGroup,
|
||||||
|
FormField,
|
||||||
|
SingleStatValueOptions,
|
||||||
|
SingleStatValueEditor,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
|
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
|
||||||
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
||||||
import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
|
|
||||||
import { SingleStatValueOptions } from '../singlestat2/types';
|
|
||||||
|
|
||||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { ReactPanelPlugin } from '@grafana/ui';
|
import { ReactPanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui';
|
||||||
|
|
||||||
import { BarGaugePanel } from './BarGaugePanel';
|
import { BarGaugePanel } from './BarGaugePanel';
|
||||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||||
import { BarGaugeOptions, defaults } from './types';
|
import { BarGaugeOptions, defaults } from './types';
|
||||||
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
|
|
||||||
|
|
||||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel)
|
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel)
|
||||||
.setDefaults(defaults)
|
.setDefaults(defaults)
|
||||||
.setEditor(BarGaugePanelEditor)
|
.setEditor(BarGaugePanelEditor)
|
||||||
.setPanelChangeHandler(singleStatBaseOptionsCheck);
|
.setPanelChangeHandler(sharedSingleStatOptionsCheck);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { VizOrientation, SelectOptionItem, StatID } from '@grafana/ui';
|
import { VizOrientation, SelectOptionItem, StatID, SingleStatBaseOptions } from '@grafana/ui';
|
||||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
|
||||||
|
|
||||||
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
||||||
minValue: number;
|
minValue: number;
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import { Gauge } from '@grafana/ui';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
import { DisplayValue, PanelProps } from '@grafana/ui';
|
import { DisplayValue, PanelProps, getSingleStatDisplayValues } from '@grafana/ui';
|
||||||
import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
|
|
||||||
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
||||||
|
|
||||||
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||||
@@ -33,7 +32,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getProcessedValues = (): DisplayValue[] => {
|
getProcessedValues = (): DisplayValue[] => {
|
||||||
return getSingleStatValues(this.props);
|
return getSingleStatDisplayValues({
|
||||||
|
valueMappings: this.props.options.valueMappings,
|
||||||
|
thresholds: this.props.options.thresholds,
|
||||||
|
valueOptions: this.props.options.valueOptions,
|
||||||
|
data: this.props.data,
|
||||||
|
theme: config.theme,
|
||||||
|
replaceVariables: this.props.replaceVariables,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
PanelOptionsGrid,
|
PanelOptionsGrid,
|
||||||
ValueMappingsEditor,
|
ValueMappingsEditor,
|
||||||
ValueMapping,
|
ValueMapping,
|
||||||
|
SingleStatValueOptions,
|
||||||
|
SingleStatValueEditor,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { GaugeOptionsBox } from './GaugeOptionsBox';
|
import { GaugeOptionsBox } from './GaugeOptionsBox';
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
|
|
||||||
import { SingleStatValueOptions } from '../singlestat2/types';
|
|
||||||
|
|
||||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { ReactPanelPlugin } from '@grafana/ui';
|
import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui';
|
||||||
|
|
||||||
import { GaugePanelEditor } from './GaugePanelEditor';
|
import { GaugePanelEditor } from './GaugePanelEditor';
|
||||||
import { GaugePanel } from './GaugePanel';
|
import { GaugePanel } from './GaugePanel';
|
||||||
import { GaugeOptions, defaults } from './types';
|
import { GaugeOptions, defaults } from './types';
|
||||||
import { singleStatBaseOptionsCheck, singleStatMigrationCheck } from '../singlestat2/module';
|
|
||||||
|
|
||||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel)
|
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel)
|
||||||
.setDefaults(defaults)
|
.setDefaults(defaults)
|
||||||
.setEditor(GaugePanelEditor)
|
.setEditor(GaugePanelEditor)
|
||||||
.setPanelChangeHandler(singleStatBaseOptionsCheck)
|
.setPanelChangeHandler(sharedSingleStatOptionsCheck)
|
||||||
.setMigrationHandler(singleStatMigrationCheck);
|
.setMigrationHandler(sharedSingleStatMigrationCheck);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
import { VizOrientation, StatID, SingleStatBaseOptions } from '@grafana/ui';
|
||||||
import { VizOrientation, StatID } from '@grafana/ui';
|
|
||||||
|
|
||||||
export interface GaugeOptions extends SingleStatBaseOptions {
|
export interface GaugeOptions extends SingleStatBaseOptions {
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
|
|||||||
@@ -5,20 +5,26 @@ import React, { PureComponent } from 'react';
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { PieChart } from '@grafana/ui';
|
import { PieChart, getSingleStatDisplayValues } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PieChartOptions } from './types';
|
import { PieChartOptions } from './types';
|
||||||
import { PanelProps } from '@grafana/ui/src/types';
|
import { PanelProps } from '@grafana/ui/src/types';
|
||||||
import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
|
|
||||||
|
|
||||||
interface Props extends PanelProps<PieChartOptions> {}
|
interface Props extends PanelProps<PieChartOptions> {}
|
||||||
|
|
||||||
export class PieChartPanel extends PureComponent<Props> {
|
export class PieChartPanel extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { width, height, options } = this.props;
|
const { width, height, options, data, replaceVariables } = this.props;
|
||||||
|
|
||||||
const values = getSingleStatValues(this.props);
|
const values = getSingleStatDisplayValues({
|
||||||
|
valueMappings: options.valueMappings,
|
||||||
|
thresholds: options.thresholds,
|
||||||
|
valueOptions: options.valueOptions,
|
||||||
|
data: data,
|
||||||
|
theme: config.theme,
|
||||||
|
replaceVariables: replaceVariables,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PieChart
|
<PieChart
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { PanelEditorProps, PanelOptionsGrid, ValueMappingsEditor, ValueMapping } from '@grafana/ui';
|
import {
|
||||||
|
PanelEditorProps,
|
||||||
|
PanelOptionsGrid,
|
||||||
|
ValueMappingsEditor,
|
||||||
|
ValueMapping,
|
||||||
|
SingleStatValueOptions,
|
||||||
|
SingleStatValueEditor,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { PieChartOptionsBox } from './PieChartOptionsBox';
|
import { PieChartOptionsBox } from './PieChartOptionsBox';
|
||||||
import { PieChartOptions } from './types';
|
import { PieChartOptions } from './types';
|
||||||
import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
|
|
||||||
import { SingleStatValueOptions } from '../singlestat2/types';
|
|
||||||
|
|
||||||
export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
|
export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
|
||||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PieChartType, StatID, VizOrientation } from '@grafana/ui';
|
import { PieChartType, StatID, VizOrientation, SingleStatBaseOptions } from '@grafana/ui';
|
||||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
|
||||||
|
|
||||||
export interface PieChartOptions extends SingleStatBaseOptions {
|
export interface PieChartOptions extends SingleStatBaseOptions {
|
||||||
pieType: PieChartType;
|
pieType: PieChartType;
|
||||||
|
|||||||
68
public/app/plugins/panel/singlestat2/ColoringEditor.tsx
Normal file
68
public/app/plugins/panel/singlestat2/ColoringEditor.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Switch, PanelOptionsGroup } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { SingleStatOptions } from './types';
|
||||||
|
|
||||||
|
const labelWidth = 6;
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
options: SingleStatOptions;
|
||||||
|
onChange: (options: SingleStatOptions) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorBackground?: boolean;
|
||||||
|
// colorValue?: boolean;
|
||||||
|
// colorPrefix?: boolean;
|
||||||
|
// colorPostfix?: boolean;
|
||||||
|
|
||||||
|
export class ColoringEditor extends PureComponent<Props> {
|
||||||
|
onToggleColorBackground = () =>
|
||||||
|
this.props.onChange({ ...this.props.options, colorBackground: !this.props.options.colorBackground });
|
||||||
|
|
||||||
|
onToggleColorValue = () => this.props.onChange({ ...this.props.options, colorValue: !this.props.options.colorValue });
|
||||||
|
|
||||||
|
onToggleColorPrefix = () =>
|
||||||
|
this.props.onChange({ ...this.props.options, colorPrefix: !this.props.options.colorPrefix });
|
||||||
|
|
||||||
|
onToggleColorPostfix = () =>
|
||||||
|
this.props.onChange({ ...this.props.options, colorPostfix: !this.props.options.colorPostfix });
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { colorBackground, colorValue, colorPrefix, colorPostfix } = this.props.options;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelOptionsGroup title="Coloring">
|
||||||
|
<Switch
|
||||||
|
label="Background"
|
||||||
|
labelClass={`width-${labelWidth}`}
|
||||||
|
checked={colorBackground}
|
||||||
|
onChange={this.onToggleColorBackground}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label="Value"
|
||||||
|
labelClass={`width-${labelWidth}`}
|
||||||
|
checked={colorValue}
|
||||||
|
onChange={this.onToggleColorValue}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label="Prefix"
|
||||||
|
labelClass={`width-${labelWidth}`}
|
||||||
|
checked={colorPrefix}
|
||||||
|
onChange={this.onToggleColorPrefix}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label="Postfix"
|
||||||
|
labelClass={`width-${labelWidth}`}
|
||||||
|
checked={colorPostfix}
|
||||||
|
onChange={this.onToggleColorPostfix}
|
||||||
|
/>
|
||||||
|
</PanelOptionsGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
public/app/plugins/panel/singlestat2/FontSizeEditor.tsx
Normal file
67
public/app/plugins/panel/singlestat2/FontSizeEditor.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { FormLabel, Select, PanelOptionsGroup, SelectOptionItem } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { SingleStatOptions } from './types';
|
||||||
|
|
||||||
|
const labelWidth = 6;
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
options: SingleStatOptions;
|
||||||
|
onChange: (options: SingleStatOptions) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percents = ['20%', '30%', '50%', '70%', '80%', '100%', '110%', '120%', '150%', '170%', '200%'];
|
||||||
|
const fontSizeOptions = percents.map(v => {
|
||||||
|
return { value: v, label: v };
|
||||||
|
});
|
||||||
|
|
||||||
|
export class FontSizeEditor extends PureComponent<Props> {
|
||||||
|
setPrefixFontSize = (v: SelectOptionItem) => this.props.onChange({ ...this.props.options, prefixFontSize: v.value });
|
||||||
|
|
||||||
|
setValueFontSize = (v: SelectOptionItem) => this.props.onChange({ ...this.props.options, valueFontSize: v.value });
|
||||||
|
|
||||||
|
setPostfixFontSize = (v: SelectOptionItem) =>
|
||||||
|
this.props.onChange({ ...this.props.options, postfixFontSize: v.value });
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { prefixFontSize, valueFontSize, postfixFontSize } = this.props.options;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelOptionsGroup title="Font Size">
|
||||||
|
<div className="gf-form">
|
||||||
|
<FormLabel width={labelWidth}>Prefix</FormLabel>
|
||||||
|
<Select
|
||||||
|
width={12}
|
||||||
|
options={fontSizeOptions}
|
||||||
|
onChange={this.setPrefixFontSize}
|
||||||
|
value={fontSizeOptions.find(option => option.value === prefixFontSize)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<FormLabel width={labelWidth}>Value</FormLabel>
|
||||||
|
<Select
|
||||||
|
width={12}
|
||||||
|
options={fontSizeOptions}
|
||||||
|
onChange={this.setValueFontSize}
|
||||||
|
value={fontSizeOptions.find(option => option.value === valueFontSize)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<FormLabel width={labelWidth}>Postfix</FormLabel>
|
||||||
|
<Select
|
||||||
|
width={12}
|
||||||
|
options={fontSizeOptions}
|
||||||
|
onChange={this.setPostfixFontSize}
|
||||||
|
value={fontSizeOptions.find(option => option.value === postfixFontSize)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PanelOptionsGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,14 @@ import {
|
|||||||
PanelOptionsGrid,
|
PanelOptionsGrid,
|
||||||
ValueMappingsEditor,
|
ValueMappingsEditor,
|
||||||
ValueMapping,
|
ValueMapping,
|
||||||
|
SingleStatValueOptions,
|
||||||
|
SingleStatValueEditor,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { SingleStatOptions, SingleStatValueOptions } from './types';
|
import { SingleStatOptions, SparklineOptions } from './types';
|
||||||
import { SingleStatValueEditor } from './SingleStatValueEditor';
|
import { ColoringEditor } from './ColoringEditor';
|
||||||
|
import { FontSizeEditor } from './FontSizeEditor';
|
||||||
|
import { SparklineEditor } from './SparklineEditor';
|
||||||
|
|
||||||
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
|
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
|
||||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||||
@@ -31,6 +35,12 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
|||||||
valueOptions,
|
valueOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onSparklineChanged = (sparkline: SparklineOptions) =>
|
||||||
|
this.props.onOptionsChange({
|
||||||
|
...this.props.options,
|
||||||
|
sparkline,
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
|
|
||||||
@@ -38,6 +48,10 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
|||||||
<>
|
<>
|
||||||
<PanelOptionsGrid>
|
<PanelOptionsGrid>
|
||||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
||||||
|
<FontSizeEditor options={options} onChange={this.props.onOptionsChange} />
|
||||||
|
<ColoringEditor options={options} onChange={this.props.onOptionsChange} />
|
||||||
|
<SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
|
||||||
|
|
||||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
|
||||||
|
|||||||
@@ -1,82 +1,127 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent, CSSProperties } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Types
|
// Utils & Services
|
||||||
import { SingleStatOptions, SingleStatBaseOptions } from './types';
|
|
||||||
|
|
||||||
import { DisplayValue, PanelProps, NullValueMode, FieldType, calculateStats } from '@grafana/ui';
|
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { getDisplayProcessor } from '@grafana/ui';
|
import { getFlotPairs } from '@grafana/ui/src/utils/flotPairs';
|
||||||
|
|
||||||
|
// Components
|
||||||
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
||||||
|
|
||||||
export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
|
// Types
|
||||||
const { data, replaceVariables, options } = props;
|
import { SingleStatOptions } from './types';
|
||||||
const { valueOptions, valueMappings } = options;
|
import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
|
||||||
const { unit, decimals, stat } = valueOptions;
|
import {
|
||||||
|
DisplayValue,
|
||||||
|
PanelProps,
|
||||||
|
getDisplayProcessor,
|
||||||
|
NullValueMode,
|
||||||
|
FieldType,
|
||||||
|
calculateStats,
|
||||||
|
getFirstTimeField,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
const display = getDisplayProcessor({
|
interface SingleStatDisplay {
|
||||||
unit,
|
value: DisplayValue;
|
||||||
decimals,
|
prefix?: DisplayValue;
|
||||||
mappings: valueMappings,
|
suffix?: DisplayValue;
|
||||||
thresholds: options.thresholds,
|
sparkline?: BigValueSparkline;
|
||||||
prefix: replaceVariables(valueOptions.prefix),
|
backgroundColor?: string;
|
||||||
suffix: replaceVariables(valueOptions.suffix),
|
}
|
||||||
theme: config.theme,
|
|
||||||
});
|
|
||||||
|
|
||||||
const values: DisplayValue[] = [];
|
|
||||||
|
|
||||||
for (const series of data) {
|
|
||||||
if (stat === 'name') {
|
|
||||||
values.push(display(series.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < series.fields.length; i++) {
|
|
||||||
const column = series.fields[i];
|
|
||||||
|
|
||||||
// Show all columns that are not 'time'
|
|
||||||
if (column.type === FieldType.number) {
|
|
||||||
const stats = calculateStats({
|
|
||||||
series,
|
|
||||||
fieldIndex: i,
|
|
||||||
stats: [stat], // The stats to calculate
|
|
||||||
nullValueMode: NullValueMode.Null,
|
|
||||||
});
|
|
||||||
const displayValue = display(stats[stat]);
|
|
||||||
values.push(displayValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.length === 0) {
|
|
||||||
values.push({
|
|
||||||
numeric: 0,
|
|
||||||
text: 'No data',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
|
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
|
||||||
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
renderValue = (value: SingleStatDisplay, width: number, height: number): JSX.Element => {
|
||||||
const style: CSSProperties = {};
|
return <BigValue {...value} width={width} height={height} theme={config.theme} />;
|
||||||
style.margin = '0 auto';
|
|
||||||
style.fontSize = '250%';
|
|
||||||
style.textAlign = 'center';
|
|
||||||
if (value.color) {
|
|
||||||
style.color = value.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width, height }}>
|
|
||||||
<div style={style}>{value.text}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getProcessedValues = (): DisplayValue[] => {
|
getProcessedValues = (): SingleStatDisplay[] => {
|
||||||
return getSingleStatValues(this.props);
|
const { data, replaceVariables, options, timeRange } = this.props;
|
||||||
|
const { valueOptions, valueMappings } = options;
|
||||||
|
|
||||||
|
const display = getDisplayProcessor({
|
||||||
|
unit: valueOptions.unit,
|
||||||
|
decimals: valueOptions.decimals,
|
||||||
|
mappings: valueMappings,
|
||||||
|
thresholds: options.thresholds,
|
||||||
|
theme: config.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { colorBackground, colorValue, colorPrefix, colorPostfix, sparkline } = options;
|
||||||
|
const { stat } = valueOptions;
|
||||||
|
|
||||||
|
const values: SingleStatDisplay[] = [];
|
||||||
|
|
||||||
|
for (const series of data) {
|
||||||
|
const timeColumn = sparkline.show ? getFirstTimeField(series) : -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < series.fields.length; i++) {
|
||||||
|
const column = series.fields[i];
|
||||||
|
|
||||||
|
// Show all fields that are not 'time'
|
||||||
|
if (column.type === FieldType.number) {
|
||||||
|
const stats = calculateStats({
|
||||||
|
series,
|
||||||
|
fieldIndex: i,
|
||||||
|
stats: [stat], // The stats to calculate
|
||||||
|
nullValueMode: NullValueMode.Null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const v: SingleStatDisplay = {
|
||||||
|
value: display(stats[stat]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = v.value.color;
|
||||||
|
if (!colorValue) {
|
||||||
|
delete v.value.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorBackground) {
|
||||||
|
v.backgroundColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.valueFontSize) {
|
||||||
|
v.value.fontSize = options.valueFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueOptions.prefix) {
|
||||||
|
v.prefix = {
|
||||||
|
text: replaceVariables(valueOptions.prefix),
|
||||||
|
numeric: NaN,
|
||||||
|
color: colorPrefix ? color : null,
|
||||||
|
fontSize: options.prefixFontSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (valueOptions.suffix) {
|
||||||
|
v.suffix = {
|
||||||
|
text: replaceVariables(valueOptions.suffix),
|
||||||
|
numeric: NaN,
|
||||||
|
color: colorPostfix ? color : null,
|
||||||
|
fontSize: options.postfixFontSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sparkline.show && timeColumn >= 0) {
|
||||||
|
const points = getFlotPairs({
|
||||||
|
series,
|
||||||
|
xIndex: timeColumn,
|
||||||
|
yIndex: i,
|
||||||
|
nullValueMode: NullValueMode.Null,
|
||||||
|
});
|
||||||
|
|
||||||
|
v.sparkline = {
|
||||||
|
...sparkline,
|
||||||
|
data: points,
|
||||||
|
minX: timeRange.from.valueOf(),
|
||||||
|
maxX: timeRange.to.valueOf(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
33
public/app/plugins/panel/singlestat2/SparklineEditor.tsx
Normal file
33
public/app/plugins/panel/singlestat2/SparklineEditor.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Switch, PanelOptionsGroup } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { SparklineOptions } from './types';
|
||||||
|
|
||||||
|
const labelWidth = 6;
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
options: SparklineOptions;
|
||||||
|
onChange: (options: SparklineOptions) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SparklineEditor extends PureComponent<Props> {
|
||||||
|
onToggleShow = () => this.props.onChange({ ...this.props.options, show: !this.props.options.show });
|
||||||
|
|
||||||
|
onToggleFull = () => this.props.onChange({ ...this.props.options, full: !this.props.options.full });
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { show, full } = this.props.options;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelOptionsGroup title="Sparkline">
|
||||||
|
<Switch label="Show" labelClass={`width-${labelWidth}`} checked={show} onChange={this.onToggleShow} />
|
||||||
|
|
||||||
|
<Switch label="Full Height" labelClass={`width-${labelWidth}`} checked={full} onChange={this.onToggleFull} />
|
||||||
|
</PanelOptionsGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,10 @@
|
|||||||
import { ReactPanelPlugin, getStatsCalculators, PanelModel } from '@grafana/ui';
|
import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui';
|
||||||
import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
|
import { SingleStatOptions, defaults } from './types';
|
||||||
import { SingleStatPanel } from './SingleStatPanel';
|
import { SingleStatPanel } from './SingleStatPanel';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
|
||||||
import { SingleStatEditor } from './SingleStatEditor';
|
import { SingleStatEditor } from './SingleStatEditor';
|
||||||
|
|
||||||
const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
|
|
||||||
|
|
||||||
export const singleStatBaseOptionsCheck = (
|
|
||||||
options: Partial<SingleStatBaseOptions>,
|
|
||||||
prevPluginId: string,
|
|
||||||
prevOptions: any
|
|
||||||
) => {
|
|
||||||
for (const k of optionsToKeep) {
|
|
||||||
if (prevOptions.hasOwnProperty(k)) {
|
|
||||||
options[k] = cloneDeep(prevOptions[k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const singleStatMigrationCheck = (panel: PanelModel<SingleStatOptions>) => {
|
|
||||||
const options = panel.options;
|
|
||||||
|
|
||||||
if (!options) {
|
|
||||||
// This happens on the first load or when migrating from angular
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.valueOptions) {
|
|
||||||
// 6.1 renamed some stats, This makes sure they are up to date
|
|
||||||
// avg -> mean, current -> last, total -> sum
|
|
||||||
const { valueOptions } = options;
|
|
||||||
if (valueOptions && valueOptions.stat) {
|
|
||||||
valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel)
|
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel)
|
||||||
.setDefaults(defaults)
|
.setDefaults(defaults)
|
||||||
.setEditor(SingleStatEditor)
|
.setEditor(SingleStatEditor)
|
||||||
.setPanelChangeHandler(singleStatMigrationCheck)
|
.setPanelChangeHandler(sharedSingleStatOptionsCheck)
|
||||||
.setMigrationHandler(singleStatMigrationCheck);
|
.setMigrationHandler(sharedSingleStatMigrationCheck);
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
import { VizOrientation, ValueMapping, Threshold, StatID } from '@grafana/ui';
|
import { VizOrientation, StatID, SingleStatBaseOptions } from '@grafana/ui';
|
||||||
|
|
||||||
export interface SingleStatBaseOptions {
|
export interface SparklineOptions {
|
||||||
valueMappings: ValueMapping[];
|
show: boolean;
|
||||||
thresholds: Threshold[];
|
full: boolean; // full height
|
||||||
valueOptions: SingleStatValueOptions;
|
fillColor: string;
|
||||||
orientation: VizOrientation;
|
lineColor: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface SingleStatValueOptions {
|
|
||||||
unit: string;
|
|
||||||
suffix: string;
|
|
||||||
stat: string;
|
|
||||||
prefix: string;
|
|
||||||
decimals?: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Structure copied from angular
|
||||||
export interface SingleStatOptions extends SingleStatBaseOptions {
|
export interface SingleStatOptions extends SingleStatBaseOptions {
|
||||||
// TODO, fill in with options from angular
|
prefixFontSize?: string;
|
||||||
|
valueFontSize?: string;
|
||||||
|
postfixFontSize?: string;
|
||||||
|
|
||||||
|
colorBackground?: boolean;
|
||||||
|
colorValue?: boolean;
|
||||||
|
colorPrefix?: boolean;
|
||||||
|
colorPostfix?: boolean;
|
||||||
|
|
||||||
|
sparkline: SparklineOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaults: SingleStatOptions = {
|
export const defaults: SingleStatOptions = {
|
||||||
|
sparkline: {
|
||||||
|
show: true,
|
||||||
|
full: false,
|
||||||
|
lineColor: 'rgb(31, 120, 193)',
|
||||||
|
fillColor: 'rgba(31, 118, 189, 0.18)',
|
||||||
|
},
|
||||||
|
|
||||||
valueOptions: {
|
valueOptions: {
|
||||||
prefix: '',
|
prefix: '',
|
||||||
suffix: '',
|
suffix: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user