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