Browse Source

Merge pull request #15466 from grafana/bar-gauge-poc

New Bar Gauge panel
Torkel Ödegaard 7 years ago
parent
commit
9e7d1f4275
36 changed files with 1601 additions and 150 deletions
  1. 296 0
      devenv/dev-dashboards/panel_tests_multiseries_gauge.json
  2. 54 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
  3. 64 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
  4. 239 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
  5. 9 0
      packages/grafana-ui/src/components/BarGauge/_BarGauge.scss
  6. 358 0
      packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap
  7. 20 34
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  8. 77 0
      packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx
  9. 1 0
      packages/grafana-ui/src/components/index.scss
  10. 6 2
      packages/grafana-ui/src/components/index.ts
  11. 10 4
      packages/grafana-ui/src/types/data.ts
  12. 1 0
      packages/grafana-ui/src/types/index.ts
  13. 13 11
      packages/grafana-ui/src/types/panel.ts
  14. 5 0
      packages/grafana-ui/src/types/threshold.ts
  15. 2 0
      packages/grafana-ui/src/utils/index.ts
  16. 33 0
      packages/grafana-ui/src/utils/singlestat.ts
  17. 23 0
      packages/grafana-ui/src/utils/thresholds.ts
  18. 7 5
      pkg/api/frontendsettings.go
  19. 1 0
      public/app/core/utils/ConfigProvider.tsx
  20. 13 8
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  21. 6 5
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  22. 4 5
      public/app/features/dashboard/state/PanelModel.test.ts
  23. 10 11
      public/app/features/dashboard/state/PanelModel.ts
  24. 2 0
      public/app/features/plugins/built_in_plugins.ts
  25. 56 0
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  26. 64 0
      public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
  27. 96 0
      public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg
  28. 22 0
      public/app/plugins/panel/bargauge/module.tsx
  29. 19 0
      public/app/plugins/panel/bargauge/plugin.json
  30. 31 0
      public/app/plugins/panel/bargauge/types.ts
  31. 6 4
      public/app/plugins/panel/gauge/GaugeOptionsBox.tsx
  32. 37 60
      public/app/plugins/panel/gauge/GaugePanel.tsx
  33. 1 0
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  34. 12 0
      public/app/plugins/panel/gauge/module.tsx
  35. 1 1
      public/app/plugins/panel/gauge/types.ts
  36. 2 0
      public/sass/utils/_utils.scss

+ 296 - 0
devenv/dev-dashboards/panel_tests_multiseries_gauge.json

@@ -0,0 +1,296 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 6,
+      "links": [],
+      "options-gauge": {
+        "decimals": 0,
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#1F78C1",
+            "index": 5,
+            "value": 96.875
+          },
+          {
+            "color": "#E24D42",
+            "index": 4,
+            "value": 93.75
+          },
+          {
+            "color": "#EF843C",
+            "index": 3,
+            "value": 87.5
+          },
+          {
+            "color": "#6ED0E0",
+            "index": 2,
+            "value": 75
+          },
+          {
+            "color": "#EAB839",
+            "index": 1,
+            "value": 50
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": [
+          {
+            "from": "50",
+            "id": 1,
+            "operator": "",
+            "text": "Hello :) ",
+            "to": "90",
+            "type": 2,
+            "value": ""
+          }
+        ]
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "B",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "C",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "D",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "E",
+          "scenarioId": "random_walk"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Horizontal with range variable",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 8
+      },
+      "id": 2,
+      "links": [],
+      "options-gauge": {
+        "decimals": 0,
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#EAB839",
+            "index": 1,
+            "value": 50
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "B",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "C",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "D",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "E",
+          "scenarioId": "random_walk"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Repeat horizontal",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 14,
+        "w": 5,
+        "x": 0,
+        "y": 16
+      },
+      "id": 4,
+      "links": [],
+      "options-gauge": {
+        "decimals": 0,
+        "maxValue": "200",
+        "minValue": 0,
+        "options": {
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "max",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#6ED0E0",
+            "index": 2,
+            "value": 75
+          },
+          {
+            "color": "#EAB839",
+            "index": 1,
+            "value": 50
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "B",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "C",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "D",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "E",
+          "scenarioId": "random_walk"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Vertical",
+      "type": "gauge"
+    }
+  ],
+  "schemaVersion": 17,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
+    "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
+  },
+  "timezone": "",
+  "title": "Multi series gauges",
+  "uid": "szkuR1umk",
+  "version": 7
+}

+ 54 - 0
packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx

@@ -0,0 +1,54 @@
+import { storiesOf } from '@storybook/react';
+import { number, text } from '@storybook/addon-knobs';
+import { BarGauge } from './BarGauge';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
+
+const getKnobs = () => {
+  return {
+    value: number('value', 70),
+    minValue: number('minValue', 0),
+    maxValue: number('maxValue', 100),
+    threshold1Value: number('threshold1Value', 40),
+    threshold1Color: text('threshold1Color', 'orange'),
+    threshold2Value: number('threshold2Value', 60),
+    threshold2Color: text('threshold2Color', 'red'),
+    unit: text('unit', 'ms'),
+    decimals: number('decimals', 1),
+  };
+};
+
+const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
+
+BarGaugeStories.addDecorator(withCenteredStory);
+
+BarGaugeStories.add('Vertical, with basic thresholds', () => {
+  const {
+    value,
+    minValue,
+    maxValue,
+    threshold1Color,
+    threshold2Color,
+    threshold1Value,
+    threshold2Value,
+    unit,
+    decimals,
+  } = getKnobs();
+
+  return renderComponentWithTheme(BarGauge, {
+    width: 200,
+    height: 400,
+    value: value,
+    minValue: minValue,
+    maxValue: maxValue,
+    unit: unit,
+    prefix: '',
+    postfix: '',
+    decimals: decimals,
+    thresholds: [
+      { index: 0, value: -Infinity, color: 'green' },
+      { index: 1, value: threshold1Value, color: threshold1Color },
+      { index: 1, value: threshold2Value, color: threshold2Color },
+    ],
+  });
+});

+ 64 - 0
packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx

@@ -0,0 +1,64 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { BarGauge, Props } from './BarGauge';
+import { VizOrientation } from '../../types';
+import { getTheme } from '../../themes';
+
+jest.mock('jquery', () => ({
+  plot: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    maxValue: 100,
+    valueMappings: [],
+    minValue: 0,
+    prefix: '',
+    suffix: '',
+    thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
+    unit: 'none',
+    height: 300,
+    width: 300,
+    value: 25,
+    decimals: 0,
+    theme: getTheme(),
+    orientation: VizOrientation.Horizontal,
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<BarGauge {...props} />);
+  const instance = wrapper.instance() as BarGauge;
+
+  return {
+    instance,
+    wrapper,
+  };
+};
+
+describe('Get font color', () => {
+  it('should get first threshold color when only one threshold', () => {
+    const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
+
+    expect(instance.getValueColors().value).toEqual('#7EB26D');
+  });
+
+  it('should get the threshold color if value is same as a threshold', () => {
+    const { instance } = setup({
+      thresholds: [
+        { index: 2, value: 75, color: '#6ED0E0' },
+        { index: 1, value: 10, color: '#EAB839' },
+        { index: 0, value: -Infinity, color: '#7EB26D' },
+      ],
+    });
+
+    expect(instance.getValueColors().value).toEqual('#EAB839');
+  });
+});
+
+describe('Render BarGauge with basic options', () => {
+  it('should render', () => {
+    const { wrapper } = setup();
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 239 - 0
packages/grafana-ui/src/components/BarGauge/BarGauge.tsx

@@ -0,0 +1,239 @@
+// Library
+import React, { PureComponent, CSSProperties } from 'react';
+import tinycolor from 'tinycolor2';
+
+// Utils
+import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+
+// Types
+import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
+
+const BAR_SIZE_RATIO = 0.8;
+
+export interface Props extends Themeable {
+  height: number;
+  unit: string;
+  width: number;
+  thresholds: Threshold[];
+  valueMappings: ValueMapping[];
+  value: TimeSeriesValue;
+  maxValue: number;
+  minValue: number;
+  orientation: VizOrientation;
+  prefix?: string;
+  suffix?: string;
+  decimals?: number;
+}
+
+/*
+ * This visualization is still in POC state, needed more tests & better structure
+ */
+export class BarGauge extends PureComponent<Props> {
+  static defaultProps: Partial<Props> = {
+    maxValue: 100,
+    minValue: 0,
+    value: 100,
+    unit: 'none',
+    orientation: VizOrientation.Horizontal,
+    thresholds: [],
+    valueMappings: [],
+  };
+
+  getNumericValue(): number {
+    if (Number.isFinite(this.props.value as number)) {
+      return this.props.value as number;
+    }
+    return 0;
+  }
+
+  getValueColors(): BarColors {
+    const { thresholds, theme, value } = this.props;
+
+    const activeThreshold = getThresholdForValue(thresholds, value);
+
+    if (activeThreshold !== null) {
+      const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
+
+      return {
+        value: color,
+        border: color,
+        bar: tinycolor(color)
+          .setAlpha(0.3)
+          .toRgbString(),
+      };
+    }
+
+    return {
+      value: getColorFromHexRgbOrName('gray', theme.type),
+      bar: getColorFromHexRgbOrName('gray', theme.type),
+      border: getColorFromHexRgbOrName('gray', theme.type),
+    };
+  }
+
+  getCellColor(positionValue: TimeSeriesValue): string {
+    const { thresholds, theme, value } = this.props;
+    const activeThreshold = getThresholdForValue(thresholds, positionValue);
+
+    if (activeThreshold !== null) {
+      const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
+
+      // if we are past real value the cell is not "on"
+      if (value === null || (positionValue !== null && positionValue > value)) {
+        return tinycolor(color)
+          .setAlpha(0.15)
+          .toRgbString();
+      } else {
+        return tinycolor(color)
+          .setAlpha(0.7)
+          .toRgbString();
+      }
+    }
+
+    return 'gray';
+  }
+
+  getValueStyles(value: string, color: string, width: number): CSSProperties {
+    const guess = width / (value.length * 1.1);
+    const fontSize = Math.min(Math.max(guess, 14), 40);
+
+    return {
+      color: color,
+      fontSize: fontSize + 'px',
+    };
+  }
+
+  renderVerticalBar(valueFormatted: string, valuePercent: number) {
+    const { height, width } = this.props;
+
+    const maxHeight = height * BAR_SIZE_RATIO;
+    const barHeight = Math.max(valuePercent * maxHeight, 0);
+    const colors = this.getValueColors();
+    const valueStyles = this.getValueStyles(valueFormatted, colors.value, width);
+
+    const containerStyles: CSSProperties = {
+      width: `${width}px`,
+      height: `${height}px`,
+      display: 'flex',
+      flexDirection: 'column',
+      justifyContent: 'flex-end',
+    };
+
+    const barStyles: CSSProperties = {
+      height: `${barHeight}px`,
+      width: `${width}px`,
+      backgroundColor: colors.bar,
+      borderTop: `1px solid ${colors.border}`,
+    };
+
+    return (
+      <div style={containerStyles}>
+        <div className="bar-gauge__value" style={valueStyles}>
+          {valueFormatted}
+        </div>
+        <div style={barStyles} />
+      </div>
+    );
+  }
+
+  renderHorizontalBar(valueFormatted: string, valuePercent: number) {
+    const { height, width } = this.props;
+
+    const maxWidth = width * BAR_SIZE_RATIO;
+    const barWidth = Math.max(valuePercent * maxWidth, 0);
+    const colors = this.getValueColors();
+    const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
+
+    valueStyles.marginLeft = '8px';
+
+    const containerStyles: CSSProperties = {
+      width: `${width}px`,
+      height: `${height}px`,
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    };
+
+    const barStyles = {
+      height: `${height}px`,
+      width: `${barWidth}px`,
+      backgroundColor: colors.bar,
+      borderRight: `1px solid ${colors.border}`,
+    };
+
+    return (
+      <div style={containerStyles}>
+        <div style={barStyles} />
+        <div className="bar-gauge__value" style={valueStyles}>
+          {valueFormatted}
+        </div>
+      </div>
+    );
+  }
+
+  renderHorizontalLCD(valueFormatted: string, valuePercent: number) {
+    const { height, width, maxValue, minValue } = this.props;
+
+    const valueRange = maxValue - minValue;
+    const maxWidth = width * BAR_SIZE_RATIO;
+    const cellSpacing = 4;
+    const cellCount = 30;
+    const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount;
+    const colors = this.getValueColors();
+    const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
+    valueStyles.marginLeft = '8px';
+
+    const containerStyles: CSSProperties = {
+      width: `${width}px`,
+      height: `${height}px`,
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    };
+
+    const cells: JSX.Element[] = [];
+
+    for (let i = 0; i < cellCount; i++) {
+      const currentValue = (valueRange / cellCount) * i;
+      const cellColor = this.getCellColor(currentValue);
+      const cellStyles: CSSProperties = {
+        width: `${cellWidth}px`,
+        backgroundColor: cellColor,
+        marginRight: '4px',
+        height: `${height}px`,
+        borderRadius: '2px',
+      };
+
+      cells.push(<div style={cellStyles} />);
+    }
+
+    return (
+      <div style={containerStyles}>
+        {cells}
+        <div className="bar-gauge__value" style={valueStyles}>
+          {valueFormatted}
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    const { maxValue, minValue, orientation, unit, decimals } = this.props;
+
+    const numericValue = this.getNumericValue();
+    const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
+
+    const formatFunc = getValueFormat(unit);
+    const valueFormatted = formatFunc(numericValue, decimals);
+    const vertical = orientation === 'vertical';
+
+    return vertical
+      ? this.renderVerticalBar(valueFormatted, valuePercent)
+      : this.renderHorizontalLCD(valueFormatted, valuePercent);
+  }
+}
+
+interface BarColors {
+  value: string;
+  bar: string;
+  border: string;
+}

+ 9 - 0
packages/grafana-ui/src/components/BarGauge/_BarGauge.scss

@@ -0,0 +1,9 @@
+.bar-gauge {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.bar-gauge__value {
+  text-align: center;
+}

+ 358 - 0
packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap

@@ -0,0 +1,358 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render BarGauge with basic options should render 1`] = `
+<div
+  style={
+    Object {
+      "alignItems": "center",
+      "display": "flex",
+      "flexDirection": "row",
+      "height": "300px",
+      "width": "300px",
+    }
+  }
+>
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    className="bar-gauge__value"
+    style={
+      Object {
+        "color": "#7EB26D",
+        "fontSize": "27.272727272727263px",
+        "marginLeft": "8px",
+      }
+    }
+  >
+    25
+  </div>
+</div>
+`;

+ 20 - 34
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -1,12 +1,12 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 import $ from 'jquery';
+
+import { ValueMapping, Threshold, GrafanaThemeType } from '../../types';
 import { getMappedValue } from '../../utils/valueMappings';
 import { getMappedValue } from '../../utils/valueMappings';
-import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
-import { Themeable, GrafanaThemeType } from '../../types/theme';
-import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
-import { getValueFormat } from '../../utils/valueFormats/valueFormats';
+import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+import { Themeable } from '../../index';
 
 
-type TimeSeriesValue = string | number | null;
+type GaugeValue = string | number | null;
 
 
 export interface Props extends Themeable {
 export interface Props extends Themeable {
   decimals?: number | null;
   decimals?: number | null;
@@ -51,7 +51,7 @@ export class Gauge extends PureComponent<Props> {
     this.draw();
     this.draw();
   }
   }
 
 
-  formatValue(value: TimeSeriesValue) {
+  formatValue(value: GaugeValue) {
     const { decimals, valueMappings, prefix, suffix, unit } = this.props;
     const { decimals, valueMappings, prefix, suffix, unit } = this.props;
 
 
     if (isNaN(value as number)) {
     if (isNaN(value as number)) {
@@ -72,26 +72,16 @@ export class Gauge extends PureComponent<Props> {
     return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
     return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
   }
   }
 
 
-  getFontColor(value: TimeSeriesValue) {
+  getFontColor(value: GaugeValue): string {
     const { thresholds, theme } = this.props;
     const { thresholds, theme } = this.props;
 
 
-    if (thresholds.length === 1) {
-      return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
-    }
-
-    const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
-    if (atThreshold) {
-      return getColorFromHexRgbOrName(atThreshold.color, theme.type);
-    }
-
-    const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
+    const activeThreshold = getThresholdForValue(thresholds, value);
 
 
-    if (belowThreshold.length > 0) {
-      const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
-      return getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
+    if (activeThreshold !== null) {
+      return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
     }
     }
 
 
-    return BasicGaugeColor.Red;
+    return '';
   }
   }
 
 
   getFormattedThresholds() {
   getFormattedThresholds() {
@@ -183,19 +173,15 @@ export class Gauge extends PureComponent<Props> {
     const { height, width } = this.props;
     const { height, width } = this.props;
 
 
     return (
     return (
-      <div className="singlestat-panel">
-        <div
-          style={{
-            height: `${height * 0.9}px`,
-            width: `${Math.min(width, height * 1.3)}px`,
-            top: '10px',
-            margin: 'auto',
-          }}
-          ref={element => (this.canvasElement = element)}
-        />
-      </div>
+      <div
+        style={{
+          height: `${Math.min(height, width * 1.3)}px`,
+          width: `${Math.min(width, height * 1.3)}px`,
+          top: '10px',
+          margin: 'auto',
+        }}
+        ref={element => (this.canvasElement = element)}
+      />
     );
     );
   }
   }
 }
 }
-
-export default Gauge;

+ 77 - 0
packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx

@@ -0,0 +1,77 @@
+import React, { PureComponent } from 'react';
+import { SingleStatValueInfo, VizOrientation } from '../../types';
+
+interface RenderProps {
+  vizWidth: number;
+  vizHeight: number;
+  valueInfo: SingleStatValueInfo;
+}
+
+interface Props {
+  children: (renderProps: RenderProps) => JSX.Element | JSX.Element[];
+  height: number;
+  width: number;
+  values: SingleStatValueInfo[];
+  orientation: VizOrientation;
+}
+
+const SPACE_BETWEEN = 10;
+
+export class VizRepeater extends PureComponent<Props> {
+  getOrientation(): VizOrientation {
+    const { orientation, width, height } = this.props;
+
+    if (orientation === VizOrientation.Auto) {
+      if (width > height) {
+        return VizOrientation.Vertical;
+      } else {
+        return VizOrientation.Horizontal;
+      }
+    }
+
+    return orientation;
+  }
+
+  render() {
+    const { children, height, values, width } = this.props;
+    const orientation = this.getOrientation();
+
+    const itemStyles: React.CSSProperties = {
+      display: 'flex',
+    };
+
+    const repeaterStyle: React.CSSProperties = {
+      display: 'flex',
+    };
+
+    let vizHeight = height;
+    let vizWidth = width;
+
+    if (orientation === VizOrientation.Horizontal) {
+      repeaterStyle.flexDirection = 'column';
+      itemStyles.margin = `${SPACE_BETWEEN / 2}px 0`;
+      vizWidth = width;
+      vizHeight = height / values.length - SPACE_BETWEEN;
+    } else {
+      repeaterStyle.flexDirection = 'row';
+      itemStyles.margin = `0 ${SPACE_BETWEEN / 2}px`;
+      vizHeight = height;
+      vizWidth = width / values.length - SPACE_BETWEEN;
+    }
+
+    itemStyles.width = `${vizWidth}px`;
+    itemStyles.height = `${vizHeight}px`;
+
+    return (
+      <div style={repeaterStyle}>
+        {values.map((valueInfo, index) => {
+          return (
+            <div key={index} style={itemStyles}>
+              {children({ vizHeight, vizWidth, valueInfo })}
+            </div>
+          );
+        })}
+      </div>
+    );
+  }
+}

+ 1 - 0
packages/grafana-ui/src/components/index.scss

@@ -11,3 +11,4 @@
 @import 'ValueMappingsEditor/ValueMappingsEditor';
 @import 'ValueMappingsEditor/ValueMappingsEditor';
 @import 'EmptySearchResult/EmptySearchResult';
 @import 'EmptySearchResult/EmptySearchResult';
 @import 'FormField/FormField';
 @import 'FormField/FormField';
+@import 'BarGauge/BarGauge';

+ 6 - 2
packages/grafana-ui/src/components/index.ts

@@ -19,11 +19,15 @@ export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
 export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
 export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
 export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
-export { Graph } from './Graph/Graph';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
 export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
 export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
-export { Gauge } from './Gauge/Gauge';
 export { Switch } from './Switch/Switch';
 export { Switch } from './Switch/Switch';
 export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
 export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
 export { UnitPicker } from './UnitPicker/UnitPicker';
 export { UnitPicker } from './UnitPicker/UnitPicker';
+
+// Visualizations
+export { Gauge } from './Gauge/Gauge';
+export { Graph } from './Graph/Graph';
+export { BarGauge } from './BarGauge/BarGauge';
+export { VizRepeater } from './VizRepeater/VizRepeater';

+ 10 - 4
packages/grafana-ui/src/types/data.ts

@@ -48,10 +48,7 @@ export enum NullValueMode {
 }
 }
 
 
 /** View model projection of many time series */
 /** View model projection of many time series */
-export interface TimeSeriesVMs {
-  [index: number]: TimeSeriesVM;
-  length: number;
-}
+export type TimeSeriesVMs = TimeSeriesVM[];
 
 
 export interface Column {
 export interface Column {
   text: string;
   text: string;
@@ -69,3 +66,12 @@ export interface TableData {
   type: string;
   type: string;
   columnMap: any;
   columnMap: any;
 }
 }
+
+export type SingleStatValue = number | string | null;
+
+/*
+ * So we can add meta info like tags & series name
+ */
+export interface SingleStatValueInfo {
+  value: SingleStatValue;
+}

+ 1 - 0
packages/grafana-ui/src/types/index.ts

@@ -4,3 +4,4 @@ export * from './panel';
 export * from './plugin';
 export * from './plugin';
 export * from './datasource';
 export * from './datasource';
 export * from './theme';
 export * from './theme';
+export * from './threshold';

+ 13 - 11
packages/grafana-ui/src/types/panel.ts

@@ -26,10 +26,13 @@ export interface PanelEditorProps<T = any> {
   onOptionsChange: (options: T) => void;
   onOptionsChange: (options: T) => void;
 }
 }
 
 
+export type PreservePanelOptionsHandler<TOptions = any> = (pluginId: string, prevOptions: any) => Partial<TOptions>;
+
 export class ReactPanelPlugin<TOptions = any> {
 export class ReactPanelPlugin<TOptions = any> {
   panel: ComponentClass<PanelProps<TOptions>>;
   panel: ComponentClass<PanelProps<TOptions>>;
   editor?: ComponentClass<PanelEditorProps<TOptions>>;
   editor?: ComponentClass<PanelEditorProps<TOptions>>;
   defaults?: TOptions;
   defaults?: TOptions;
+  preserveOptions?: PreservePanelOptionsHandler<TOptions>;
 
 
   constructor(panel: ComponentClass<PanelProps<TOptions>>) {
   constructor(panel: ComponentClass<PanelProps<TOptions>>) {
     this.panel = panel;
     this.panel = panel;
@@ -42,6 +45,10 @@ export class ReactPanelPlugin<TOptions = any> {
   setDefaults(defaults: TOptions) {
   setDefaults(defaults: TOptions) {
     this.defaults = defaults;
     this.defaults = defaults;
   }
   }
+
+  setPreserveOptionsHandler(handler: PreservePanelOptionsHandler<TOptions>) {
+    this.preserveOptions = handler;
+  }
 }
 }
 
 
 export interface PanelSize {
 export interface PanelSize {
@@ -58,17 +65,6 @@ export interface PanelMenuItem {
   subMenu?: PanelMenuItem[];
   subMenu?: PanelMenuItem[];
 }
 }
 
 
-export interface Threshold {
-  index: number;
-  value: number;
-  color: string;
-}
-
-export enum BasicGaugeColor {
-  Green = '#299c46',
-  Red = '#d44a3a',
-}
-
 export enum MappingType {
 export enum MappingType {
   ValueToText = 1,
   ValueToText = 1,
   RangeToText = 2,
   RangeToText = 2,
@@ -91,3 +87,9 @@ export interface RangeMap extends BaseMap {
   from: string;
   from: string;
   to: string;
   to: string;
 }
 }
+
+export enum VizOrientation {
+  Auto = 'auto',
+  Vertical = 'vertical',
+  Horizontal = 'horizontal',
+}

+ 5 - 0
packages/grafana-ui/src/types/threshold.ts

@@ -0,0 +1,5 @@
+export interface Threshold {
+  index: number;
+  value: number;
+  color: string;
+}

+ 2 - 0
packages/grafana-ui/src/utils/index.ts

@@ -1,7 +1,9 @@
 export * from './processTimeSeries';
 export * from './processTimeSeries';
+export * from './singlestat';
 export * from './valueFormats/valueFormats';
 export * from './valueFormats/valueFormats';
 export * from './colors';
 export * from './colors';
 export * from './namedColorsPalette';
 export * from './namedColorsPalette';
+export * from './thresholds';
 export * from './string';
 export * from './string';
 export * from './deprecationWarning';
 export * from './deprecationWarning';
 export { getMappedValue } from './valueMappings';
 export { getMappedValue } from './valueMappings';

+ 33 - 0
packages/grafana-ui/src/utils/singlestat.ts

@@ -0,0 +1,33 @@
+import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
+import { processTimeSeries } from './processTimeSeries';
+
+export interface SingleStatProcessingOptions {
+  panelData: PanelData;
+  stat: string;
+}
+
+//
+// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
+//
+export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
+  const { panelData, stat } = options;
+
+  if (panelData.timeSeries) {
+    const timeSeries = processTimeSeries({
+      timeSeries: panelData.timeSeries,
+      nullValueMode: NullValueMode.Null,
+    });
+
+    return timeSeries.map((series, index) => {
+      const value = stat !== 'name' ? series.stats[stat] : series.label;
+
+      return {
+        value: value,
+      };
+    });
+  } else if (panelData.tableData) {
+    throw { message: 'Panel data not supported' };
+  }
+
+  return [];
+}

+ 23 - 0
packages/grafana-ui/src/utils/thresholds.ts

@@ -0,0 +1,23 @@
+import { Threshold } from '../types';
+
+export function getThresholdForValue(
+  thresholds: Threshold[],
+  value: number | null | string | undefined
+): Threshold | null {
+  if (thresholds.length === 1) {
+    return thresholds[0];
+  }
+
+  const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
+  if (atThreshold) {
+    return atThreshold;
+  }
+
+  const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
+  if (belowThreshold.length > 0) {
+    const nearestThreshold = belowThreshold.sort((t1: Threshold, t2: Threshold) => t2.value - t1.value)[0];
+    return nearestThreshold;
+  }
+
+  return null;
+}

+ 7 - 5
pkg/api/frontendsettings.go

@@ -192,16 +192,18 @@ func getPanelSort(id string) int {
 		sort = 2
 		sort = 2
 	case "gauge":
 	case "gauge":
 		sort = 3
 		sort = 3
-	case "table":
+	case "bargauge":
 		sort = 4
 		sort = 4
-	case "text":
+	case "table":
 		sort = 5
 		sort = 5
-	case "heatmap":
+	case "text":
 		sort = 6
 		sort = 6
-	case "alertlist":
+	case "heatmap":
 		sort = 7
 		sort = 7
-	case "dashlist":
+	case "alertlist":
 		sort = 8
 		sort = 8
+	case "dashlist":
+		sort = 9
 	}
 	}
 	return sort
 	return sort
 }
 }

+ 1 - 0
public/app/core/utils/ConfigProvider.tsx

@@ -15,6 +15,7 @@ export const provideConfig = (component: React.ComponentType<any>) => {
 
 
 export const getCurrentThemeName = () =>
 export const getCurrentThemeName = () =>
   config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
   config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
+
 export const getCurrentTheme = () => getTheme(getCurrentThemeName());
 export const getCurrentTheme = () => getTheme(getCurrentThemeName());
 
 
 export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
 export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {

+ 13 - 8
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -76,21 +76,26 @@ export class DashboardPanel extends PureComponent<Props, State> {
       // unmount angular panel
       // unmount angular panel
       this.cleanUpAngularPanel();
       this.cleanUpAngularPanel();
 
 
-      if (panel.type !== pluginId) {
-        this.props.panel.changeType(pluginId, fromAngularPanel);
-      }
-
-      if (plugin.exports) {
-        this.setState({ plugin, angularPanel: null });
-      } else {
+      if (!plugin.exports) {
         try {
         try {
           plugin.exports = await importPluginModule(plugin.module);
           plugin.exports = await importPluginModule(plugin.module);
         } catch (e) {
         } catch (e) {
           plugin = getPanelPluginNotFound(pluginId);
           plugin = getPanelPluginNotFound(pluginId);
         }
         }
+      }
 
 
-        this.setState({ plugin, angularPanel: null });
+      if (panel.type !== pluginId) {
+        if (fromAngularPanel) {
+          // for angular panels only we need to remove all events and let angular panels do some cleanup
+          panel.destroy();
+
+          this.props.panel.changeType(pluginId);
+        } else {
+          panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions);
+        }
       }
       }
+
+      this.setState({ plugin, angularPanel: null });
     }
     }
   }
   }
 
 

+ 6 - 5
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -2,9 +2,12 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
+// Components
+import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
+
 // Types
 // Types
+import { PanelPlugin, AppNotificationSeverity } from 'app/types';
 import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
 import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
-import { PanelPlugin } from 'app/types';
 
 
 interface Props {
 interface Props {
   pluginId: string;
   pluginId: string;
@@ -19,15 +22,13 @@ class PanelPluginNotFound extends PureComponent<Props> {
     const style = {
     const style = {
       display: 'flex',
       display: 'flex',
       alignItems: 'center',
       alignItems: 'center',
-      textAlign: 'center' as 'center',
+      justifyContent: 'center',
       height: '100%',
       height: '100%',
     };
     };
 
 
     return (
     return (
       <div style={style}>
       <div style={style}>
-        <div className="alert alert-error" style={{ margin: '0 auto' }}>
-          Panel plugin with id {this.props.pluginId} could not be found
-        </div>
+        <AlertBox severity={AppNotificationSeverity.Error} title={`Panel plugin not found: ${this.props.pluginId}`} />
       </div>
       </div>
     );
     );
   }
   }

+ 4 - 5
public/app/features/dashboard/state/PanelModel.test.ts

@@ -1,5 +1,4 @@
-import _ from 'lodash';
-import { PanelModel } from '../state/PanelModel';
+import { PanelModel } from './PanelModel';
 
 
 describe('PanelModel', () => {
 describe('PanelModel', () => {
   describe('when creating new panel model', () => {
   describe('when creating new panel model', () => {
@@ -66,7 +65,7 @@ describe('PanelModel', () => {
 
 
     describe('when changing panel type', () => {
     describe('when changing panel type', () => {
       beforeEach(() => {
       beforeEach(() => {
-        model.changeType('graph', true);
+        model.changeType('graph');
         model.alert = { id: 2 };
         model.alert = { id: 2 };
       });
       });
 
 
@@ -75,12 +74,12 @@ describe('PanelModel', () => {
       });
       });
 
 
       it('should restore table properties when changing back', () => {
       it('should restore table properties when changing back', () => {
-        model.changeType('table', true);
+        model.changeType('table');
         expect(model.showColumns).toBe(true);
         expect(model.showColumns).toBe(true);
       });
       });
 
 
       it('should remove alert rule when changing type that does not support it', () => {
       it('should remove alert rule when changing type that does not support it', () => {
-        model.changeType('table', true);
+        model.changeType('table');
         expect(model.alert).toBe(undefined);
         expect(model.alert).toBe(undefined);
       });
       });
     });
     });

+ 10 - 11
public/app/features/dashboard/state/PanelModel.ts

@@ -229,10 +229,6 @@ export class PanelModel {
     }, {});
     }, {});
   }
   }
 
 
-  private saveCurrentPanelOptions() {
-    this.cachedPluginOptions[this.type] = this.getOptionsToRemember();
-  }
-
   private restorePanelOptions(pluginId: string) {
   private restorePanelOptions(pluginId: string) {
     const prevOptions = this.cachedPluginOptions[pluginId] || {};
     const prevOptions = this.cachedPluginOptions[pluginId] || {};
 
 
@@ -241,14 +237,11 @@ export class PanelModel {
     });
     });
   }
   }
 
 
-  changeType(pluginId: string, fromAngularPanel: boolean) {
-    this.saveCurrentPanelOptions();
-    this.type = pluginId;
+  changeType(pluginId: string, preserveOptions?: any) {
+    const oldOptions: any = this.getOptionsToRemember();
+    const oldPluginId = this.type;
 
 
-    // for angular panels only we need to remove all events and let angular panels do some cleanup
-    if (fromAngularPanel) {
-      this.destroy();
-    }
+    this.type = pluginId;
 
 
     // remove panel type specific  options
     // remove panel type specific  options
     for (const key of _.keys(this)) {
     for (const key of _.keys(this)) {
@@ -259,7 +252,13 @@ export class PanelModel {
       delete this[key];
       delete this[key];
     }
     }
 
 
+    this.cachedPluginOptions[oldPluginId] = oldOptions;
     this.restorePanelOptions(pluginId);
     this.restorePanelOptions(pluginId);
+
+    if (preserveOptions && oldOptions) {
+      this.options = this.options || {};
+      Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options));
+    }
   }
   }
 
 
   addQuery(query?: Partial<DataQuery>) {
   addQuery(query?: Partial<DataQuery>) {

+ 2 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -27,6 +27,7 @@ import * as table2Panel from 'app/plugins/panel/table2/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
 import * as gaugePanel from 'app/plugins/panel/gauge/module';
 import * as gaugePanel from 'app/plugins/panel/gauge/module';
+import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
 
 
 const builtInPlugins = {
 const builtInPlugins = {
   'app/plugins/datasource/graphite/module': graphitePlugin,
   'app/plugins/datasource/graphite/module': graphitePlugin,
@@ -58,6 +59,7 @@ const builtInPlugins = {
   'app/plugins/panel/singlestat/module': singlestatPanel,
   'app/plugins/panel/singlestat/module': singlestatPanel,
   'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
   'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
   'app/plugins/panel/gauge/module': gaugePanel,
   'app/plugins/panel/gauge/module': gaugePanel,
+  'app/plugins/panel/bargauge/module': barGaugePanel,
 };
 };
 
 
 export default builtInPlugins;
 export default builtInPlugins;

+ 56 - 0
public/app/plugins/panel/bargauge/BarGaugePanel.tsx

@@ -0,0 +1,56 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Services & Utils
+import { processSingleStatPanelData } from '@grafana/ui';
+import { config } from 'app/core/config';
+
+// Components
+import { BarGauge, VizRepeater } from '@grafana/ui';
+
+// Types
+import { BarGaugeOptions } from './types';
+import { PanelProps } from '@grafana/ui/src/types';
+
+interface Props extends PanelProps<BarGaugeOptions> {}
+
+export class BarGaugePanel extends PureComponent<Props> {
+  renderBarGauge(value, width, height) {
+    const { replaceVariables, options } = this.props;
+    const { valueOptions } = options;
+
+    const prefix = replaceVariables(valueOptions.prefix);
+    const suffix = replaceVariables(valueOptions.suffix);
+
+    return (
+      <BarGauge
+        value={value}
+        width={width}
+        height={height}
+        prefix={prefix}
+        suffix={suffix}
+        orientation={options.orientation}
+        unit={valueOptions.unit}
+        decimals={valueOptions.decimals}
+        thresholds={options.thresholds}
+        valueMappings={options.valueMappings}
+        theme={config.theme}
+      />
+    );
+  }
+
+  render() {
+    const { panelData, options, width, height } = this.props;
+
+    const values = processSingleStatPanelData({
+      panelData: panelData,
+      stat: options.valueOptions.stat,
+    });
+
+    return (
+      <VizRepeater height={height} width={width} values={values} orientation={options.orientation}>
+        {({ vizHeight, vizWidth, valueInfo }) => this.renderBarGauge(valueInfo.value, vizWidth, vizHeight)}
+      </VizRepeater>
+    );
+  }
+}

+ 64 - 0
public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx

@@ -0,0 +1,64 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Components
+import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
+import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
+
+// Types
+import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
+import { BarGaugeOptions, orientationOptions } from './types';
+import { SingleStatValueOptions } from '../gauge/types';
+
+export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
+  onThresholdsChanged = (thresholds: Threshold[]) =>
+    this.props.onOptionsChange({
+      ...this.props.options,
+      thresholds,
+    });
+
+  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
+    this.props.onOptionsChange({
+      ...this.props.options,
+      valueMappings,
+    });
+
+  onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+    this.props.onOptionsChange({
+      ...this.props.options,
+      valueOptions,
+    });
+
+  onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
+  onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
+  onOrientationChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
+
+  render() {
+    const { options } = this.props;
+
+    return (
+      <>
+        <PanelOptionsGrid>
+          <SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
+          <PanelOptionsGroup title="Gauge">
+            <FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
+            <FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={options.maxValue} />
+            <div className="form-field">
+              <FormLabel width={8}>Orientation</FormLabel>
+              <Select
+                width={12}
+                options={orientationOptions}
+                defaultValue={orientationOptions[0]}
+                onChange={this.onOrientationChange}
+                value={orientationOptions.find(item => item.value === options.orientation)}
+              />
+            </div>
+          </PanelOptionsGroup>
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
+        </PanelOptionsGrid>
+
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
+      </>
+    );
+  }
+}

+ 96 - 0
public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="218.21px" height="187.4px" viewBox="0 0 218.21 187.4" style="enable-background:new 0 0 218.21 187.4;"
+	 xml:space="preserve">
+<style type="text/css">
+	.st0{fill:url(#SVGID_1_);}
+	.st1{fill:url(#SVGID_2_);}
+	.st2{fill:url(#SVGID_3_);}
+	.st3{fill:url(#SVGID_4_);}
+	.st4{fill:url(#SVGID_5_);}
+	.st5{fill:url(#SVGID_6_);}
+	.st6{fill:url(#SVGID_7_);}
+	.st7{fill:url(#SVGID_8_);}
+	.st8{fill:url(#SVGID_9_);}
+</style>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="68.7753" y1="8.4663" x2="68.7753" y2="59.7143">
+	<stop  offset="0" style="stop-color:#FAA91F"/>
+	<stop  offset="1" style="stop-color:#F37324"/>
+</linearGradient>
+<path class="st0" d="M7.61,50.71V12.52c0-2.66,1.95-4.82,4.36-4.82h113.62c2.41,0,4.36,2.16,4.36,4.82v38.19
+	c0,2.66-1.95,4.82-4.36,4.82H11.97C9.56,55.53,7.61,53.37,7.61,50.71z"/>
+<g>
+	<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="152.8987" y1="19.9183" x2="152.3315" y2="39.7688">
+		<stop  offset="0" style="stop-color:#FAA91F"/>
+		<stop  offset="1" style="stop-color:#F37324"/>
+	</linearGradient>
+	<path class="st1" d="M153.89,28.04c0.95,0,1.78,0.18,2.47,0.55c0.7,0.36,1.28,0.86,1.75,1.49c0.47,0.63,0.81,1.37,1.05,2.23
+		c0.23,0.86,0.35,1.79,0.35,2.78c0,1.24-0.17,2.33-0.52,3.29c-0.34,0.96-0.83,1.77-1.46,2.43c-0.63,0.66-1.37,1.16-2.22,1.5
+		c-0.85,0.34-1.78,0.52-2.79,0.52c-1.03,0-1.98-0.17-2.85-0.5c-0.87-0.33-1.62-0.83-2.25-1.49c-0.63-0.66-1.12-1.46-1.47-2.41
+		c-0.35-0.95-0.53-2.03-0.53-3.25c0-1.32,0.17-2.5,0.52-3.55c0.34-1.05,0.78-2.07,1.31-3.04l4.1-7.71h5.56l-4.16,7.35l0.03,0.03
+		c0.1-0.06,0.27-0.11,0.5-0.15C153.5,28.06,153.71,28.04,153.89,28.04z M154.83,35.02c0-1.05-0.21-1.91-0.62-2.57
+		c-0.41-0.66-0.99-0.99-1.72-0.99c-0.69,0-1.26,0.33-1.72,1c-0.46,0.67-0.68,1.52-0.68,2.55c0,1.03,0.22,1.88,0.67,2.55
+		c0.44,0.67,1.01,1,1.7,1c0.71,0,1.28-0.33,1.72-1C154.62,36.9,154.83,36.05,154.83,35.02z"/>
+	<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="167.1879" y1="20.3265" x2="166.6207" y2="40.1771">
+		<stop  offset="0" style="stop-color:#FAA91F"/>
+		<stop  offset="1" style="stop-color:#F37324"/>
+	</linearGradient>
+	<path class="st2" d="M173.57,26.79c0,0.71-0.09,1.37-0.27,1.97c-0.18,0.61-0.43,1.18-0.73,1.73c-0.3,0.55-0.65,1.07-1.03,1.58
+		c-0.38,0.51-0.78,0.99-1.18,1.46l-4.01,4.65h6.95v4.19h-12.72v-4.31l6.53-7.5c0.53-0.61,0.94-1.23,1.24-1.87
+		c0.3-0.64,0.46-1.25,0.46-1.84c0-0.63-0.15-1.16-0.44-1.61c-0.29-0.45-0.75-0.67-1.38-0.67c-0.59,0-1.07,0.24-1.46,0.71
+		c-0.38,0.48-0.62,1.17-0.7,2.08l-4.56-0.43c0.24-2.19,0.99-3.82,2.23-4.9c1.24-1.08,2.79-1.62,4.63-1.62
+		c0.99,0,1.88,0.15,2.67,0.46c0.79,0.3,1.46,0.73,2.02,1.28c0.56,0.55,0.99,1.21,1.29,2C173.41,24.94,173.57,25.82,173.57,26.79z"/>
+</g>
+<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="90.8472" y1="69.5294" x2="90.8472" y2="118.2775">
+	<stop  offset="0" style="stop-color:#E81E25"/>
+	<stop  offset="1" style="stop-color:#A91D39"/>
+</linearGradient>
+<path class="st3" d="M7.61,115.16V71.24c0-1.08,0.87-1.95,1.95-1.95h162.57c1.08,0,1.95,0.87,1.95,1.95v43.92
+	c0,1.08-0.87,1.95-1.95,1.95H9.56C8.48,117.11,7.61,116.23,7.61,115.16z"/>
+<g>
+	<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="195.8516" y1="81.1631" x2="196.9859" y2="100.4465">
+		<stop  offset="0" style="stop-color:#E81E25"/>
+		<stop  offset="1" style="stop-color:#A91D39"/>
+	</linearGradient>
+	<path class="st4" d="M196.03,103.72h-5.28l6.35-17.22h-7.38v-4.28h12.66v3.64L196.03,103.72z"/>
+	<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="208.6078" y1="80.4128" x2="209.7422" y2="99.6962">
+		<stop  offset="0" style="stop-color:#E81E25"/>
+		<stop  offset="1" style="stop-color:#A91D39"/>
+	</linearGradient>
+	<path class="st5" d="M210.98,89.39c0.95,0,1.78,0.18,2.47,0.55c0.7,0.36,1.28,0.86,1.75,1.49c0.47,0.63,0.81,1.37,1.05,2.23
+		c0.23,0.86,0.35,1.79,0.35,2.78c0,1.24-0.17,2.33-0.52,3.29c-0.34,0.96-0.83,1.77-1.46,2.43c-0.63,0.66-1.37,1.16-2.22,1.5
+		c-0.85,0.34-1.78,0.52-2.79,0.52c-1.03,0-1.99-0.17-2.85-0.5c-0.87-0.33-1.62-0.83-2.25-1.49c-0.63-0.66-1.12-1.46-1.47-2.41
+		c-0.35-0.95-0.53-2.03-0.53-3.25c0-1.32,0.17-2.5,0.52-3.55c0.34-1.05,0.78-2.07,1.31-3.04l4.1-7.71h5.56l-4.16,7.35l0.03,0.03
+		c0.1-0.06,0.27-0.11,0.5-0.15C210.59,89.41,210.8,89.39,210.98,89.39z M211.92,96.37c0-1.05-0.21-1.91-0.62-2.57
+		c-0.41-0.66-0.99-0.99-1.72-0.99c-0.69,0-1.26,0.33-1.72,1c-0.46,0.67-0.68,1.52-0.68,2.55s0.22,1.88,0.67,2.55
+		c0.44,0.67,1.01,1,1.7,1c0.71,0,1.28-0.33,1.72-1C211.7,98.25,211.92,97.4,211.92,96.37z"/>
+</g>
+<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="49.8893" y1="131.8424" x2="49.8893" y2="178.0907">
+	<stop  offset="0" style="stop-color:#04A64D"/>
+	<stop  offset="1" style="stop-color:#007E39"/>
+</linearGradient>
+<path class="st6" d="M7.61,176.74v-43.92c0-1.08,0.87-1.95,1.95-1.95h80.66c1.08,0,1.95,0.87,1.95,1.95v43.92
+	c0,1.08-0.87,1.95-1.95,1.95H9.56C8.48,178.69,7.61,177.81,7.61,176.74z"/>
+<g>
+	<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="112.038" y1="145.9514" x2="118.2768" y2="172.6079">
+		<stop  offset="0" style="stop-color:#04A64D"/>
+		<stop  offset="1" style="stop-color:#007E39"/>
+	</linearGradient>
+	<path class="st7" d="M119.69,161.21v4.31h-4.31v-4.31h-7.87v-4.13l6.29-13.06h5.89v13.18h2.19v4.01H119.69z M115.47,148.77h-0.06
+		l-3.68,8.44h3.74V148.77z"/>
+	<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="126.6068" y1="142.5417" x2="132.8455" y2="169.1982">
+		<stop  offset="0" style="stop-color:#04A64D"/>
+		<stop  offset="1" style="stop-color:#007E39"/>
+	</linearGradient>
+	<path class="st8" d="M136.52,159.24c0,1.09-0.18,2.06-0.53,2.88c-0.35,0.83-0.84,1.53-1.44,2.11c-0.61,0.58-1.32,1.01-2.14,1.31
+		c-0.82,0.29-1.71,0.44-2.66,0.44c-1.76,0-3.27-0.45-4.52-1.35c-1.26-0.9-2.09-2.27-2.49-4.11l4.28-1.09
+		c0.16,0.71,0.45,1.29,0.85,1.73c0.4,0.45,0.96,0.67,1.67,0.67c0.38,0,0.72-0.08,1-0.24c0.28-0.16,0.52-0.37,0.71-0.64
+		s0.33-0.56,0.43-0.9c0.09-0.33,0.14-0.67,0.14-1.02c0-0.93-0.27-1.62-0.8-2.07c-0.54-0.45-1.25-0.67-2.14-0.67h-1.09v-3.58h1.21
+		c0.81,0,1.43-0.24,1.87-0.71c0.43-0.48,0.65-1.14,0.65-1.99c0-0.55-0.16-1.06-0.47-1.55c-0.31-0.49-0.82-0.73-1.5-0.73
+		c-0.59,0-1.04,0.19-1.37,0.58c-0.32,0.39-0.57,0.87-0.73,1.46l-4.16-1.03c0.24-0.93,0.59-1.72,1.05-2.37s0.97-1.18,1.55-1.59
+		c0.58-0.42,1.21-0.72,1.9-0.91c0.69-0.19,1.39-0.29,2.1-0.29c0.79,0,1.55,0.12,2.29,0.36c0.74,0.24,1.4,0.61,1.99,1.09
+		c0.59,0.49,1.06,1.09,1.41,1.82c0.35,0.73,0.53,1.58,0.53,2.55c0,1.26-0.27,2.31-0.8,3.17c-0.54,0.86-1.25,1.43-2.14,1.72v0.09
+		c1.03,0.28,1.85,0.87,2.46,1.76C136.22,157.04,136.52,158.07,136.52,159.24z"/>
+</g>
+</svg>

+ 22 - 0
public/app/plugins/panel/bargauge/module.tsx

@@ -0,0 +1,22 @@
+import { ReactPanelPlugin } from '@grafana/ui';
+
+import { BarGaugePanel } from './BarGaugePanel';
+import { BarGaugePanelEditor } from './BarGaugePanelEditor';
+import { BarGaugeOptions, defaults } from './types';
+
+export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
+
+reactPanel.setEditor(BarGaugePanelEditor);
+reactPanel.setDefaults(defaults);
+reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
+  const options: Partial<BarGaugeOptions> = {};
+
+  if (prevOptions.valueOptions) {
+    options.valueOptions = prevOptions.valueOptions;
+    options.thresholds = prevOptions.thresholds;
+    options.maxValue = prevOptions.maxValue;
+    options.minValue = prevOptions.minValue;
+  }
+
+  return options;
+});

+ 19 - 0
public/app/plugins/panel/bargauge/plugin.json

@@ -0,0 +1,19 @@
+{
+  "type": "panel",
+  "name": "Bar Gauge",
+  "id": "bargauge",
+  "state": "alpha",
+
+  "dataFormats": ["time_series"],
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icon_bar_gauge.svg",
+      "large": "img/icon_bar_gauge.svg"
+    }
+  }
+}

+ 31 - 0
public/app/plugins/panel/bargauge/types.ts

@@ -0,0 +1,31 @@
+import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui';
+import { SingleStatValueOptions } from '../gauge/types';
+
+export interface BarGaugeOptions {
+  minValue: number;
+  maxValue: number;
+  orientation: VizOrientation;
+  valueOptions: SingleStatValueOptions;
+  valueMappings: ValueMapping[];
+  thresholds: Threshold[];
+}
+
+export const orientationOptions: SelectOptionItem[] = [
+  { value: VizOrientation.Horizontal, label: 'Horizontal' },
+  { value: VizOrientation.Vertical, label: 'Vertical' },
+];
+
+export const defaults: BarGaugeOptions = {
+  minValue: 0,
+  maxValue: 100,
+  orientation: VizOrientation.Horizontal,
+  valueOptions: {
+    unit: 'none',
+    stat: 'avg',
+    prefix: '',
+    suffix: '',
+    decimals: null,
+  },
+  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+  valueMappings: [],
+};

+ 6 - 4
public/app/plugins/panel/gauge/GaugeOptionsBox.tsx

@@ -9,6 +9,8 @@ import { FormField, PanelEditorProps } from '@grafana/ui';
 import { GaugeOptions } from './types';
 import { GaugeOptions } from './types';
 
 
 export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
 export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
+  labelWidth = 8;
+
   onToggleThresholdLabels = () =>
   onToggleThresholdLabels = () =>
     this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
     this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
 
 
@@ -28,17 +30,17 @@ export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions
 
 
     return (
     return (
       <PanelOptionsGroup title="Gauge">
       <PanelOptionsGroup title="Gauge">
-        <FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={minValue} />
-        <FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={maxValue} />
+        <FormField label="Min value" labelWidth={this.labelWidth} onChange={this.onMinValueChange} value={minValue} />
+        <FormField label="Max value" labelWidth={this.labelWidth} onChange={this.onMaxValueChange} value={maxValue} />
         <Switch
         <Switch
           label="Show labels"
           label="Show labels"
-          labelClass="width-8"
+          labelClass={`width-${this.labelWidth}`}
           checked={showThresholdLabels}
           checked={showThresholdLabels}
           onChange={this.onToggleThresholdLabels}
           onChange={this.onToggleThresholdLabels}
         />
         />
         <Switch
         <Switch
           label="Show markers"
           label="Show markers"
-          labelClass="width-8"
+          labelClass={`width-${this.labelWidth}`}
           checked={showThresholdMarkers}
           checked={showThresholdMarkers}
           onChange={this.onToggleThresholdMarkers}
           onChange={this.onToggleThresholdMarkers}
         />
         />

+ 37 - 60
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -1,82 +1,59 @@
 // Libraries
 // Libraries
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 
 
 // Services & Utils
 // Services & Utils
-import { processTimeSeries, ThemeContext } from '@grafana/ui';
+import { processSingleStatPanelData } from '@grafana/ui';
+import { config } from 'app/core/config';
 
 
 // Components
 // Components
-import { Gauge } from '@grafana/ui';
+import { Gauge, VizRepeater } from '@grafana/ui';
 
 
 // Types
 // Types
 import { GaugeOptions } from './types';
 import { GaugeOptions } from './types';
-import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
+import { PanelProps, VizOrientation } from '@grafana/ui/src/types';
 
 
 interface Props extends PanelProps<GaugeOptions> {}
 interface Props extends PanelProps<GaugeOptions> {}
-interface State {
-  value: TimeSeriesValue;
-}
-
-export class GaugePanel extends Component<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      value: this.findValue(props),
-    };
-  }
 
 
-  componentDidUpdate(prevProps: Props) {
-    if (this.props.panelData !== prevProps.panelData) {
-      this.setState({ value: this.findValue(this.props) });
-    }
-  }
-
-  findValue(props: Props): number | null {
-    const { panelData, options } = props;
+export class GaugePanel extends PureComponent<Props> {
+  renderGauge(value, width, height) {
+    const { replaceVariables, options } = this.props;
     const { valueOptions } = options;
     const { valueOptions } = options;
 
 
-    if (panelData.timeSeries) {
-      const vmSeries = processTimeSeries({
-        timeSeries: panelData.timeSeries,
-        nullValueMode: NullValueMode.Null,
-      });
+    const prefix = replaceVariables(valueOptions.prefix);
+    const suffix = replaceVariables(valueOptions.suffix);
 
 
-      if (vmSeries[0]) {
-        return vmSeries[0].stats[valueOptions.stat];
-      }
-    } else if (panelData.tableData) {
-      return panelData.tableData.rows[0].find(prop => prop > 0);
-    }
-    return null;
+    return (
+      <Gauge
+        value={value}
+        width={width}
+        height={height}
+        prefix={prefix}
+        suffix={suffix}
+        unit={valueOptions.unit}
+        decimals={valueOptions.decimals}
+        thresholds={options.thresholds}
+        valueMappings={options.valueMappings}
+        showThresholdLabels={options.showThresholdLabels}
+        showThresholdMarkers={options.showThresholdMarkers}
+        minValue={options.minValue}
+        maxValue={options.maxValue}
+        theme={config.theme}
+      />
+    );
   }
   }
 
 
   render() {
   render() {
-    const { width, height, replaceVariables, options } = this.props;
-    const { valueOptions } = options;
-    const { value } = this.state;
+    const { panelData, options, height, width } = this.props;
+
+    const values = processSingleStatPanelData({
+      panelData: panelData,
+      stat: options.valueOptions.stat,
+    });
 
 
-    const prefix = replaceVariables(valueOptions.prefix);
-    const suffix = replaceVariables(valueOptions.suffix);
     return (
     return (
-      <ThemeContext.Consumer>
-        {theme => (
-          <Gauge
-            value={value}
-            width={width}
-            height={height}
-            prefix={prefix}
-            suffix={suffix}
-            unit={valueOptions.unit}
-            decimals={valueOptions.decimals}
-            thresholds={options.thresholds}
-            valueMappings={options.valueMappings}
-            showThresholdLabels={options.showThresholdLabels}
-            showThresholdMarkers={options.showThresholdMarkers}
-            minValue={options.minValue}
-            maxValue={options.maxValue}
-            theme={theme}
-          />
-        )}
-      </ThemeContext.Consumer>
+      <VizRepeater height={height} width={width} values={values} orientation={VizOrientation.Auto}>
+        {({ vizHeight, vizWidth, valueInfo }) => this.renderGauge(valueInfo.value, vizWidth, vizHeight)}
+      </VizRepeater>
     );
     );
   }
   }
 }
 }

+ 1 - 0
public/app/plugins/panel/gauge/GaugePanelEditor.tsx

@@ -1,3 +1,4 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import {
 import {
   PanelEditorProps,
   PanelEditorProps,

+ 12 - 0
public/app/plugins/panel/gauge/module.tsx

@@ -8,3 +8,15 @@ export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
 
 
 reactPanel.setEditor(GaugePanelEditor);
 reactPanel.setEditor(GaugePanelEditor);
 reactPanel.setDefaults(defaults);
 reactPanel.setDefaults(defaults);
+reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
+  const options: Partial<GaugeOptions> = {};
+
+  if (prevOptions.valueOptions) {
+    options.valueOptions = prevOptions.valueOptions;
+    options.thresholds = prevOptions.thresholds;
+    options.maxValue = prevOptions.maxValue;
+    options.minValue = prevOptions.minValue;
+  }
+
+  return options;
+});

+ 1 - 1
public/app/plugins/panel/gauge/types.ts

@@ -31,5 +31,5 @@ export const defaults: GaugeOptions = {
     unit: 'none',
     unit: 'none',
   },
   },
   valueMappings: [],
   valueMappings: [],
-  thresholds: [],
+  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
 };
 };

+ 2 - 0
public/sass/utils/_utils.scss

@@ -96,7 +96,9 @@ button.close {
 }
 }
 
 
 .center-vh {
 .center-vh {
+  height: 100%;
   display: flex;
   display: flex;
+  flex-direction: column;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   justify-items: center;
   justify-items: center;