+
+ {this.renderText(prefix, '0px 2px 0px 0px')}
+ {this.renderText(value)}
+ {this.renderText(suffix)}
+
+
+ {sparkline &&
(this.canvasElement = element)} />}
+
+ );
+ }
+}
diff --git a/packages/grafana-ui/src/components/BigValue/_BigValue.scss b/packages/grafana-ui/src/components/BigValue/_BigValue.scss
new file mode 100644
index 00000000000..13603daa457
--- /dev/null
+++ b/packages/grafana-ui/src/components/BigValue/_BigValue.scss
@@ -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;
+}
diff --git a/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx b/packages/grafana-ui/src/components/SingleStatShared/SingleStatValueEditor.tsx
similarity index 67%
rename from public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
rename to packages/grafana-ui/src/components/SingleStatShared/SingleStatValueEditor.tsx
index 715c8d1f524..b08b7ce383d 100644
--- a/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
+++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatValueEditor.tsx
@@ -1,11 +1,19 @@
// Libraries
-import React, { PureComponent } from 'react';
+import React, { PureComponent, ChangeEvent } from 'react';
// Components
-import { FormField, FormLabel, PanelOptionsGroup, StatsPicker, UnitPicker, StatID } from '@grafana/ui';
+import {
+ FormField,
+ FormLabel,
+ PanelOptionsGroup,
+ StatsPicker,
+ UnitPicker,
+ StatID,
+ SelectOptionItem,
+} from '@grafana/ui';
// Types
-import { SingleStatValueOptions } from './types';
+import { SingleStatValueOptions } from './shared';
const labelWidth = 6;
@@ -15,15 +23,15 @@ export interface Props {
}
export class SingleStatValueEditor extends PureComponent
{
- 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;
this.props.onChange({ ...this.props.options, stat });
};
- onDecimalChange = event => {
- if (!isNaN(event.target.value)) {
+ onDecimalChange = (event: ChangeEvent) => {
+ if (!isNaN(parseInt(event.target.value, 10))) {
this.props.onChange({
...this.props.options,
decimals: parseInt(event.target.value, 10),
@@ -36,14 +44,16 @@ export class SingleStatValueEditor extends PureComponent {
}
};
- onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
- onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
+ onPrefixChange = (event: ChangeEvent) =>
+ this.props.onChange({ ...this.props.options, prefix: event.target.value });
+ onSuffixChange = (event: ChangeEvent) =>
+ this.props.onChange({ ...this.props.options, suffix: event.target.value });
render() {
const { stat, unit, decimals, prefix, suffix } = this.props.options;
let decimalsString = '';
- if (Number.isFinite(decimals)) {
+ if (decimals !== null && decimals !== undefined && Number.isFinite(decimals as number)) {
decimalsString = decimals.toString();
}
diff --git a/packages/grafana-ui/src/components/SingleStatShared/shared.ts b/packages/grafana-ui/src/components/SingleStatShared/shared.ts
new file mode 100644
index 00000000000..0fdf69087fe
--- /dev/null
+++ b/packages/grafana-ui/src/components/SingleStatShared/shared.ts
@@ -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 | 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) => {
+ 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;
+};
diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss
index 91e5c88e33e..7db4206fa27 100644
--- a/packages/grafana-ui/src/components/index.scss
+++ b/packages/grafana-ui/src/components/index.scss
@@ -1,4 +1,5 @@
@import 'CustomScrollbar/CustomScrollbar';
+@import 'BigValue/BigValue';
@import 'DeleteButton/DeleteButton';
@import 'ThresholdsEditor/ThresholdsEditor';
@import 'Table/Table';
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 2cc86c83db2..273cc14159a 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -33,9 +33,11 @@ export { StatsPicker } from './StatsPicker/StatsPicker';
export { Input, InputStatus } from './Input/Input';
// Visualizations
+export { BigValue } from './BigValue/BigValue';
export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
export { BarGauge } from './BarGauge/BarGauge';
export { VizRepeater } from './VizRepeater/VizRepeater';
+export * from './SingleStatShared/shared';
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
diff --git a/packages/grafana-ui/src/types/displayValue.ts b/packages/grafana-ui/src/types/displayValue.ts
index 65bf916b3f4..f1ca32c3c9c 100644
--- a/packages/grafana-ui/src/types/displayValue.ts
+++ b/packages/grafana-ui/src/types/displayValue.ts
@@ -3,6 +3,7 @@ export interface DisplayValue {
numeric: number; // Use isNaN to check if it is a real number
color?: string; // color based on configs or Threshold
title?: string;
+ fontSize?: string;
}
export interface DecimalInfo {
diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
index 2358b1ddab4..ceaced93547 100644
--- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
+++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
@@ -2,12 +2,11 @@
import React, { PureComponent } from 'react';
// Services & Utils
-import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui';
+import { DisplayValue, PanelProps, BarGauge, getSingleStatDisplayValues } from '@grafana/ui';
import { config } from 'app/core/config';
// Types
import { BarGaugeOptions } from './types';
-import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
export class BarGaugePanel extends PureComponent> {
@@ -28,7 +27,14 @@ export class BarGaugePanel extends PureComponent> {
};
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() {
diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
index 3404c8d8805..2984fb61544 100644
--- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
+++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
@@ -2,13 +2,19 @@
import React, { PureComponent } from 'react';
// Components
-import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
+import {
+ ThresholdsEditor,
+ ValueMappingsEditor,
+ PanelOptionsGrid,
+ PanelOptionsGroup,
+ FormField,
+ SingleStatValueOptions,
+ SingleStatValueEditor,
+} from '@grafana/ui';
// Types
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
-import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
-import { SingleStatValueOptions } from '../singlestat2/types';
export class BarGaugePanelEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx
index e84c64f3710..0d0312087e8 100644
--- a/public/app/plugins/panel/bargauge/module.tsx
+++ b/public/app/plugins/panel/bargauge/module.tsx
@@ -1,11 +1,10 @@
-import { ReactPanelPlugin } from '@grafana/ui';
+import { ReactPanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui';
import { BarGaugePanel } from './BarGaugePanel';
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
import { BarGaugeOptions, defaults } from './types';
-import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin(BarGaugePanel)
.setDefaults(defaults)
.setEditor(BarGaugePanelEditor)
- .setPanelChangeHandler(singleStatBaseOptionsCheck);
+ .setPanelChangeHandler(sharedSingleStatOptionsCheck);
diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts
index 2cfd6bdd653..37e1edf0e10 100644
--- a/public/app/plugins/panel/bargauge/types.ts
+++ b/public/app/plugins/panel/bargauge/types.ts
@@ -1,5 +1,4 @@
-import { VizOrientation, SelectOptionItem, StatID } from '@grafana/ui';
-import { SingleStatBaseOptions } from '../singlestat2/types';
+import { VizOrientation, SelectOptionItem, StatID, SingleStatBaseOptions } from '@grafana/ui';
export interface BarGaugeOptions extends SingleStatBaseOptions {
minValue: number;
diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx
index a0fdf9ed405..998fcdebcb8 100644
--- a/public/app/plugins/panel/gauge/GaugePanel.tsx
+++ b/public/app/plugins/panel/gauge/GaugePanel.tsx
@@ -9,8 +9,7 @@ import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
-import { DisplayValue, PanelProps } from '@grafana/ui';
-import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
+import { DisplayValue, PanelProps, getSingleStatDisplayValues } from '@grafana/ui';
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
export class GaugePanel extends PureComponent> {
@@ -33,7 +32,14 @@ export class GaugePanel extends PureComponent> {
};
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() {
diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
index e3b21302331..1229a881748 100644
--- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
+++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
@@ -7,12 +7,12 @@ import {
PanelOptionsGrid,
ValueMappingsEditor,
ValueMapping,
+ SingleStatValueOptions,
+ SingleStatValueEditor,
} from '@grafana/ui';
import { GaugeOptionsBox } from './GaugeOptionsBox';
import { GaugeOptions } from './types';
-import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
-import { SingleStatValueOptions } from '../singlestat2/types';
export class GaugePanelEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx
index d811d29029a..7cf7841d225 100644
--- a/public/app/plugins/panel/gauge/module.tsx
+++ b/public/app/plugins/panel/gauge/module.tsx
@@ -1,12 +1,10 @@
-import { ReactPanelPlugin } from '@grafana/ui';
-
+import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui';
import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types';
-import { singleStatBaseOptionsCheck, singleStatMigrationCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin(GaugePanel)
.setDefaults(defaults)
.setEditor(GaugePanelEditor)
- .setPanelChangeHandler(singleStatBaseOptionsCheck)
- .setMigrationHandler(singleStatMigrationCheck);
+ .setPanelChangeHandler(sharedSingleStatOptionsCheck)
+ .setMigrationHandler(sharedSingleStatMigrationCheck);
diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts
index a409e3524c0..0ebd162f862 100644
--- a/public/app/plugins/panel/gauge/types.ts
+++ b/public/app/plugins/panel/gauge/types.ts
@@ -1,5 +1,4 @@
-import { SingleStatBaseOptions } from '../singlestat2/types';
-import { VizOrientation, StatID } from '@grafana/ui';
+import { VizOrientation, StatID, SingleStatBaseOptions } from '@grafana/ui';
export interface GaugeOptions extends SingleStatBaseOptions {
maxValue: number;
diff --git a/public/app/plugins/panel/piechart/PieChartPanel.tsx b/public/app/plugins/panel/piechart/PieChartPanel.tsx
index 9c6d7d1a072..fae799abc58 100644
--- a/public/app/plugins/panel/piechart/PieChartPanel.tsx
+++ b/public/app/plugins/panel/piechart/PieChartPanel.tsx
@@ -5,20 +5,26 @@ import React, { PureComponent } from 'react';
import { config } from 'app/core/config';
// Components
-import { PieChart } from '@grafana/ui';
+import { PieChart, getSingleStatDisplayValues } from '@grafana/ui';
// Types
import { PieChartOptions } from './types';
import { PanelProps } from '@grafana/ui/src/types';
-import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
interface Props extends PanelProps {}
export class PieChartPanel extends PureComponent {
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 (
> {
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
diff --git a/public/app/plugins/panel/piechart/types.ts b/public/app/plugins/panel/piechart/types.ts
index b402eafe6a1..36beadbb45e 100644
--- a/public/app/plugins/panel/piechart/types.ts
+++ b/public/app/plugins/panel/piechart/types.ts
@@ -1,5 +1,4 @@
-import { PieChartType, StatID, VizOrientation } from '@grafana/ui';
-import { SingleStatBaseOptions } from '../singlestat2/types';
+import { PieChartType, StatID, VizOrientation, SingleStatBaseOptions } from '@grafana/ui';
export interface PieChartOptions extends SingleStatBaseOptions {
pieType: PieChartType;
diff --git a/public/app/plugins/panel/singlestat2/ColoringEditor.tsx b/public/app/plugins/panel/singlestat2/ColoringEditor.tsx
new file mode 100644
index 00000000000..a71379af3a8
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/ColoringEditor.tsx
@@ -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 {
+ 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 (
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/FontSizeEditor.tsx b/public/app/plugins/panel/singlestat2/FontSizeEditor.tsx
new file mode 100644
index 00000000000..9b582843c2a
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/FontSizeEditor.tsx
@@ -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 {
+ 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 (
+
+
+ Prefix
+
+
+
+ Value
+
+
+
+ Postfix
+
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
index 61b8588adce..5f5d2e02a3e 100644
--- a/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
+++ b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
@@ -7,10 +7,14 @@ import {
PanelOptionsGrid,
ValueMappingsEditor,
ValueMapping,
+ SingleStatValueOptions,
+ SingleStatValueEditor,
} from '@grafana/ui';
-import { SingleStatOptions, SingleStatValueOptions } from './types';
-import { SingleStatValueEditor } from './SingleStatValueEditor';
+import { SingleStatOptions, SparklineOptions } from './types';
+import { ColoringEditor } from './ColoringEditor';
+import { FontSizeEditor } from './FontSizeEditor';
+import { SparklineEditor } from './SparklineEditor';
export class SingleStatEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
@@ -31,6 +35,12 @@ export class SingleStatEditor extends PureComponent
+ this.props.onOptionsChange({
+ ...this.props.options,
+ sparkline,
+ });
+
render() {
const { options } = this.props;
@@ -38,6 +48,10 @@ export class SingleStatEditor extends PureComponent
+
+
+
+
diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
index 03d83fa431b..de0d22da11d 100644
--- a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
+++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
@@ -1,82 +1,127 @@
// Libraries
-import React, { PureComponent, CSSProperties } from 'react';
+import React, { PureComponent } from 'react';
-// Types
-import { SingleStatOptions, SingleStatBaseOptions } from './types';
-
-import { DisplayValue, PanelProps, NullValueMode, FieldType, calculateStats } from '@grafana/ui';
+// Utils & Services
import { config } from 'app/core/config';
-import { getDisplayProcessor } from '@grafana/ui';
+import { getFlotPairs } from '@grafana/ui/src/utils/flotPairs';
+
+// Components
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
-export const getSingleStatValues = (props: PanelProps): DisplayValue[] => {
- const { data, replaceVariables, options } = props;
- const { valueOptions, valueMappings } = options;
- const { unit, decimals, stat } = valueOptions;
+// Types
+import { SingleStatOptions } from './types';
+import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
+import {
+ DisplayValue,
+ PanelProps,
+ getDisplayProcessor,
+ NullValueMode,
+ FieldType,
+ calculateStats,
+ getFirstTimeField,
+} from '@grafana/ui';
- const display = getDisplayProcessor({
- unit,
- decimals,
- mappings: valueMappings,
- thresholds: options.thresholds,
- prefix: replaceVariables(valueOptions.prefix),
- 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;
-};
+interface SingleStatDisplay {
+ value: DisplayValue;
+ prefix?: DisplayValue;
+ suffix?: DisplayValue;
+ sparkline?: BigValueSparkline;
+ backgroundColor?: string;
+}
export class SingleStatPanel extends PureComponent> {
- renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
- const style: CSSProperties = {};
- style.margin = '0 auto';
- style.fontSize = '250%';
- style.textAlign = 'center';
- if (value.color) {
- style.color = value.color;
- }
-
- return (
-
- );
+ renderValue = (value: SingleStatDisplay, width: number, height: number): JSX.Element => {
+ return ;
};
- getProcessedValues = (): DisplayValue[] => {
- return getSingleStatValues(this.props);
+ getProcessedValues = (): SingleStatDisplay[] => {
+ 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() {
diff --git a/public/app/plugins/panel/singlestat2/SparklineEditor.tsx b/public/app/plugins/panel/singlestat2/SparklineEditor.tsx
new file mode 100644
index 00000000000..5783f762e0d
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/SparklineEditor.tsx
@@ -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 {
+ 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 (
+
+
+
+
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx
index b4892adef55..2be94666d5c 100644
--- a/public/app/plugins/panel/singlestat2/module.tsx
+++ b/public/app/plugins/panel/singlestat2/module.tsx
@@ -1,45 +1,10 @@
-import { ReactPanelPlugin, getStatsCalculators, PanelModel } from '@grafana/ui';
-import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
+import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui';
+import { SingleStatOptions, defaults } from './types';
import { SingleStatPanel } from './SingleStatPanel';
-import cloneDeep from 'lodash/cloneDeep';
import { SingleStatEditor } from './SingleStatEditor';
-const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
-
-export const singleStatBaseOptionsCheck = (
- options: Partial,
- 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) => {
- 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(SingleStatPanel)
.setDefaults(defaults)
.setEditor(SingleStatEditor)
- .setPanelChangeHandler(singleStatMigrationCheck)
- .setMigrationHandler(singleStatMigrationCheck);
+ .setPanelChangeHandler(sharedSingleStatOptionsCheck)
+ .setMigrationHandler(sharedSingleStatMigrationCheck);
diff --git a/public/app/plugins/panel/singlestat2/types.ts b/public/app/plugins/panel/singlestat2/types.ts
index 797e0cb0116..d2646bc0436 100644
--- a/public/app/plugins/panel/singlestat2/types.ts
+++ b/public/app/plugins/panel/singlestat2/types.ts
@@ -1,25 +1,34 @@
-import { VizOrientation, ValueMapping, Threshold, StatID } from '@grafana/ui';
+import { VizOrientation, StatID, SingleStatBaseOptions } from '@grafana/ui';
-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 SparklineOptions {
+ show: boolean;
+ full: boolean; // full height
+ fillColor: string;
+ lineColor: string;
}
+// Structure copied from angular
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 = {
+ sparkline: {
+ show: true,
+ full: false,
+ lineColor: 'rgb(31, 120, 193)',
+ fillColor: 'rgba(31, 118, 189, 0.18)',
+ },
+
valueOptions: {
prefix: '',
suffix: '',