Commit 81dd5752 by Torkel Ödegaard Committed by GitHub

Panels: Progress on new singlestat / BigValue (#19374)

* POC: friday hack

* exploring new singlestat styles

* minor changes

* Testing bizcharts

* style tweaks

* Updated

* minor progress

* updated

* Updated layout handling

* Updated editor

* added editor options

* adding mode

* progress on new display mode

* tweaks

* Added classic style

* Added final mode

* Minor tweak

* tweaks

* minor tweak

* Singlestat: Adjust colors for light theme

* fixed build issues with bizcharts

* fixed typescript issue

* updated snapshot

* Added demo dashboard
parent 45e0ebcc
......@@ -29,6 +29,7 @@
"@grafana/slate-react": "0.22.9-grafana",
"@torkelo/react-select": "2.1.1",
"@types/react-color": "2.17.0",
"bizcharts": "^3.5.5",
"@types/slate": "0.47.1",
"@types/slate-react": "0.22.5",
"classnames": "2.2.6",
......
......@@ -57,6 +57,7 @@ const buildCjsPackage = ({ env }) => {
],
'node_modules/immutable/dist/immutable.js': ['Record', 'Set', 'Map', 'List', 'OrderedSet', 'is', 'Stack'],
'../../node_modules/esrever/esrever.js': ['reverse'],
'../../node_modules/bizcharts/es6/index.js': ['Chart', 'Geom', 'View', 'Tooltip', 'Legend'],
},
}),
resolve(),
......
import { storiesOf } from '@storybook/react';
import { number, text } from '@storybook/addon-knobs';
import { BigValue } from './BigValue';
import { text } from '@storybook/addon-knobs';
import { BigValue, SingleStatDisplayMode } 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', ''),
value: text('value', '$5022'),
title: text('title', 'Total Earnings'),
};
};
......@@ -16,22 +15,37 @@ const BigValueStories = storiesOf('UI/BigValue', module);
BigValueStories.addDecorator(withCenteredStory);
BigValueStories.add('Singlestat viz', () => {
const { value, prefix, valueFontSize } = getKnobs();
interface StoryOptions {
mode: SingleStatDisplayMode;
width?: number;
height?: number;
noSparkline?: boolean;
}
return renderComponentWithTheme(BigValue, {
width: 300,
height: 250,
value: {
text: value,
numeric: NaN,
fontSize: valueFontSize + '%',
},
prefix: prefix
? {
text: prefix,
numeric: NaN,
}
: null,
function addStoryForMode(options: StoryOptions) {
BigValueStories.add(`Mode: ${SingleStatDisplayMode[options.mode]}`, () => {
const { value, title } = getKnobs();
return renderComponentWithTheme(BigValue, {
width: options.width || 400,
height: options.height || 300,
displayMode: options.mode,
value: {
text: value,
numeric: 5022,
color: 'red',
title,
},
sparkline: {
minX: 0,
maxX: 5,
data: [[0, 10], [1, 20], [2, 15], [3, 25], [4, 5], [5, 10]],
},
});
});
});
}
addStoryForMode({ mode: SingleStatDisplayMode.Classic });
addStoryForMode({ mode: SingleStatDisplayMode.Classic2 });
addStoryForMode({ mode: SingleStatDisplayMode.Vibrant });
addStoryForMode({ mode: SingleStatDisplayMode.Vibrant2 });
import React from 'react';
import { shallow } from 'enzyme';
import { BigValue, Props } from './BigValue';
import { BigValue, Props, SingleStatDisplayMode } from './BigValue';
import { getTheme } from '../../themes/index';
jest.mock('jquery', () => ({
......@@ -11,6 +11,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
height: 300,
width: 300,
displayMode: SingleStatDisplayMode.Classic,
value: {
text: '25',
numeric: 25,
......@@ -29,7 +30,7 @@ const setup = (propOverrides?: object) => {
};
};
describe('Render BarGauge with basic options', () => {
describe('Render SingleStat with basic options', () => {
it('should render', () => {
const { wrapper } = setup();
expect(wrapper).toBeDefined();
......
......@@ -164,6 +164,8 @@ exports[`Render should render with base threshold 1`] = `
"md": "32px",
"sm": "24px",
},
"isDark": true,
"isLight": false,
"name": "Grafana Dark",
"panelHeaderHeight": 28,
"panelPadding": 8,
......@@ -331,6 +333,8 @@ exports[`Render should render with base threshold 1`] = `
"md": "32px",
"sm": "24px",
},
"isDark": true,
"isLight": false,
"name": "Grafana Dark",
"panelHeaderHeight": 28,
"panelPadding": 8,
......
......@@ -46,7 +46,7 @@ export { Table } from './Table/Table';
export { TableInputCSV } from './Table/TableInputCSV';
// Visualizations
export { BigValue } from './BigValue/BigValue';
export { BigValue, SingleStatDisplayMode } from './BigValue/BigValue';
export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
export { GraphLegend } from './Graph/GraphLegend';
......
......@@ -42,6 +42,8 @@ const basicColors = {
const darkTheme: GrafanaTheme = {
...defaultTheme,
type: GrafanaThemeType.Dark,
isDark: true,
isLight: false,
name: 'Grafana Dark',
colors: {
...basicColors,
......
......@@ -42,6 +42,8 @@ const basicColors = {
const lightTheme: GrafanaTheme = {
...defaultTheme,
type: GrafanaThemeType.Light,
isDark: false,
isLight: true,
name: 'Grafana Light',
colors: {
...basicColors,
......
......@@ -92,6 +92,8 @@ export interface GrafanaThemeCommons {
export interface GrafanaTheme extends GrafanaThemeCommons {
type: GrafanaThemeType;
isDark: boolean;
isLight: boolean;
background: {
dropdown: string;
scrollbar: string;
......
......@@ -142,6 +142,7 @@ export class PanelQueryRunner {
request.interval = norm.interval;
request.intervalMs = norm.intervalMs;
console.log('to', request.range.to.valueOf());
this.pipeToSubject(runRequest(ds, request));
} catch (err) {
......
// 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;
}
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>
);
}
}
// Libraries
import React, { PureComponent } from 'react';
// Components
import { FormLabel, Select, PanelOptionsGroup } from '@grafana/ui';
// Types
import { SingleStatOptions } from './types';
import { SelectableValue } from '@grafana/data';
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: SelectableValue<string>) =>
this.props.onChange({ ...this.props.options, prefixFontSize: v.value });
setValueFontSize = (v: SelectableValue<string>) =>
this.props.onChange({ ...this.props.options, valueFontSize: v.value });
setPostfixFontSize = (v: SelectableValue<string>) =>
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>
);
}
}
// Libraries
import React, { PureComponent } from 'react';
import {
PanelEditorProps,
ThresholdsEditor,
......@@ -10,12 +11,13 @@ import {
FieldPropertiesEditor,
PanelOptionsGroup,
DataLinksEditor,
FormLabel,
Select,
} from '@grafana/ui';
import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
import { SingleStatOptions, SparklineOptions } from './types';
import { ColoringEditor } from './ColoringEditor';
import { FontSizeEditor } from './FontSizeEditor';
import { SingleStatOptions, SparklineOptions, displayModes, colorModes } from './types';
import { SparklineEditor } from './SparklineEditor';
import {
getDataLinksVariableSuggestions,
......@@ -51,6 +53,9 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
sparkline,
});
onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
onColorModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, colorMode: value });
onDefaultsChange = (field: FieldConfig) => {
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
......@@ -77,17 +82,34 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor onChange={this.onDisplayOptionsChanged} value={fieldOptions} />
<FieldDisplayEditor onChange={this.onDisplayOptionsChanged} value={fieldOptions} labelWidth={8} />
<div className="form-field">
<FormLabel width={8}>Display mode</FormLabel>
<Select
width={12}
options={displayModes}
defaultValue={displayModes[0]}
onChange={this.onDisplayModeChange}
value={displayModes.find(item => item.value === options.displayMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Color by</FormLabel>
<Select
width={12}
options={colorModes}
defaultValue={colorModes[0]}
onChange={this.onColorModeChange}
value={colorModes.find(item => item.value === options.colorMode)}
/>
</div>
<SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
</PanelOptionsGroup>
<PanelOptionsGroup title="Field (default)">
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
</PanelOptionsGroup>
<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={defaults.thresholds} />
</PanelOptionsGrid>
......
......@@ -19,10 +19,10 @@ import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSupplie
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
let sparkline: BigValueSparkline;
if (value.sparkline) {
const { timeRange, options } = this.props;
const { timeRange, options } = this.props;
let sparkline: BigValueSparkline | undefined;
if (value.sparkline) {
sparkline = {
...options.sparkline,
data: value.sparkline,
......@@ -38,6 +38,7 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
<BigValue
value={value.display}
sparkline={sparkline}
displayMode={options.displayMode}
width={width}
height={height}
theme={config.theme}
......@@ -52,6 +53,7 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
getValues = (): FieldDisplay[] => {
const { data, options, replaceVariables } = this.props;
return getFieldDisplayValues({
...options,
replaceVariables,
......
......@@ -2,14 +2,12 @@
import React, { PureComponent } from 'react';
// Components
import { Switch, PanelOptionsGroup } from '@grafana/ui';
import { Switch } from '@grafana/ui';
// Types
import { SparklineOptions } from './types';
const labelWidth = 6;
export interface Props {
interface Props {
options: SparklineOptions;
onChange: (options: SparklineOptions) => void;
}
......@@ -17,17 +15,9 @@ export interface Props {
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} />
const { show } = this.props.options;
<Switch label="Full Height" labelClass={`width-${labelWidth}`} checked={full} onChange={this.onToggleFull} />
</PanelOptionsGroup>
);
return <Switch label="Graph" labelClass="width-8" checked={show} onChange={this.onToggleShow} />;
}
}
import { VizOrientation, SingleStatBaseOptions, FieldDisplayOptions } from '@grafana/ui';
import { VizOrientation, SingleStatBaseOptions, FieldDisplayOptions, SingleStatDisplayMode } from '@grafana/ui';
import { ReducerID } from '@grafana/data';
import { SelectableValue } from '@grafana/data';
export interface SparklineOptions {
show: boolean;
full: boolean; // full height
fillColor: string;
lineColor: string;
}
// Structure copied from angular
export interface SingleStatOptions extends SingleStatBaseOptions {
prefixFontSize?: string;
valueFontSize?: string;
postfixFontSize?: string;
colorBackground: boolean;
colorValue: boolean;
colorPrefix: boolean;
colorPostfix: boolean;
sparkline: SparklineOptions;
colorMode: ColorMode;
displayMode: SingleStatDisplayMode;
}
export const displayModes: Array<SelectableValue<SingleStatDisplayMode>> = [
{ value: SingleStatDisplayMode.Classic, label: 'Classic' },
{ value: SingleStatDisplayMode.Classic2, label: 'Classic 2' },
{ value: SingleStatDisplayMode.Vibrant, label: 'Vibrant' },
{ value: SingleStatDisplayMode.Vibrant2, label: 'Vibrant 2' },
];
export enum ColorMode {
Thresholds,
Series,
}
export const colorModes: Array<SelectableValue<ColorMode>> = [
{ value: ColorMode.Thresholds, label: 'Thresholds' },
{ value: ColorMode.Series, label: 'Series' },
];
export const standardFieldDisplayOptions: FieldDisplayOptions = {
values: false,
calcs: [ReducerID.mean],
......@@ -38,14 +48,9 @@ export const standardFieldDisplayOptions: FieldDisplayOptions = {
export const defaults: SingleStatOptions = {
sparkline: {
show: true,
full: false,
lineColor: 'rgb(31, 120, 193)',
fillColor: 'rgba(31, 118, 189, 0.18)',
},
colorMode: ColorMode.Thresholds,
displayMode: SingleStatDisplayMode.Vibrant,
fieldOptions: standardFieldDisplayOptions,
orientation: VizOrientation.Auto,
colorBackground: false,
colorValue: false,
colorPrefix: false,
colorPostfix: false,
};
......@@ -386,3 +386,7 @@ a.external-link {
th {
font-weight: $font-weight-semi-bold;
}
canvas {
display: block;
}
......@@ -40,7 +40,8 @@ const localStorageMock = (() => {
})();
global.localStorage = localStorageMock;
// Object.defineProperty(window, 'localStorage', { value: localStorageMock });
HTMLCanvasElement.prototype.getContext = jest.fn() as any;
const throwUnhandledRejections = () => {
process.on('unhandledRejection', err => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment