Procházet zdrojové kódy

merge master, works with bar gauge now

ryan před 6 roky
rodič
revize
1bc8e7a8d1
93 změnil soubory, kde provedl 3581 přidání a 390 odebrání
  1. 296 0
      devenv/dev-dashboards/panel_tests_multiseries_gauge.json
  2. 54 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
  3. 62 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
  4. 224 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. 10 1
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx
  8. 23 0
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx
  9. 29 19
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  10. 1 1
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  11. 56 0
      packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx
  12. 0 53
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  13. 18 22
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  14. 99 0
      packages/grafana-ui/src/components/Table/Table.story.tsx
  15. 287 0
      packages/grafana-ui/src/components/Table/Table.tsx
  16. 291 0
      packages/grafana-ui/src/components/Table/TableCellBuilder.tsx
  17. 80 0
      packages/grafana-ui/src/components/Table/_Table.scss
  18. 167 0
      packages/grafana-ui/src/components/Table/examples.ts
  19. 2 3
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  20. 77 0
      packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx
  21. 2 0
      packages/grafana-ui/src/components/index.scss
  22. 6 2
      packages/grafana-ui/src/components/index.ts
  23. 8 21
      packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
  24. 4 4
      packages/grafana-ui/src/themes/default.ts
  25. 10 4
      packages/grafana-ui/src/types/data.ts
  26. 3 1
      packages/grafana-ui/src/types/datasource.ts
  27. 1 0
      packages/grafana-ui/src/types/index.ts
  28. 13 11
      packages/grafana-ui/src/types/panel.ts
  29. 2 2
      packages/grafana-ui/src/types/theme.ts
  30. 5 0
      packages/grafana-ui/src/types/threshold.ts
  31. 2 2
      packages/grafana-ui/src/utils/displayValue.ts
  32. 2 0
      packages/grafana-ui/src/utils/index.ts
  33. 26 2
      packages/grafana-ui/src/utils/processTableData.ts
  34. 39 0
      packages/grafana-ui/src/utils/singlestat.ts
  35. 24 0
      packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx
  36. 23 0
      packages/grafana-ui/src/utils/thresholds.ts
  37. 7 5
      pkg/api/frontendsettings.go
  38. 1 1
      pkg/tsdb/graphite/graphite.go
  39. 1 0
      public/app/core/utils/ConfigProvider.tsx
  40. 18 4
      public/app/core/utils/explore.ts
  41. 5 3
      public/app/features/dashboard/components/DashboardSettings/template.html
  42. 17 4
      public/app/features/dashboard/containers/DashboardPage.tsx
  43. 13 8
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  44. 6 5
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  45. 4 5
      public/app/features/dashboard/state/PanelModel.test.ts
  46. 10 11
      public/app/features/dashboard/state/PanelModel.ts
  47. 2 1
      public/app/features/explore/state/actions.ts
  48. 4 0
      public/app/features/plugins/built_in_plugins.ts
  49. 36 0
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  50. 75 0
      public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
  51. 96 0
      public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg
  52. 23 0
      public/app/plugins/panel/bargauge/module.tsx
  53. 19 0
      public/app/plugins/panel/bargauge/plugin.json
  54. 34 0
      public/app/plugins/panel/bargauge/types.ts
  55. 6 4
      public/app/plugins/panel/gauge/GaugeOptionsBox.tsx
  56. 23 74
      public/app/plugins/panel/gauge/GaugePanel.tsx
  57. 1 0
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  58. 77 0
      public/app/plugins/panel/gauge/SingleStatPanel.tsx
  59. 13 0
      public/app/plugins/panel/gauge/module.tsx
  60. 6 18
      public/app/plugins/panel/gauge/types.ts
  61. 5 3
      public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx
  62. 11 0
      public/app/plugins/panel/graph/axes_editor.html
  63. 10 2
      public/app/plugins/panel/graph/graph.ts
  64. 4 0
      public/app/plugins/panel/graph/histogram.ts
  65. 404 0
      public/app/plugins/panel/graph/specs/graph.test.ts
  66. 20 11
      public/app/plugins/panel/heatmap/color_legend.ts
  67. 6 4
      public/app/plugins/panel/heatmap/rendering.ts
  68. 4 3
      public/app/plugins/panel/table/renderer.ts
  69. 9 0
      public/app/plugins/panel/table2/README.md
  70. 29 0
      public/app/plugins/panel/table2/TablePanel.tsx
  71. 55 0
      public/app/plugins/panel/table2/TablePanelEditor.tsx
  72. 67 0
      public/app/plugins/panel/table2/img/icn-table-panel.svg
  73. 9 0
      public/app/plugins/panel/table2/module.tsx
  74. 19 0
      public/app/plugins/panel/table2/plugin.json
  75. 35 0
      public/app/plugins/panel/table2/types.ts
  76. 8 1
      public/app/types/explore.ts
  77. 4 18
      public/sass/_variables.generated.scss
  78. 1 1
      public/sass/base/_forms.scss
  79. 2 2
      public/sass/base/_reboot.scss
  80. 2 2
      public/sass/base/_type.scss
  81. 5 5
      public/sass/components/_buttons.scss
  82. 1 1
      public/sass/components/_code_editor.scss
  83. 5 5
      public/sass/components/_gf-form.scss
  84. 1 1
      public/sass/components/_panel_gettingstarted.scss
  85. 1 1
      public/sass/components/_slate_editor.scss
  86. 1 1
      public/sass/components/_submenu.scss
  87. 2 2
      public/sass/components/_switch.scss
  88. 1 1
      public/sass/components/_toolbar.scss
  89. 2 0
      public/sass/utils/_utils.scss
  90. 19 0
      scripts/ci-frontend-metrics.sh
  91. 18 0
      scripts/ci-metrics-publisher.sh
  92. 0 30
      scripts/circle-metrics.sh
  93. 11 5
      scripts/circle-test-frontend.sh

+ 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 },
+    ],
+  });
+});

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

@@ -0,0 +1,62 @@
+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,
+    minValue: 0,
+    thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
+    height: 300,
+    width: 300,
+    value: {
+      text: '25',
+      numeric: 25,
+    },
+    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();
+  });
+});

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

@@ -0,0 +1,224 @@
+// Library
+import React, { PureComponent, CSSProperties } from 'react';
+import tinycolor from 'tinycolor2';
+
+// Utils
+import { getColorFromHexRgbOrName, getThresholdForValue, DisplayValue } from '../../utils';
+
+// Types
+import { Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
+
+const BAR_SIZE_RATIO = 0.8;
+
+export interface Props extends Themeable {
+  height: number;
+  width: number;
+  thresholds: Threshold[];
+  value: DisplayValue;
+  maxValue: number;
+  minValue: number;
+  orientation: VizOrientation;
+}
+
+/*
+ * 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: {
+      text: '100',
+      numeric: 100,
+    },
+    orientation: VizOrientation.Horizontal,
+    thresholds: [],
+  };
+
+  getValueColors(): BarColors {
+    const { thresholds, theme, value } = this.props;
+
+    const activeThreshold = getThresholdForValue(thresholds, value.numeric);
+
+    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.numeric)) {
+        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, value } = this.props;
+
+    const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
+    const vertical = orientation === 'vertical';
+
+    return vertical
+      ? this.renderVerticalBar(value.text, valuePercent)
+      : this.renderHorizontalLCD(value.text, 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>
+`;

+ 10 - 1
packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx

@@ -50,7 +50,16 @@ ColorPickerStories.add('Series color picker', () => {
             color={selectedColor}
             color={selectedColor}
             onChange={color => updateSelectedColor(color)}
             onChange={color => updateSelectedColor(color)}
           >
           >
-            <div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
+            {({ ref, showColorPicker, hideColorPicker }) => (
+              <div
+                ref={ref}
+                onMouseLeave={hideColorPicker}
+                onClick={showColorPicker}
+                style={{ color: selectedColor, cursor: 'pointer' }}
+              >
+                Open color picker
+              </div>
+            )}
           </SeriesColorPicker>
           </SeriesColorPicker>
         );
         );
       }}
       }}

+ 23 - 0
packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { ColorPicker } from './ColorPicker';
+import { ColorPickerTrigger } from './ColorPickerTrigger';
+
+describe('ColorPicker', () => {
+  it('renders ColorPickerTrigger component by default', () => {
+    expect(
+      renderer.create(<ColorPicker color="#EAB839" onChange={() => {}} />).root.findByType(ColorPickerTrigger)
+    ).toBeTruthy();
+  });
+
+  it('renders custom trigger when supplied', () => {
+    const div = renderer
+      .create(
+        <ColorPicker color="#EAB839" onChange={() => {}}>
+          {() => <div>Custom trigger</div>}
+        </ColorPicker>
+      )
+      .root.findByType('div');
+    expect(div.children[0]).toBe('Custom trigger');
+  });
+});

+ 29 - 19
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

@@ -1,4 +1,5 @@
 import React, { Component, createRef } from 'react';
 import React, { Component, createRef } from 'react';
+import { omit } from 'lodash';
 import { PopperController } from '../Tooltip/PopperController';
 import { PopperController } from '../Tooltip/PopperController';
 import { Popper } from '../Tooltip/Popper';
 import { Popper } from '../Tooltip/Popper';
 import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover';
 import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover';
@@ -6,14 +7,29 @@ import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
 import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
 import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
 
 
 import { withTheme } from '../../themes/ThemeContext';
 import { withTheme } from '../../themes/ThemeContext';
+import { ColorPickerTrigger } from './ColorPickerTrigger';
+
+/**
+ * If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function
+ * as a child. You will get show/hide function which you can map to desired interaction (like onClick or onMouseLeave)
+ * and a ref which needs to be passed to an HTMLElement for correct positioning. If you want to use class or functional
+ * component as a custom trigger you will need to forward the reference to first HTMLElement child.
+ */
+type ColorPickerTriggerRenderer = (props: {
+  // This should be a React.RefObject<HTMLElement> but due to how object refs are defined you cannot downcast from that
+  // to a specific type like React.RefObject<HTMLDivElement> even though it would be fine in runtime.
+  ref: React.RefObject<any>;
+  showColorPicker: () => void;
+  hideColorPicker: () => void;
+}) => React.ReactNode;
 
 
 export const colorPickerFactory = <T extends ColorPickerProps>(
 export const colorPickerFactory = <T extends ColorPickerProps>(
   popover: React.ComponentType<T>,
   popover: React.ComponentType<T>,
   displayName = 'ColorPicker'
   displayName = 'ColorPicker'
 ) => {
 ) => {
-  return class ColorPicker extends Component<T, any> {
+  return class ColorPicker extends Component<T & { children?: ColorPickerTriggerRenderer }, any> {
     static displayName = displayName;
     static displayName = displayName;
-    pickerTriggerRef = createRef<HTMLDivElement>();
+    pickerTriggerRef = createRef<any>();
 
 
     onColorChange = (color: string) => {
     onColorChange = (color: string) => {
       const { onColorChange, onChange } = this.props;
       const { onColorChange, onChange } = this.props;
@@ -23,11 +39,11 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
     };
     };
 
 
     render() {
     render() {
+      const { theme, children } = this.props;
       const popoverElement = React.createElement(popover, {
       const popoverElement = React.createElement(popover, {
-        ...this.props,
+        ...omit(this.props, 'children'),
         onChange: this.onColorChange,
         onChange: this.onColorChange,
       });
       });
-      const { theme, children } = this.props;
 
 
       return (
       return (
         <PopperController content={popoverElement} hideAfter={300}>
         <PopperController content={popoverElement} hideAfter={300}>
@@ -45,27 +61,21 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
                 )}
                 )}
 
 
                 {children ? (
                 {children ? (
-                  React.cloneElement(children as JSX.Element, {
+                  // Children have a bit weird type due to intersection used in the definition so we need to cast here,
+                  // but the definition is correct and should not allow to pass a children that does not conform to
+                  // ColorPickerTriggerRenderer type.
+                  (children as ColorPickerTriggerRenderer)({
                     ref: this.pickerTriggerRef,
                     ref: this.pickerTriggerRef,
-                    onClick: showPopper,
-                    onMouseLeave: hidePopper,
+                    showColorPicker: showPopper,
+                    hideColorPicker: hidePopper,
                   })
                   })
                 ) : (
                 ) : (
-                  <div
+                  <ColorPickerTrigger
                     ref={this.pickerTriggerRef}
                     ref={this.pickerTriggerRef}
                     onClick={showPopper}
                     onClick={showPopper}
                     onMouseLeave={hidePopper}
                     onMouseLeave={hidePopper}
-                    className="sp-replacer sp-light"
-                  >
-                    <div className="sp-preview">
-                      <div
-                        className="sp-preview-inner"
-                        style={{
-                          backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme.type),
-                        }}
-                      />
-                    </div>
-                  </div>
+                    color={getColorFromHexRgbOrName(this.props.color || '#000000', theme.type)}
+                  />
                 )}
                 )}
               </>
               </>
             );
             );

+ 1 - 1
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

@@ -17,8 +17,8 @@ export interface ColorPickerProps extends Themeable {
    */
    */
   onColorChange?: ColorPickerChangeHandler;
   onColorChange?: ColorPickerChangeHandler;
   enableNamedColors?: boolean;
   enableNamedColors?: boolean;
-  children?: JSX.Element;
 }
 }
+
 export interface Props<T> extends ColorPickerProps, PopperContentProps {
 export interface Props<T> extends ColorPickerProps, PopperContentProps {
   customPickers?: T;
   customPickers?: T;
 }
 }

+ 56 - 0
packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx

@@ -0,0 +1,56 @@
+import React, { forwardRef } from 'react';
+
+interface ColorPickerTriggerProps {
+  onClick: () => void;
+  onMouseLeave: () => void;
+  color: string;
+}
+
+export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger(
+  props: ColorPickerTriggerProps,
+  ref: React.Ref<HTMLDivElement>
+) {
+  return (
+    <div
+      ref={ref}
+      onClick={props.onClick}
+      onMouseLeave={props.onMouseLeave}
+      style={{
+        overflow: 'hidden',
+        background: 'inherit',
+        border: 'none',
+        color: 'inherit',
+        padding: 0,
+        borderRadius: 10,
+        cursor: 'pointer',
+      }}
+    >
+      <div
+        style={{
+          position: 'relative',
+          width: 15,
+          height: 15,
+          border: 'none',
+          margin: 0,
+          float: 'left',
+          zIndex: 0,
+          backgroundImage:
+            // tslint:disable-next-line:max-line-length
+            'url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)',
+        }}
+      >
+        <div
+          style={{
+            backgroundColor: props.color,
+            display: 'block',
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            bottom: 0,
+            right: 0,
+          }}
+        />
+      </div>
+    </div>
+  );
+});

+ 0 - 53
packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss

@@ -161,59 +161,6 @@ $arrowSize: 15px;
   flex-grow: 1;
   flex-grow: 1;
 }
 }
 
 
-.sp-replacer {
-  background: inherit;
-  border: none;
-  color: inherit;
-  padding: 0;
-  border-radius: 10px;
-  cursor: pointer;
-}
-
-.sp-replacer:hover,
-.sp-replacer.sp-active {
-  border-color: inherit;
-  color: inherit;
-}
-
-.sp-container {
-  border-radius: 0;
-  background-color: $dropdownBackground;
-  border: none;
-  padding: 0;
-}
-
-.sp-palette-container,
-.sp-picker-container {
-  border: none;
-}
-
-.sp-dd {
-  display: none;
-}
-
-.sp-preview {
-  position: relative;
-  width: 15px;
-  height: 15px;
-  border: none;
-  margin: 0;
-  float: left;
-  z-index: 0;
-  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
-}
-
-.sp-preview-inner,
-.sp-alpha-inner,
-.sp-thumb-inner {
-  display: block;
-  position: absolute;
-  top: 0;
-  left: 0;
-  bottom: 0;
-  right: 0;
-}
-
 .gf-color-picker__body {
 .gf-color-picker__body {
   padding-bottom: $arrowSize;
   padding-bottom: $arrowSize;
   padding-left: 6px;
   padding-left: 6px;

+ 18 - 22
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -1,19 +1,19 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 import $ from 'jquery';
-import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
-import { Themeable, GrafanaThemeType } from '../../types/theme';
-import { Threshold, BasicGaugeColor } from '../../types/panel';
-import { DisplayValue } from '../../utils';
+
+import { Threshold, GrafanaThemeType } from '../../types';
+import { getColorFromHexRgbOrName } from '../../utils';
+import { Themeable } from '../../index';
+import { DisplayValue } from '../../utils/displayValue';
 
 
 export interface Props extends Themeable {
 export interface Props extends Themeable {
-  width: number;
   height: number;
   height: number;
   maxValue: number;
   maxValue: number;
   minValue: number;
   minValue: number;
   thresholds: Threshold[];
   thresholds: Threshold[];
   showThresholdMarkers: boolean;
   showThresholdMarkers: boolean;
   showThresholdLabels: boolean;
   showThresholdLabels: boolean;
-
+  width: number;
   value: DisplayValue;
   value: DisplayValue;
 }
 }
 
 
@@ -72,7 +72,7 @@ export class Gauge extends PureComponent<Props> {
     const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
     const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
     const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
     const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
     const thresholdMarkersWidth = gaugeWidth / 5;
     const thresholdMarkersWidth = gaugeWidth / 5;
-    const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length);
+    const fontSize = Math.min(dimension / 5, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
     const thresholdLabelFontSize = fontSize / 2.5;
     const thresholdLabelFontSize = fontSize / 2.5;
 
 
     const options: any = {
     const options: any = {
@@ -101,7 +101,7 @@ export class Gauge extends PureComponent<Props> {
             width: thresholdMarkersWidth,
             width: thresholdMarkersWidth,
           },
           },
           value: {
           value: {
-            color: value.color ? value.color : BasicGaugeColor.Red,
+            color: value.color,
             formatter: () => {
             formatter: () => {
               return value.text;
               return value.text;
             },
             },
@@ -112,7 +112,7 @@ export class Gauge extends PureComponent<Props> {
       },
       },
     };
     };
 
 
-    const plotSeries = { data: [[0, value.numeric]] }; // May be NaN
+    const plotSeries = { data: [[0, value]] };
 
 
     try {
     try {
       $.plot(this.canvasElement, [plotSeries], options);
       $.plot(this.canvasElement, [plotSeries], options);
@@ -125,19 +125,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;

+ 99 - 0
packages/grafana-ui/src/components/Table/Table.story.tsx

@@ -0,0 +1,99 @@
+// import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { Table } from './Table';
+import { getTheme } from '../../themes';
+
+import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
+import { ScopedVars, TableData, GrafanaThemeType } from '../../types/index';
+import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
+import { number, boolean } from '@storybook/addon-knobs';
+
+const replaceVariables = (value: string, scopedVars?: ScopedVars) => {
+  if (scopedVars) {
+    // For testing variables replacement in link
+    for (const key in scopedVars) {
+      const val = scopedVars[key];
+      value = value.replace('$' + key, val.value);
+    }
+  }
+  return value;
+};
+
+export function columnIndexToLeter(column: number) {
+  const A = 'A'.charCodeAt(0);
+  const c1 = Math.floor(column / 26);
+  const c2 = column % 26;
+  if (c1 > 0) {
+    return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2);
+  }
+  return String.fromCharCode(A + c2);
+}
+
+export function makeDummyTable(columnCount: number, rowCount: number): TableData {
+  return {
+    columns: Array.from(new Array(columnCount), (x, i) => {
+      return {
+        text: columnIndexToLeter(i),
+      };
+    }),
+    rows: Array.from(new Array(rowCount), (x, rowId) => {
+      const suffix = (rowId + 1).toString();
+      return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
+    }),
+    type: 'table',
+    columnMap: {},
+  };
+}
+
+storiesOf('Alpha/Table', module)
+  .add('Basic Table', () => {
+    // NOTE: This example does not seem to survice rotate &
+    // Changing fixed headers... but the next one does?
+    // perhaps `simpleTable` is static and reused?
+
+    const showHeader = boolean('Show Header', true);
+    const fixedHeader = boolean('Fixed Header', true);
+    const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false });
+    const rotate = boolean('Rotate', false);
+
+    return withFullSizeStory(Table, {
+      styles: [],
+      data: simpleTable,
+      replaceVariables,
+      showHeader,
+      fixedHeader,
+      fixedColumns,
+      rotate,
+      theme: getTheme(GrafanaThemeType.Light),
+    });
+  })
+  .add('Variable Size', () => {
+    const columnCount = number('Column Count', 15, { min: 2, max: 50, step: 1, range: false });
+    const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false });
+
+    const showHeader = boolean('Show Header', true);
+    const fixedHeader = boolean('Fixed Header', true);
+    const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false });
+    const rotate = boolean('Rotate', false);
+
+    return withFullSizeStory(Table, {
+      styles: [],
+      data: makeDummyTable(columnCount, rowCount),
+      replaceVariables,
+      showHeader,
+      fixedHeader,
+      fixedColumns,
+      rotate,
+      theme: getTheme(GrafanaThemeType.Light),
+    });
+  })
+  .add('Test Config (migrated)', () => {
+    return withFullSizeStory(Table, {
+      styles: migratedTestStyles,
+      data: migratedTestTable,
+      replaceVariables,
+      showHeader: true,
+      rotate: true,
+      theme: getTheme(GrafanaThemeType.Light),
+    });
+  });

+ 287 - 0
packages/grafana-ui/src/components/Table/Table.tsx

@@ -0,0 +1,287 @@
+// Libraries
+import _ from 'lodash';
+import React, { Component, ReactElement } from 'react';
+import {
+  SortDirectionType,
+  SortIndicator,
+  MultiGrid,
+  CellMeasurerCache,
+  CellMeasurer,
+  GridCellProps,
+} from 'react-virtualized';
+import { Themeable } from '../../types/theme';
+
+import { sortTableData } from '../../utils/processTableData';
+
+import { TableData, InterpolateFunction } from '@grafana/ui';
+import {
+  TableCellBuilder,
+  ColumnStyle,
+  getCellBuilder,
+  TableCellBuilderOptions,
+  simpleCellBuilder,
+} from './TableCellBuilder';
+import { stringToJsRegex } from '../../utils/index';
+
+export interface Props extends Themeable {
+  data: TableData;
+
+  showHeader: boolean;
+  fixedHeader: boolean;
+  fixedColumns: number;
+  rotate: boolean;
+  styles: ColumnStyle[];
+
+  replaceVariables: InterpolateFunction;
+  width: number;
+  height: number;
+  isUTC?: boolean;
+}
+
+interface State {
+  sortBy?: number;
+  sortDirection?: SortDirectionType;
+  data: TableData;
+}
+
+interface ColumnRenderInfo {
+  header: string;
+  builder: TableCellBuilder;
+}
+
+interface DataIndex {
+  column: number;
+  row: number; // -1 is the header!
+}
+
+export class Table extends Component<Props, State> {
+  renderer: ColumnRenderInfo[];
+  measurer: CellMeasurerCache;
+  scrollToTop = false;
+
+  static defaultProps = {
+    showHeader: true,
+    fixedHeader: true,
+    fixedColumns: 0,
+    rotate: false,
+  };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      data: props.data,
+    };
+
+    this.renderer = this.initColumns(props);
+    this.measurer = new CellMeasurerCache({
+      defaultHeight: 30,
+      defaultWidth: 150,
+    });
+  }
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    const { data, styles, showHeader } = this.props;
+    const { sortBy, sortDirection } = this.state;
+    const dataChanged = data !== prevProps.data;
+    const configsChanged =
+      showHeader !== prevProps.showHeader ||
+      this.props.rotate !== prevProps.rotate ||
+      this.props.fixedColumns !== prevProps.fixedColumns ||
+      this.props.fixedHeader !== prevProps.fixedHeader;
+
+    // Reset the size cache
+    if (dataChanged || configsChanged) {
+      this.measurer.clearAll();
+    }
+
+    // Update the renderer if options change
+    // We only *need* do to this if the header values changes, but this does every data update
+    if (dataChanged || styles !== prevProps.styles) {
+      this.renderer = this.initColumns(this.props);
+    }
+
+    // Update the data when data or sort changes
+    if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
+      this.scrollToTop = true;
+      this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') });
+    }
+  }
+
+  /** Given the configuration, setup how each column gets rendered */
+  initColumns(props: Props): ColumnRenderInfo[] {
+    const { styles, data } = props;
+
+    return data.columns.map((col, index) => {
+      let title = col.text;
+      let style: ColumnStyle | null = null; // ColumnStyle
+
+      // Find the style based on the text
+      for (let i = 0; i < styles.length; i++) {
+        const s = styles[i];
+        const regex = stringToJsRegex(s.pattern);
+        if (title.match(regex)) {
+          style = s;
+          if (s.alias) {
+            title = title.replace(regex, s.alias);
+          }
+          break;
+        }
+      }
+
+      return {
+        header: title,
+        builder: getCellBuilder(col, style, this.props),
+      };
+    });
+  }
+
+  //----------------------------------------------------------------------
+  //----------------------------------------------------------------------
+
+  doSort = (columnIndex: number) => {
+    let sort: any = this.state.sortBy;
+    let dir = this.state.sortDirection;
+    if (sort !== columnIndex) {
+      dir = 'DESC';
+      sort = columnIndex;
+    } else if (dir === 'DESC') {
+      dir = 'ASC';
+    } else {
+      sort = null;
+    }
+    this.setState({ sortBy: sort, sortDirection: dir });
+  };
+
+  /** Converts the grid coordinates to TableData coordinates */
+  getCellRef = (rowIndex: number, columnIndex: number): DataIndex => {
+    const { showHeader, rotate } = this.props;
+    const rowOffset = showHeader ? -1 : 0;
+
+    if (rotate) {
+      return { column: rowIndex, row: columnIndex + rowOffset };
+    } else {
+      return { column: columnIndex, row: rowIndex + rowOffset };
+    }
+  };
+
+  onCellClick = (rowIndex: number, columnIndex: number) => {
+    const { row, column } = this.getCellRef(rowIndex, columnIndex);
+    if (row < 0) {
+      this.doSort(column);
+    } else {
+      const values = this.state.data.rows[row];
+      const value = values[column];
+      console.log('CLICK', value, row);
+    }
+  };
+
+  headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
+    const { data, sortBy, sortDirection } = this.state;
+    const { columnIndex, rowIndex, style } = cell.props;
+    const { column } = this.getCellRef(rowIndex, columnIndex);
+
+    let col = data.columns[column];
+    const sorting = sortBy === column;
+    if (!col) {
+      col = {
+        text: '??' + columnIndex + '???',
+      };
+    }
+
+    return (
+      <div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
+        {col.text}
+        {sorting && <SortIndicator sortDirection={sortDirection} />}
+      </div>
+    );
+  };
+
+  getTableCellBuilder = (column: number): TableCellBuilder => {
+    const render = this.renderer[column];
+    if (render && render.builder) {
+      return render.builder;
+    }
+    return simpleCellBuilder; // the default
+  };
+
+  cellRenderer = (props: GridCellProps): React.ReactNode => {
+    const { rowIndex, columnIndex, key, parent } = props;
+    const { row, column } = this.getCellRef(rowIndex, columnIndex);
+    const { data } = this.state;
+
+    const isHeader = row < 0;
+    const rowData = isHeader ? data.columns : data.rows[row];
+    const value = rowData ? rowData[column] : '';
+    const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
+
+    return (
+      <CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
+        {builder({
+          value,
+          row: rowData,
+          column: data.columns[column],
+          table: this,
+          props,
+        })}
+      </CellMeasurer>
+    );
+  };
+
+  render() {
+    const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
+    const { data } = this.state;
+
+    let columnCount = data.columns.length;
+    let rowCount = data.rows.length + (showHeader ? 1 : 0);
+
+    let fixedColumnCount = Math.min(fixedColumns, columnCount);
+    let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
+
+    if (rotate) {
+      const temp = columnCount;
+      columnCount = rowCount;
+      rowCount = temp;
+
+      fixedRowCount = 0;
+      fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0);
+    }
+
+    // Called after sort or the data changes
+    const scroll = this.scrollToTop ? 1 : -1;
+    const scrollToRow = rotate ? -1 : scroll;
+    const scrollToColumn = rotate ? scroll : -1;
+    if (this.scrollToTop) {
+      this.scrollToTop = false;
+    }
+
+    return (
+      <MultiGrid
+        {
+          ...this.state /** Force MultiGrid to update when data changes */
+        }
+        {
+          ...this.props /** Force MultiGrid to update when data changes */
+        }
+        scrollToRow={scrollToRow}
+        columnCount={columnCount}
+        scrollToColumn={scrollToColumn}
+        rowCount={rowCount}
+        overscanColumnCount={8}
+        overscanRowCount={8}
+        columnWidth={this.measurer.columnWidth}
+        deferredMeasurementCache={this.measurer}
+        cellRenderer={this.cellRenderer}
+        rowHeight={this.measurer.rowHeight}
+        width={width}
+        height={height}
+        fixedColumnCount={fixedColumnCount}
+        fixedRowCount={fixedRowCount}
+        classNameTopLeftGrid="gf-table-fixed-column"
+        classNameBottomLeftGrid="gf-table-fixed-column"
+      />
+    );
+  }
+}
+
+export default Table;

+ 291 - 0
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx

@@ -0,0 +1,291 @@
+// Libraries
+import _ from 'lodash';
+import React, { ReactElement } from 'react';
+import { GridCellProps } from 'react-virtualized';
+import { Table, Props } from './Table';
+import moment from 'moment';
+import { ValueFormatter } from '../../utils/index';
+import { GrafanaTheme } from '../../types/theme';
+import { getValueFormat, getColorFromHexRgbOrName, Column } from '@grafana/ui';
+import { InterpolateFunction } from '../../types/panel';
+
+export interface TableCellBuilderOptions {
+  value: any;
+  column?: Column;
+  row?: any[];
+  table?: Table;
+  className?: string;
+  props: GridCellProps;
+}
+
+export type TableCellBuilder = (cell: TableCellBuilderOptions) => ReactElement<'div'>;
+
+/** Simplest cell that just spits out the value */
+export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => {
+  const { props, value, className } = cell;
+  const { style } = props;
+
+  return (
+    <div style={style} className={'gf-table-cell ' + className}>
+      {value}
+    </div>
+  );
+};
+
+// ***************************************************************************
+// HERE BE DRAGONS!!!
+// ***************************************************************************
+//
+//  The following code has been migrated blindy two times from the angular
+//  table panel.  I don't understand all the options nor do I know if they
+//  are correct!
+//
+// ***************************************************************************
+
+// Made to match the existing (untyped) settings in the angular table
+export interface ColumnStyle {
+  pattern: string;
+
+  alias?: string;
+  colorMode?: 'cell' | 'value';
+  colors?: any[];
+  decimals?: number;
+  thresholds?: any[];
+  type?: 'date' | 'number' | 'string' | 'hidden';
+  unit?: string;
+  dateFormat?: string;
+  sanitize?: boolean; // not used in react
+  mappingType?: any;
+  valueMaps?: any;
+  rangeMaps?: any;
+
+  link?: any;
+  linkUrl?: any;
+  linkTooltip?: any;
+  linkTargetBlank?: boolean;
+
+  preserveFormat?: boolean;
+}
+
+// private mapper:ValueMapper,
+// private style:ColumnStyle,
+// private theme:GrafanaTheme,
+// private column:Column,
+// private replaceVariables: InterpolateFunction,
+// private fmt?:ValueFormatter) {
+
+export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: Props): TableCellBuilder {
+  if (!style) {
+    return simpleCellBuilder;
+  }
+
+  if (style.type === 'hidden') {
+    // TODO -- for hidden, we either need to:
+    // 1. process the Table and remove hidden fields
+    // 2. do special math to pick the right column skipping hidden fields
+    throw new Error('hidden not supported!');
+  }
+
+  if (style.type === 'date') {
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (v === undefined || v === null) {
+          return '-';
+        }
+
+        if (_.isArray(v)) {
+          v = v[0];
+        }
+        let date = moment(v);
+        if (false) {
+          // TODO?????? this.props.isUTC) {
+          date = date.utc();
+        }
+        return date.format(style.dateFormat);
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables
+    ).build;
+  }
+
+  if (style.type === 'string') {
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (_.isArray(v)) {
+          v = v.join(', ');
+        }
+        return v;
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables
+    ).build;
+    // TODO!!!!  all the mapping stuff!!!!
+  }
+
+  if (style.type === 'number') {
+    const valueFormatter = getValueFormat(style.unit || schema.unit || 'none');
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (v === null || v === void 0) {
+          return '-';
+        }
+        return v;
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables,
+      valueFormatter
+    ).build;
+  }
+
+  return simpleCellBuilder;
+}
+
+type ValueMapper = (value: any) => any;
+
+// Runs the value through a formatter and adds colors to the cell properties
+class CellBuilderWithStyle {
+  constructor(
+    private mapper: ValueMapper,
+    private style: ColumnStyle,
+    private theme: GrafanaTheme,
+    private column: Column,
+    private replaceVariables: InterpolateFunction,
+    private fmt?: ValueFormatter
+  ) {
+    //
+    console.log('COLUMN', column.text, theme);
+  }
+
+  getColorForValue = (value: any): string | null => {
+    const { thresholds, colors } = this.style;
+    if (!thresholds || !colors) {
+      return null;
+    }
+
+    for (let i = thresholds.length; i > 0; i--) {
+      if (value >= thresholds[i - 1]) {
+        return getColorFromHexRgbOrName(colors[i], this.theme.type);
+      }
+    }
+    return getColorFromHexRgbOrName(_.first(colors), this.theme.type);
+  };
+
+  build = (cell: TableCellBuilderOptions) => {
+    let { props } = cell;
+    let value = this.mapper(cell.value);
+
+    if (_.isNumber(value)) {
+      if (this.fmt) {
+        value = this.fmt(value, this.style.decimals);
+      }
+
+      // For numeric values set the color
+      const { colorMode } = this.style;
+      if (colorMode) {
+        const color = this.getColorForValue(Number(value));
+        if (color) {
+          if (colorMode === 'cell') {
+            props = {
+              ...props,
+              style: {
+                ...props.style,
+                backgroundColor: color,
+                color: 'white',
+              },
+            };
+          } else if (colorMode === 'value') {
+            props = {
+              ...props,
+              style: {
+                ...props.style,
+                color: color,
+              },
+            };
+          }
+        }
+      }
+    }
+
+    const cellClasses = [];
+    if (this.style.preserveFormat) {
+      cellClasses.push('table-panel-cell-pre');
+    }
+
+    if (this.style.link) {
+      // Render cell as link
+      const { row } = cell;
+
+      const scopedVars: any = {};
+      if (row) {
+        for (let i = 0; i < row.length; i++) {
+          scopedVars[`__cell_${i}`] = { value: row[i] };
+        }
+      }
+      scopedVars['__cell'] = { value: value };
+
+      const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent);
+      const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars);
+      const cellTarget = this.style.linkTargetBlank ? '_blank' : '';
+
+      cellClasses.push('table-panel-cell-link');
+      value = (
+        <a
+          href={cellLink}
+          target={cellTarget}
+          data-link-tooltip
+          data-original-title={cellLinkTooltip}
+          data-placement="right"
+        >
+          {value}
+        </a>
+      );
+    }
+
+    // ??? I don't think this will still work!
+    if (this.column.filterable) {
+      cellClasses.push('table-panel-cell-filterable');
+      value = (
+        <>
+          {value}
+          <span>
+            <a
+              className="table-panel-filter-link"
+              data-link-tooltip
+              data-original-title="Filter out value"
+              data-placement="bottom"
+              data-row={props.rowIndex}
+              data-column={props.columnIndex}
+              data-operator="!="
+            >
+              <i className="fa fa-search-minus" />
+            </a>
+            <a
+              className="table-panel-filter-link"
+              data-link-tooltip
+              data-original-title="Filter for value"
+              data-placement="bottom"
+              data-row={props.rowIndex}
+              data-column={props.columnIndex}
+              data-operator="="
+            >
+              <i className="fa fa-search-plus" />
+            </a>
+          </span>
+        </>
+      );
+    }
+
+    let className;
+    if (cellClasses.length) {
+      className = cellClasses.join(' ');
+    }
+
+    return simpleCellBuilder({ value, props, className });
+  };
+}

+ 80 - 0
packages/grafana-ui/src/components/Table/_Table.scss

@@ -0,0 +1,80 @@
+// .ReactVirtualized__Table {
+// }
+
+// .ReactVirtualized__Table__Grid {
+// }
+
+.ReactVirtualized__Table__headerRow {
+  font-weight: 700;
+  display: flex;
+  flex-direction: row;
+  align-items: left;
+}
+.ReactVirtualized__Table__row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  border-bottom: 2px solid $body-bg;
+}
+
+.ReactVirtualized__Table__headerTruncatedText {
+  display: inline-block;
+  max-width: 100%;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
+.ReactVirtualized__Table__headerColumn,
+.ReactVirtualized__Table__rowColumn {
+  margin-right: 10px;
+  min-width: 0px;
+}
+
+.ReactVirtualized__Table__headerColumn:first-of-type,
+.ReactVirtualized__Table__rowColumn:first-of-type {
+  margin-left: 10px;
+}
+.ReactVirtualized__Table__sortableHeaderColumn {
+  cursor: pointer;
+}
+
+.ReactVirtualized__Table__sortableHeaderIconContainer {
+  align-items: center;
+}
+.ReactVirtualized__Table__sortableHeaderIcon {
+  flex: 0 0 24px;
+  height: 1em;
+  width: 1em;
+  fill: currentColor;
+}
+
+.gf-table-header {
+  padding: 3px 10px;
+
+  background: $list-item-bg;
+  border-top: 2px solid $body-bg;
+  border-bottom: 2px solid $body-bg;
+
+  cursor: pointer;
+  white-space: nowrap;
+
+  color: $blue;
+}
+
+.gf-table-cell {
+  padding: 3px 10px;
+
+  background: $page-gradient;
+
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  border-right: 2px solid $body-bg;
+  border-bottom: 2px solid $body-bg;
+}
+
+.gf-table-fixed-column {
+  border-right: 1px solid #ccc;
+}

+ 167 - 0
packages/grafana-ui/src/components/Table/examples.ts

@@ -0,0 +1,167 @@
+import { TableData } from '../../types/data';
+import { ColumnStyle } from './TableCellBuilder';
+
+import { getColorDefinitionByName } from '@grafana/ui';
+
+const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
+
+export const migratedTestTable = {
+  type: 'table',
+  columns: [
+    { text: 'Time' },
+    { text: 'Value' },
+    { text: 'Colored' },
+    { text: 'Undefined' },
+    { text: 'String' },
+    { text: 'United', unit: 'bps' },
+    { text: 'Sanitized' },
+    { text: 'Link' },
+    { text: 'Array' },
+    { text: 'Mapping' },
+    { text: 'RangeMapping' },
+    { text: 'MappingColored' },
+    { text: 'RangeMappingColored' },
+  ],
+  rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
+} as TableData;
+
+export const migratedTestStyles: ColumnStyle[] = [
+  {
+    pattern: 'Time',
+    type: 'date',
+    alias: 'Timestamp',
+  },
+  {
+    pattern: '/(Val)ue/',
+    type: 'number',
+    unit: 'ms',
+    decimals: 3,
+    alias: '$1',
+  },
+  {
+    pattern: 'Colored',
+    type: 'number',
+    unit: 'none',
+    decimals: 1,
+    colorMode: 'value',
+    thresholds: [50, 80],
+    colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
+  },
+  {
+    pattern: 'String',
+    type: 'string',
+  },
+  {
+    pattern: 'String',
+    type: 'string',
+  },
+  {
+    pattern: 'United',
+    type: 'number',
+    unit: 'ms',
+    decimals: 2,
+  },
+  {
+    pattern: 'Sanitized',
+    type: 'string',
+    sanitize: true,
+  },
+  {
+    pattern: 'Link',
+    type: 'string',
+    link: true,
+    linkUrl: '/dashboard?param=$__cell&param_1=$__cell_1&param_2=$__cell_2',
+    linkTooltip: '$__cell $__cell_1 $__cell_6',
+    linkTargetBlank: true,
+  },
+  {
+    pattern: 'Array',
+    type: 'number',
+    unit: 'ms',
+    decimals: 3,
+  },
+  {
+    pattern: 'Mapping',
+    type: 'string',
+    mappingType: 1,
+    valueMaps: [
+      {
+        value: '1',
+        text: 'on',
+      },
+      {
+        value: '0',
+        text: 'off',
+      },
+      {
+        value: 'HELLO WORLD',
+        text: 'HELLO GRAFANA',
+      },
+      {
+        value: 'value1, value2',
+        text: 'value3, value4',
+      },
+    ],
+  },
+  {
+    pattern: 'RangeMapping',
+    type: 'string',
+    mappingType: 2,
+    rangeMaps: [
+      {
+        from: '1',
+        to: '3',
+        text: 'on',
+      },
+      {
+        from: '3',
+        to: '6',
+        text: 'off',
+      },
+    ],
+  },
+  {
+    pattern: 'MappingColored',
+    type: 'string',
+    mappingType: 1,
+    valueMaps: [
+      {
+        value: '1',
+        text: 'on',
+      },
+      {
+        value: '0',
+        text: 'off',
+      },
+    ],
+    colorMode: 'value',
+    thresholds: [1, 2],
+    colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
+  },
+  {
+    pattern: 'RangeMappingColored',
+    type: 'string',
+    mappingType: 2,
+    rangeMaps: [
+      {
+        from: '1',
+        to: '3',
+        text: 'on',
+      },
+      {
+        from: '3',
+        to: '6',
+        text: 'off',
+      },
+    ],
+    colorMode: 'value',
+    thresholds: [2, 5],
+    colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
+  },
+];
+
+export const simpleTable = {
+  type: 'table',
+  columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }],
+  rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]],
+};

+ 2 - 3
packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss

@@ -68,7 +68,7 @@
 }
 }
 
 
 .thresholds-row-input-inner-value > input {
 .thresholds-row-input-inner-value > input {
-  height: $gf-form-input-height;
+  height: $input-height;
   padding: $input-padding-y $input-padding-x;
   padding: $input-padding-y $input-padding-x;
   width: 150px;
   width: 150px;
   border-top: 1px solid $input-label-border-color;
   border-top: 1px solid $input-label-border-color;
@@ -86,7 +86,6 @@
 
 
 .thresholds-row-input-inner-color-colorpicker {
 .thresholds-row-input-inner-color-colorpicker {
   border-radius: 10px;
   border-radius: 10px;
-  overflow: hidden;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
   box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
@@ -96,7 +95,7 @@
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  height: $gf-form-input-height;
+  height: $input-height;
   padding: $input-padding-y $input-padding-x;
   padding: $input-padding-y $input-padding-x;
   width: 42px;
   width: 42px;
   background-color: $input-label-bg;
   background-color: $input-label-bg;

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

@@ -0,0 +1,77 @@
+import React, { PureComponent } from 'react';
+import { VizOrientation } from '../../types';
+
+interface RenderProps<T> {
+  vizWidth: number;
+  vizHeight: number;
+  value: T;
+}
+
+interface Props<T> {
+  children: (renderProps: RenderProps<T>) => JSX.Element | JSX.Element[];
+  height: number;
+  width: number;
+  values: T[];
+  orientation: VizOrientation;
+}
+
+const SPACE_BETWEEN = 10;
+
+export class VizRepeater<T> extends PureComponent<Props<T>> {
+  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((value, index) => {
+          return (
+            <div key={index} style={itemStyles}>
+              {children({ vizHeight, vizWidth, value })}
+            </div>
+          );
+        })}
+      </div>
+    );
+  }
+}

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

@@ -1,6 +1,7 @@
 @import 'CustomScrollbar/CustomScrollbar';
 @import 'CustomScrollbar/CustomScrollbar';
 @import 'DeleteButton/DeleteButton';
 @import 'DeleteButton/DeleteButton';
 @import 'ThresholdsEditor/ThresholdsEditor';
 @import 'ThresholdsEditor/ThresholdsEditor';
+@import 'Table/Table';
 @import 'Table/TableInputCSV';
 @import 'Table/TableInputCSV';
 @import 'Tooltip/Tooltip';
 @import 'Tooltip/Tooltip';
 @import 'Select/Select';
 @import 'Select/Select';
@@ -10,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';

+ 8 - 21
packages/grafana-ui/src/themes/_variables.scss.tmpl.ts

@@ -52,7 +52,6 @@ $spacers: (
     ),
     ),
   ),
   ),
 ) !default;
 ) !default;
-$border-width: ${theme.border.width.sm} !default;
 
 
 // Grid breakpoints
 // Grid breakpoints
 //
 //
@@ -83,16 +82,13 @@ $container-max-widths: (
 // Set the number of columns and specify the width of the gutters.
 // Set the number of columns and specify the width of the gutters.
 
 
 $grid-columns: 12 !default;
 $grid-columns: 12 !default;
-$grid-gutter-width: 30px !default;
-
-$enable-flex: true;
+$grid-gutter-width: ${theme.spacing.gutter} !default;
 
 
 // Typography
 // Typography
 // -------------------------
 // -------------------------
 
 
 $font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
 $font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
 $font-family-monospace: ${theme.typography.fontFamily.monospace};
 $font-family-monospace: ${theme.typography.fontFamily.monospace};
-$font-family-base: $font-family-sans-serif !default;
 
 
 $font-size-root: ${theme.typography.size.root} !default;
 $font-size-root: ${theme.typography.size.root} !default;
 $font-size-base: ${theme.typography.size.base} !default;
 $font-size-base: ${theme.typography.size.base} !default;
@@ -103,7 +99,9 @@ $font-size-sm: ${theme.typography.size.sm} !default;
 $font-size-xs: ${theme.typography.size.xs} !default;
 $font-size-xs: ${theme.typography.size.xs} !default;
 
 
 $line-height-base: ${theme.typography.lineHeight.lg} !default;
 $line-height-base: ${theme.typography.lineHeight.lg} !default;
-$font-weight-semi-bold: ${theme.typography.weight.semibold};
+
+$font-weight-regular: ${theme.typography.weight.regular} !default;
+$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
 
 
 $font-size-h1: ${theme.typography.heading.h1} !default;
 $font-size-h1: ${theme.typography.heading.h1} !default;
 $font-size-h2: ${theme.typography.heading.h2} !default;
 $font-size-h2: ${theme.typography.heading.h2} !default;
@@ -113,22 +111,17 @@ $font-size-h5: ${theme.typography.heading.h5} !default;
 $font-size-h6: ${theme.typography.heading.h6} !default;
 $font-size-h6: ${theme.typography.heading.h6} !default;
 
 
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-$headings-font-weight: ${theme.typography.weight.normal} !default;
 $headings-line-height: ${theme.typography.lineHeight.sm} !default;
 $headings-line-height: ${theme.typography.lineHeight.sm} !default;
 
 
-$hr-border-width: $border-width !default;
-$dt-font-weight: bold !default;
-
 // Components
 // Components
 //
 //
 // Define common padding and border radius sizes and more.
 // Define common padding and border radius sizes and more.
 
 
-$line-height-lg: (4 / 3) !default;
-$line-height-sm: 1.5 !default;
+$border-width: ${theme.border.width.sm} !default;
 
 
-$border-radius: 3px !default;
-$border-radius-lg: 5px !default;
-$border-radius-sm: 2px !default;
+$border-radius: ${theme.border.radius.md} !default;
+$border-radius-lg: ${theme.border.radius.lg}!default;
+$border-radius-sm: ${theme.border.radius.sm} !default;
 
 
 // Page
 // Page
 
 
@@ -151,22 +144,17 @@ $input-padding-x: 10px !default;
 $input-padding-y: 8px !default;
 $input-padding-y: 8px !default;
 $input-line-height: 18px !default;
 $input-line-height: 18px !default;
 
 
-$input-btn-border-width: 1px;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 
 
 $label-border-radius: $border-radius 0 0 $border-radius !default;
 $label-border-radius: $border-radius 0 0 $border-radius !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 
 
-$input-padding-y-sm: 4px !default;
-
 $input-padding-x-lg: 20px !default;
 $input-padding-x-lg: 20px !default;
 $input-padding-y-lg: 10px !default;
 $input-padding-y-lg: 10px !default;
 
 
 $input-height: 35px !default;
 $input-height: 35px !default;
 
 
-$gf-form-input-height: 35px;
-
 $cursor-disabled: not-allowed !default;
 $cursor-disabled: not-allowed !default;
 
 
 // Form validation icons
 // Form validation icons
@@ -203,7 +191,6 @@ $btn-padding-y-lg: 11px !default;
 $btn-padding-x-xl: 21px !default;
 $btn-padding-x-xl: 21px !default;
 $btn-padding-y-xl: 11px !default;
 $btn-padding-y-xl: 11px !default;
 
 
-$btn-border-radius: 2px;
 
 
 $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 
 

+ 4 - 4
packages/grafana-ui/src/themes/default.ts

@@ -25,7 +25,7 @@ const theme: GrafanaThemeCommons = {
     },
     },
     weight: {
     weight: {
       light: 300,
       light: 300,
-      normal: 400,
+      regular: 400,
       semibold: 500,
       semibold: 500,
     },
     },
     lineHeight: {
     lineHeight: {
@@ -54,9 +54,9 @@ const theme: GrafanaThemeCommons = {
   },
   },
   border: {
   border: {
     radius: {
     radius: {
-      xs: '2px',
-      sm: '3px',
-      md: '5px',
+      sm: '2px',
+      md: '3px',
+      lg: '5px',
     },
     },
     width: {
     width: {
       sm: '1px',
       sm: '1px',

+ 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;
+}

+ 3 - 1
packages/grafana-ui/src/types/datasource.ts

@@ -3,9 +3,11 @@ import { PluginMeta } from './plugin';
 import { TableData, TimeSeries } from './data';
 import { TableData, TimeSeries } from './data';
 
 
 export interface DataQueryResponse {
 export interface DataQueryResponse {
-  data: TimeSeries[] | [TableData] | any;
+  data: DataQueryResponseData;
 }
 }
 
 
+export type DataQueryResponseData = TimeSeries[] | [TableData] | any;
+
 export interface DataQuery {
 export interface DataQuery {
   /**
   /**
    * A - Z
    * A - Z

+ 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',
+}

+ 2 - 2
packages/grafana-ui/src/types/theme.ts

@@ -28,7 +28,7 @@ export interface GrafanaThemeCommons {
     };
     };
     weight: {
     weight: {
       light: number;
       light: number;
-      normal: number;
+      regular: number;
       semibold: number;
       semibold: number;
     };
     };
     lineHeight: {
     lineHeight: {
@@ -59,9 +59,9 @@ export interface GrafanaThemeCommons {
   };
   };
   border: {
   border: {
     radius: {
     radius: {
-      xs: string;
       sm: string;
       sm: string;
       md: string;
       md: string;
+      lg: string;
     };
     };
     width: {
     width: {
       sm: string;
       sm: string;

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

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

+ 2 - 2
packages/grafana-ui/src/utils/displayValue.ts

@@ -1,8 +1,8 @@
-import { ValueMapping, Threshold } from '../types/panel';
+import { ValueMapping, Threshold } from '../types';
 import _ from 'lodash';
 import _ from 'lodash';
 import { getValueFormat, DecimalCount } from './valueFormats/valueFormats';
 import { getValueFormat, DecimalCount } from './valueFormats/valueFormats';
 import { getMappedValue } from './valueMappings';
 import { getMappedValue } from './valueMappings';
-import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
+import { GrafanaTheme, GrafanaThemeType } from '../types';
 import { getColorFromHexRgbOrName } from './namedColorsPalette';
 import { getColorFromHexRgbOrName } from './namedColorsPalette';
 import moment from 'moment';
 import moment from 'moment';
 
 

+ 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 './displayValue';
 export * from './displayValue';
 export * from './deprecationWarning';
 export * from './deprecationWarning';

+ 26 - 2
packages/grafana-ui/src/utils/processTableData.ts

@@ -1,7 +1,10 @@
-import { TableData, Column } from '../types/index';
-
+// Libraries
+import isNumber from 'lodash/isNumber';
 import Papa, { ParseError, ParseMeta } from 'papaparse';
 import Papa, { ParseError, ParseMeta } from 'papaparse';
 
 
+// Types
+import { TableData, Column } from '../types';
+
 // Subset of all parse options
 // Subset of all parse options
 export interface TableParseOptions {
 export interface TableParseOptions {
   headerIsFirstLine?: boolean; // Not a papa-parse option
   headerIsFirstLine?: boolean; // Not a papa-parse option
@@ -131,3 +134,24 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
     columnMap: {},
     columnMap: {},
   });
   });
 }
 }
+
+export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
+  if (isNumber(sortIndex)) {
+    const copy = {
+      ...data,
+      rows: [...data.rows].sort((a, b) => {
+        a = a[sortIndex];
+        b = b[sortIndex];
+        // Sort null or undefined separately from comparable values
+        return +(a == null) - +(b == null) || +(a > b) || -(a < b);
+      }),
+    };
+
+    if (reverse) {
+      copy.rows.reverse();
+    }
+
+    return copy;
+  }
+  return data;
+}

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

@@ -0,0 +1,39 @@
+import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
+import { processTimeSeries } from './processTimeSeries';
+import { DisplayValueOptions } from './displayValue';
+
+export interface SingleStatProcessingOptions {
+  panelData: PanelData;
+  stat: string;
+}
+
+export interface SingleStatOptions {
+  stat: string;
+  display: DisplayValueOptions;
+}
+
+//
+// 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 [];
+}

+ 24 - 0
packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { AutoSizer } from 'react-virtualized';
+
+/** This will add full size with & height properties */
+export const withFullSizeStory = (component: React.ComponentType<any>, props: any) => (
+  <div
+    style={{
+      height: '100vh',
+      width: '100%',
+    }}
+  >
+    <AutoSizer>
+      {({ width, height }) => (
+        <>
+          {React.createElement(component, {
+            ...props,
+            width,
+            height,
+          })}
+        </>
+      )}
+    </AutoSizer>
+  </div>
+);

+ 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 - 1
pkg/tsdb/graphite/graphite.go

@@ -49,7 +49,7 @@ func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource,
 	}
 	}
 
 
 	for _, query := range tsdbQuery.Queries {
 	for _, query := range tsdbQuery.Queries {
-		glog.Info("graphite", "query", query.Model)
+		glog.Debug("graphite", "query", query.Model)
 		if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
 		if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
 			target = fixIntervalFormat(fullTarget)
 			target = fixIntervalFormat(fullTarget)
 		} else {
 		} else {

+ 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 }) => {

+ 18 - 4
public/app/core/utils/explore.ts

@@ -20,6 +20,7 @@ import {
   ResultType,
   ResultType,
   QueryIntervals,
   QueryIntervals,
   QueryOptions,
   QueryOptions,
+  ResultGetter,
 } from 'app/types/explore';
 } from 'app/types/explore';
 import { LogsDedupStrategy } from 'app/core/logs_model';
 import { LogsDedupStrategy } from 'app/core/logs_model';
 
 
@@ -301,11 +302,24 @@ export function getIntervals(range: RawTimeRange, lowLimit: string, resolution:
   return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
   return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
 }
 }
 
 
-export function makeTimeSeriesList(dataList) {
-  return dataList.map((seriesData, index) => {
+export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => {
+  // Prevent multiple Graph transactions to have the same colors
+  let colorIndexOffset = 0;
+  for (const other of allTransactions) {
+    // Only need to consider transactions that came before the current one
+    if (other === transaction) {
+      break;
+    }
+    // Count timeseries of previous query results
+    if (other.resultType === 'Graph' && other.done) {
+      colorIndexOffset += other.result.length;
+    }
+  }
+
+  return dataList.map((seriesData, index: number) => {
     const datapoints = seriesData.datapoints || [];
     const datapoints = seriesData.datapoints || [];
     const alias = seriesData.target;
     const alias = seriesData.target;
-    const colorIndex = index % colors.length;
+    const colorIndex = (colorIndexOffset + index) % colors.length;
     const color = colors[colorIndex];
     const color = colors[colorIndex];
 
 
     const series = new TimeSeries({
     const series = new TimeSeries({
@@ -317,7 +331,7 @@ export function makeTimeSeriesList(dataList) {
 
 
     return series;
     return series;
   });
   });
-}
+};
 
 
 /**
 /**
  * Update the query history. Side-effect: store history in local storage
  * Update the query history. Side-effect: store history in local storage

+ 5 - 3
public/app/features/dashboard/components/DashboardSettings/template.html

@@ -16,9 +16,6 @@
 		<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
 		<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
 			Save As...
 			Save As...
 		</button>
 		</button>
-		<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
-			Delete
-		</button>
 	</div>
 	</div>
 </aside>
 </aside>
 
 
@@ -70,6 +67,11 @@
 			<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
 			<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
 		</div>
 		</div>
 	</div>
 	</div>
+	<div class="gf-form-button-row">
+	  <button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
+		  Delete Dashboard
+	  </button>
+  </div>
 </div>
 </div>
 
 
 <div class="dashboard-settings__content" ng-if="ctrl.viewId === 'annotations'" ng-include="'public/app/features/annotations/partials/editor.html'">
 <div class="dashboard-settings__content" ng-if="ctrl.viewId === 'annotations'" ng-include="'public/app/features/annotations/partials/editor.html'">

+ 17 - 4
public/app/features/dashboard/containers/DashboardPage.tsx

@@ -92,11 +92,12 @@ export class DashboardPage extends PureComponent<Props, State> {
   componentWillUnmount() {
   componentWillUnmount() {
     if (this.props.dashboard) {
     if (this.props.dashboard) {
       this.props.cleanUpDashboard();
       this.props.cleanUpDashboard();
+      this.setPanelFullscreenClass(false);
     }
     }
   }
   }
 
 
   componentDidUpdate(prevProps: Props) {
   componentDidUpdate(prevProps: Props) {
-    const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props;
+    const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId, urlUid } = this.props;
 
 
     if (!dashboard) {
     if (!dashboard) {
       return;
       return;
@@ -107,6 +108,12 @@ export class DashboardPage extends PureComponent<Props, State> {
       document.title = dashboard.title + ' - Grafana';
       document.title = dashboard.title + ' - Grafana';
     }
     }
 
 
+    // Due to the angular -> react url bridge we can ge an update here with new uid before the container unmounts
+    // Can remove this condition after we switch to react router
+    if (prevProps.urlUid !== urlUid) {
+      return;
+    }
+
     // handle animation states when opening dashboard settings
     // handle animation states when opening dashboard settings
     if (!prevProps.editview && editview) {
     if (!prevProps.editview && editview) {
       this.setState({ isSettingsOpening: true });
       this.setState({ isSettingsOpening: true });
@@ -163,14 +170,20 @@ export class DashboardPage extends PureComponent<Props, State> {
         fullscreenPanel: null,
         fullscreenPanel: null,
         scrollTop: this.state.rememberScrollTop,
         scrollTop: this.state.rememberScrollTop,
       },
       },
-      () => {
-        dashboard.render();
-      }
+      this.triggerPanelsRendering.bind(this)
     );
     );
 
 
     this.setPanelFullscreenClass(false);
     this.setPanelFullscreenClass(false);
   }
   }
 
 
+  triggerPanelsRendering() {
+    try {
+      this.props.dashboard.render();
+    } catch (err) {
+      this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
+    }
+  }
+
   handleFullscreenPanelNotFound(urlPanelId: string) {
   handleFullscreenPanelNotFound(urlPanelId: string) {
     // Panel not found
     // Panel not found
     this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));
     this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));

+ 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 - 1
public/app/features/explore/state/actions.ts

@@ -597,7 +597,8 @@ function runQueriesForType(
         const res = await datasourceInstance.query(transaction.options);
         const res = await datasourceInstance.query(transaction.options);
         eventBridge.emit('data-received', res.data || []);
         eventBridge.emit('data-received', res.data || []);
         const latency = Date.now() - now;
         const latency = Date.now() - now;
-        const results = resultGetter ? resultGetter(res.data) : res.data;
+        const { queryTransactions } = getState().explore[exploreId];
+        const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
         dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
         dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
       } catch (response) {
       } catch (response) {
         eventBridge.emit('data-error', response);
         eventBridge.emit('data-error', response);

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

@@ -23,9 +23,11 @@ import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
 import * as alertListPanel from 'app/plugins/panel/alertlist/module';
 import * as alertListPanel from 'app/plugins/panel/alertlist/module';
 import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
 import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
 import * as tablePanel from 'app/plugins/panel/table/module';
 import * as tablePanel from 'app/plugins/panel/table/module';
+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,
@@ -53,9 +55,11 @@ const builtInPlugins = {
   'app/plugins/panel/alertlist/module': alertListPanel,
   'app/plugins/panel/alertlist/module': alertListPanel,
   'app/plugins/panel/heatmap/module': heatmapPanel,
   'app/plugins/panel/heatmap/module': heatmapPanel,
   'app/plugins/panel/table/module': tablePanel,
   'app/plugins/panel/table/module': tablePanel,
+  'app/plugins/panel/table2/module': table2Panel,
   '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;

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

@@ -0,0 +1,36 @@
+// Libraries
+import React from 'react';
+
+// Services & Utils
+import { DisplayValue, VizOrientation } from '@grafana/ui';
+import { config } from 'app/core/config';
+
+// Components
+import { BarGauge } from '@grafana/ui';
+
+// Types
+import { BarGaugeOptions } from './types';
+import { SingleStatPanel } from '../gauge/SingleStatPanel';
+
+export class BarGaugePanel extends SingleStatPanel<BarGaugeOptions> {
+  getOrientation(): VizOrientation {
+    const { options } = this.props;
+    return options.orientation;
+  }
+
+  renderStat(value: DisplayValue, width: number, height: number) {
+    const { options } = this.props;
+    const { display } = options;
+
+    return (
+      <BarGauge
+        value={value}
+        width={width}
+        height={height}
+        orientation={options.orientation}
+        thresholds={display.thresholds}
+        theme={config.theme}
+      />
+    );
+  }
+}

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

@@ -0,0 +1,75 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Components
+import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
+import {
+  PanelOptionsGrid,
+  PanelOptionsGroup,
+  FormField,
+  DisplayValueOptions,
+  ThresholdsEditor,
+  Threshold,
+} from '@grafana/ui';
+
+// Types
+import { FormLabel, PanelEditorProps, Select, ValueMappingsEditor, ValueMapping } from '@grafana/ui';
+import { BarGaugeOptions, orientationOptions } from './types';
+import { DisplayValueEditor } from '../gauge/DisplayValueEditor';
+
+export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
+  onDisplayOptionsChanged = (displayOptions: DisplayValueOptions) =>
+    this.props.onOptionsChange({
+      ...this.props.options,
+      display: displayOptions,
+    });
+
+  onThresholdsChanged = (thresholds: Threshold[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.display,
+      thresholds,
+    });
+
+  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.display,
+      mappings: valueMappings,
+    });
+
+  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 { onOptionsChange, options } = this.props;
+    const { display } = options;
+
+    return (
+      <>
+        <PanelOptionsGrid>
+          {/* This just sets the 'stats', that should be moved to somethign more general */}
+          <SingleStatValueEditor onChange={onOptionsChange} options={options} />
+
+          <DisplayValueEditor onChange={this.onDisplayOptionsChanged} options={display} />
+
+          <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={display.thresholds} />
+          <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={display.mappings} />
+        </PanelOptionsGrid>
+      </>
+    );
+  }
+}

+ 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>

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

@@ -0,0 +1,23 @@
+import { ReactPanelPlugin } from '@grafana/ui';
+import cloneDeep from 'lodash/cloneDeep';
+
+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.display) {
+    options.stat = prevOptions.stat;
+    options.display = cloneDeep(prevOptions.display);
+    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"
+    }
+  }
+}

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

@@ -0,0 +1,34 @@
+import { SelectOptionItem, VizOrientation } from '@grafana/ui';
+
+import { SingleStatOptions } from '@grafana/ui';
+
+export interface BarGaugeOptions extends SingleStatOptions {
+  maxValue: number;
+  minValue: number;
+  showThresholdLabels: boolean;
+  showThresholdMarkers: boolean;
+  orientation: VizOrientation;
+}
+
+export const orientationOptions: SelectOptionItem[] = [
+  { value: VizOrientation.Horizontal, label: 'Horizontal' },
+  { value: VizOrientation.Vertical, label: 'Vertical' },
+];
+
+export const defaults: BarGaugeOptions = {
+  minValue: 0,
+  maxValue: 100,
+  showThresholdMarkers: true,
+  showThresholdLabels: false,
+  orientation: VizOrientation.Horizontal,
+
+  stat: 'avg',
+  display: {
+    prefix: '',
+    suffix: '',
+    decimals: null,
+    unit: 'none',
+    mappings: [],
+    thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+  },
+};

+ 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}
         />
         />

+ 23 - 74
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -1,94 +1,43 @@
 // Libraries
 // Libraries
-import React, { Component } from 'react';
+import React from 'react';
 
 
 // Services & Utils
 // Services & Utils
-import { processTimeSeries, ThemeContext } from '@grafana/ui';
+import { PanelProps } from '@grafana/ui';
+import { config } from 'app/core/config';
 
 
 // Components
 // Components
 import { Gauge } from '@grafana/ui';
 import { Gauge } from '@grafana/ui';
 
 
 // Types
 // Types
 import { GaugeOptions } from './types';
 import { GaugeOptions } from './types';
-import { PanelProps, NullValueMode, BasicGaugeColor, DisplayValue, getDisplayProcessor } from '@grafana/ui';
+import { DisplayValue } from '@grafana/ui/src/utils/displayValue';
+import { SingleStatPanel } from './SingleStatPanel';
 
 
-interface Props extends PanelProps<GaugeOptions> {}
-interface State {
-  value: DisplayValue;
-}
-
-export class GaugePanel extends Component<Props, State> {
-  constructor(props: Props) {
+export class GaugePanel extends SingleStatPanel<GaugeOptions> {
+  constructor(props: PanelProps<GaugeOptions>) {
     super(props);
     super(props);
 
 
-    if (props.options.valueOptions) {
-      console.warn('TODO!! how do we best migration options?');
-    }
-
-    this.state = {
-      value: this.findDisplayValue(props),
-    };
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (this.props.panelData !== prevProps.panelData) {
-      this.setState({ value: this.findDisplayValue(this.props) });
-    }
-  }
-
-  findDisplayValue(props: Props): DisplayValue {
-    const { replaceVariables, options } = this.props;
-    const { display } = options;
-
-    const prefix = replaceVariables(display.prefix);
-    const suffix = replaceVariables(display.suffix);
-    return getDisplayProcessor({
-      color: BasicGaugeColor.Red, // The default color
-      ...display,
-      prefix,
-      suffix,
-      // ??? theme:getTheme(GrafanaThemeType.Dark), !! how do I get it here???
-    })(this.findValue(props));
-  }
-
-  findValue(props: Props): number | null {
-    const { panelData, options } = props;
-
-    if (panelData.timeSeries) {
-      const vmSeries = processTimeSeries({
-        timeSeries: panelData.timeSeries,
-        nullValueMode: NullValueMode.Null,
-      });
-
-      if (vmSeries[0]) {
-        return vmSeries[0].stats[options.stat];
-      }
-    } else if (panelData.tableData) {
-      return panelData.tableData.rows[0].find(prop => prop > 0);
-    }
-    return null;
+    // if (props.options.valueOptions) {
+    //   console.warn('TODO!! how do we best migration options?');
+    // }
   }
   }
 
 
-  render() {
-    const { width, height, options } = this.props;
-    const { value } = this.state;
+  renderStat(value: DisplayValue, width: number, height: number) {
+    const { options } = this.props;
     const { display } = options;
     const { display } = options;
 
 
     return (
     return (
-      <ThemeContext.Consumer>
-        {theme => (
-          <Gauge
-            value={value}
-            width={width}
-            height={height}
-            thresholds={display.thresholds}
-            showThresholdLabels={options.showThresholdLabels}
-            showThresholdMarkers={options.showThresholdMarkers}
-            minValue={options.minValue}
-            maxValue={options.maxValue}
-            theme={theme}
-          />
-        )}
-      </ThemeContext.Consumer>
+      <Gauge
+        value={value}
+        width={width}
+        height={height}
+        thresholds={display.thresholds}
+        showThresholdLabels={options.showThresholdLabels}
+        showThresholdMarkers={options.showThresholdMarkers}
+        minValue={options.minValue}
+        maxValue={options.maxValue}
+        theme={config.theme}
+      />
     );
     );
   }
   }
 }
 }

+ 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,

+ 77 - 0
public/app/plugins/panel/gauge/SingleStatPanel.tsx

@@ -0,0 +1,77 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Services & Utils
+import { processSingleStatPanelData, SingleStatOptions, DisplayValue, PanelProps, VizOrientation } from '@grafana/ui';
+import { config } from 'app/core/config';
+
+// Components
+import { VizRepeater, getDisplayProcessor } from '@grafana/ui';
+
+interface State {
+  values: DisplayValue[];
+}
+
+export class SingleStatPanel<T extends SingleStatOptions> extends PureComponent<PanelProps<T>, State> {
+  constructor(props: PanelProps<T>) {
+    super(props);
+
+    // if (props.options.valueOptions) {
+    //   console.warn('TODO!! how do we best migration options?');
+    // }
+
+    this.state = {
+      values: this.findDisplayValues(props),
+    };
+  }
+
+  componentDidUpdate(prevProps: PanelProps<T>) {
+    if (this.props.panelData !== prevProps.panelData) {
+      this.setState({ values: this.findDisplayValues(this.props) });
+    }
+  }
+
+  findDisplayValues(props: PanelProps<T>): DisplayValue[] {
+    const { panelData, replaceVariables, options } = this.props;
+    const { display } = options;
+
+    const processor = getDisplayProcessor({
+      ...display,
+      prefix: replaceVariables(display.prefix),
+      suffix: replaceVariables(display.suffix),
+      theme: config.theme,
+    });
+
+    return processSingleStatPanelData({
+      panelData: panelData,
+      stat: options.stat,
+    }).map(stat => processor(stat.value));
+  }
+
+  /**
+   * Subclasses can render this function
+   *
+   * @param value
+   * @param width
+   * @param height
+   */
+  renderStat(value: DisplayValue, width: number, height: number) {
+    return <div style={{ width, height, border: '1px solid red' }}>{value.text}</div>;
+  }
+
+  // Or we could add this to single stat props?
+  getOrientation(): VizOrientation {
+    return VizOrientation.Auto;
+  }
+
+  render() {
+    const { height, width } = this.props;
+    const { values } = this.state;
+
+    return (
+      <VizRepeater height={height} width={width} values={values} orientation={this.getOrientation()}>
+        {({ vizHeight, vizWidth, value }) => this.renderStat(value, vizWidth, vizHeight)}
+      </VizRepeater>
+    );
+  }
+}

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

@@ -1,4 +1,5 @@
 import { ReactPanelPlugin } from '@grafana/ui';
 import { ReactPanelPlugin } from '@grafana/ui';
+import cloneDeep from 'lodash/cloneDeep';
 
 
 import { GaugePanelEditor } from './GaugePanelEditor';
 import { GaugePanelEditor } from './GaugePanelEditor';
 import { GaugePanel } from './GaugePanel';
 import { GaugePanel } from './GaugePanel';
@@ -8,3 +9,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.display) {
+    options.stat = prevOptions.stat;
+    options.display = cloneDeep(prevOptions.display);
+    options.maxValue = prevOptions.maxValue;
+    options.minValue = prevOptions.minValue;
+  }
+
+  return options;
+});

+ 6 - 18
public/app/plugins/panel/gauge/types.ts

@@ -1,27 +1,15 @@
-import { Threshold, ValueMapping, DisplayValueOptions } from '@grafana/ui';
+import { SingleStatOptions } from '@grafana/ui';
 
 
-export interface GaugeOptions {
+export interface GaugeOptions extends SingleStatOptions {
   maxValue: number;
   maxValue: number;
   minValue: number;
   minValue: number;
   showThresholdLabels: boolean;
   showThresholdLabels: boolean;
   showThresholdMarkers: boolean;
   showThresholdMarkers: boolean;
 
 
-  stat: string;
-  display: DisplayValueOptions;
-
   // TODO: migrate to DisplayValueOptions
   // TODO: migrate to DisplayValueOptions
-  thresholds?: Threshold[];
-  valueMappings?: ValueMapping[];
-  valueOptions?: SingleStatValueOptions;
-}
-
-/** Deprecated -- migrate to  */
-export interface SingleStatValueOptions {
-  unit: string;
-  suffix: string;
-  stat: string;
-  prefix: string;
-  decimals?: number | null;
+  // thresholds?: Threshold[];
+  // valueMappings?: ValueMapping[];
+  // valueOptions?: SingleStatValueOptions;
 }
 }
 
 
 export const defaults: GaugeOptions = {
 export const defaults: GaugeOptions = {
@@ -37,6 +25,6 @@ export const defaults: GaugeOptions = {
     decimals: null,
     decimals: null,
     unit: 'none',
     unit: 'none',
     mappings: [],
     mappings: [],
-    thresholds: [],
+    thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
   },
   },
 };
 };

+ 5 - 3
public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx

@@ -174,9 +174,11 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
         onToggleAxis={this.props.onToggleAxis}
         onToggleAxis={this.props.onToggleAxis}
         enableNamedColors
         enableNamedColors
       >
       >
-        <span className="graph-legend-icon">
-          <SeriesIcon color={this.props.color} />
-        </span>
+        {({ ref, showColorPicker, hideColorPicker }) => (
+          <span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker} className="graph-legend-icon">
+            <SeriesIcon color={this.props.color} />
+          </span>
+        )}
       </SeriesColorPicker>
       </SeriesColorPicker>
     );
     );
   }
   }

+ 11 - 0
public/app/plugins/panel/graph/axes_editor.html

@@ -67,6 +67,17 @@
 			<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Number of buckets'" data-placement="right">
 			<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Number of buckets'" data-placement="right">
 		</div>
 		</div>
 
 
+		<div class="gf-form-inline" ng-if="ctrl.panel.xaxis.mode === 'histogram'">
+			<div class="gf-form">
+				<label class="gf-form-label width-6">X-Min</label>
+				<input type="number" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="ctrl.panel.xaxis.min" ng-change="ctrl.render()" ng-model-onblur>
+			</div>
+			<div class="gf-form">
+				<label class="gf-form-label width-6">X-Max</label>
+				<input type="number" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="ctrl.panel.xaxis.max" ng-change="ctrl.render()" ng-model-onblur>
+			</div>
+		</div>
+
 		<div>
 		<div>
 			<br/>
 			<br/>
 			<h5 class="section-heading">Y-Axes</h5>
 			<h5 class="section-heading">Y-Axes</h5>

+ 10 - 2
public/app/plugins/panel/graph/graph.ts

@@ -337,9 +337,17 @@ class GraphElement {
         let bucketSize: number;
         let bucketSize: number;
 
 
         if (this.data.length) {
         if (this.data.length) {
-          const histMin = _.min(_.map(this.data, s => s.stats.min));
-          const histMax = _.max(_.map(this.data, s => s.stats.max));
+          let histMin = _.min(_.map(this.data, s => s.stats.min));
+          let histMax = _.max(_.map(this.data, s => s.stats.max));
           const ticks = panel.xaxis.buckets || this.panelWidth / 50;
           const ticks = panel.xaxis.buckets || this.panelWidth / 50;
+          if (panel.xaxis.min != null) {
+            const isInvalidXaxisMin = tickStep(panel.xaxis.min, histMax, ticks) <= 0;
+            histMin = isInvalidXaxisMin ? histMin : panel.xaxis.min;
+          }
+          if (panel.xaxis.max != null) {
+            const isInvalidXaxisMax = tickStep(histMin, panel.xaxis.max, ticks) <= 0;
+            histMax = isInvalidXaxisMax ? histMax : panel.xaxis.max;
+          }
           bucketSize = tickStep(histMin, histMax, ticks);
           bucketSize = tickStep(histMin, histMax, ticks);
           options.series.bars.barWidth = bucketSize * 0.8;
           options.series.bars.barWidth = bucketSize * 0.8;
           this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
           this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);

+ 4 - 0
public/app/plugins/panel/graph/histogram.ts

@@ -43,6 +43,10 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, m
   }
   }
 
 
   for (let i = 0; i < values.length; i++) {
   for (let i = 0; i < values.length; i++) {
+    // filter out values outside the min and max boundaries
+    if (values[i] < min || values[i] > max) {
+      continue;
+    }
     const bound = getBucketBound(values[i], bucketSize);
     const bound = getBucketBound(values[i], bucketSize);
     histogram[bound] = histogram[bound] + 1;
     histogram[bound] = histogram[bound] + 1;
   }
   }

+ 404 - 0
public/app/plugins/panel/graph/specs/graph.test.ts

@@ -516,4 +516,408 @@ describe('grafanaGraph', () => {
       expect(ctx.plotData[0].data[0][1]).toBe(2);
       expect(ctx.plotData[0].data[0][1]).toBe(2);
     });
     });
   });
   });
+
+  describe('when graph is histogram, and xaxis min is set', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 150;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should not contain values lower than min', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min is zero', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 0;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should not contain values lower than zero', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min is null', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = null;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min should not affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min is undefined', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = undefined;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min should not affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is set', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = 250;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should not contain values greater than max', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is zero', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = 0;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should not contain values greater than zero', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is null', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = null;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis max should not affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is undefined', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = undefined;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis max should not should node affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min and max are set', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 150;
+        ctrl.panel.xaxis.max = 250;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should not contain values lower than min and greater than max', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min and max are zero', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 0;
+        ctrl.panel.xaxis.max = 0;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis max should be ignored otherwise the bucketSize is zero', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min and max are null', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = null;
+        ctrl.panel.xaxis.max = null;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min and max should not affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min and max are undefined', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = undefined;
+        ctrl.panel.xaxis.max = undefined;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min and max should not affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min is greater than xaxis max', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 150;
+        ctrl.panel.xaxis.max = 100;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis max should be ignored otherwise the bucketSize is negative', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  // aaa
+  describe('when graph is histogram, and xaxis min is greater than the maximum value', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 301;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min should be ignored otherwise the bucketSize is negative', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min is equal to the maximum value', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 300;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min should be ignored otherwise the bucketSize is zero', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis min is lower than the minimum value', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.min = 99;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('xaxis min should not affect the histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is equal to the minimum value', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = 100;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is a lower than the minimum value', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = 99;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should calculate empty histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(nonZero.length).toBe(0);
+    });
+  });
+
+  describe('when graph is histogram, and xaxis max is greater than the maximum value', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.xaxis.max = 301;
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', () => {
+      const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+      expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+      expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+    });
+  });
 });
 });

+ 20 - 11
public/app/plugins/panel/heatmap/color_legend.ts

@@ -69,10 +69,11 @@ coreModule.directive('heatmapLegend', () => {
       function render() {
       function render() {
         clearLegend(elem);
         clearLegend(elem);
         if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
         if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
-          const rangeFrom = 0;
-          const rangeTo = ctrl.data.cardStats.max;
-          const maxValue = panel.color.max || rangeTo;
-          const minValue = panel.color.min || 0;
+          const cardStats = ctrl.data.cardStats;
+          const rangeFrom = _.isNil(panel.color.min) ? Math.min(cardStats.min, 0) : panel.color.min;
+          const rangeTo = _.isNil(panel.color.max) ? cardStats.max : panel.color.max;
+          const maxValue = cardStats.max;
+          const minValue = cardStats.min;
 
 
           if (panel.color.mode === 'spectrum') {
           if (panel.color.mode === 'spectrum') {
             const colorScheme = _.find(ctrl.colorSchemes, {
             const colorScheme = _.find(ctrl.colorSchemes, {
@@ -110,7 +111,7 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
     .data(valuesRange)
     .data(valuesRange)
     .enter()
     .enter()
     .append('rect')
     .append('rect')
-    .attr('x', d => Math.round(d * widthFactor))
+    .attr('x', d => Math.round((d - rangeFrom) * widthFactor))
     .attr('y', 0)
     .attr('y', 0)
     .attr('width', Math.round(rangeStep * widthFactor + 1)) // Overlap rectangles to prevent gaps
     .attr('width', Math.round(rangeStep * widthFactor + 1)) // Overlap rectangles to prevent gaps
     .attr('height', legendHeight)
     .attr('height', legendHeight)
@@ -141,7 +142,7 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
     .data(valuesRange)
     .data(valuesRange)
     .enter()
     .enter()
     .append('rect')
     .append('rect')
-    .attr('x', d => Math.round(d * widthFactor))
+    .attr('x', d => Math.round((d - rangeFrom) * widthFactor))
     .attr('y', 0)
     .attr('y', 0)
     .attr('width', Math.round(rangeStep * widthFactor))
     .attr('width', Math.round(rangeStep * widthFactor))
     .attr('height', legendHeight)
     .attr('height', legendHeight)
@@ -162,10 +163,10 @@ function drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWi
 
 
   const legendValueScale = d3
   const legendValueScale = d3
     .scaleLinear()
     .scaleLinear()
-    .domain([0, rangeTo])
+    .domain([rangeFrom, rangeTo])
     .range([0, legendWidth]);
     .range([0, legendWidth]);
 
 
-  const ticks = buildLegendTicks(0, rangeTo, maxValue, minValue);
+  const ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
   const xAxis = d3
   const xAxis = d3
     .axisBottom(legendValueScale)
     .axisBottom(legendValueScale)
     .tickValues(ticks)
     .tickValues(ticks)
@@ -286,11 +287,12 @@ function getSvgElemHeight(elem) {
 function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
 function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
   const range = rangeTo - rangeFrom;
   const range = rangeTo - rangeFrom;
   const tickStepSize = tickStep(rangeFrom, rangeTo, 3);
   const tickStepSize = tickStep(rangeFrom, rangeTo, 3);
-  const ticksNum = Math.round(range / tickStepSize);
+  const ticksNum = Math.ceil(range / tickStepSize);
+  const firstTick = getFirstCloseTick(rangeFrom, tickStepSize);
   let ticks = [];
   let ticks = [];
 
 
   for (let i = 0; i < ticksNum; i++) {
   for (let i = 0; i < ticksNum; i++) {
-    const current = tickStepSize * i;
+    const current = firstTick + tickStepSize * i;
     // Add user-defined min and max if it had been set
     // Add user-defined min and max if it had been set
     if (isValueCloseTo(minValue, current, tickStepSize)) {
     if (isValueCloseTo(minValue, current, tickStepSize)) {
       ticks.push(minValue);
       ticks.push(minValue);
@@ -304,7 +306,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
     } else if (maxValue < current) {
     } else if (maxValue < current) {
       ticks.push(maxValue);
       ticks.push(maxValue);
     }
     }
-    ticks.push(tickStepSize * i);
+    ticks.push(current);
   }
   }
   if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
   if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
     ticks.push(maxValue);
     ticks.push(maxValue);
@@ -318,3 +320,10 @@ function isValueCloseTo(val, valueTo, step) {
   const diff = Math.abs(val - valueTo);
   const diff = Math.abs(val - valueTo);
   return diff < step * 0.3;
   return diff < step * 0.3;
 }
 }
+
+function getFirstCloseTick(minValue, step) {
+  if (minValue < 0) {
+    return Math.floor(minValue / step) * step;
+  }
+  return 0;
+}

+ 6 - 4
public/app/plugins/panel/heatmap/rendering.ts

@@ -524,14 +524,16 @@ export class HeatmapRenderer {
     }
     }
 
 
     const cardsData = this.data.cards;
     const cardsData = this.data.cards;
-    const maxValueAuto = this.data.cardStats.max;
-    const maxValue = this.panel.color.max || maxValueAuto;
-    const minValue = this.panel.color.min || 0;
+    const cardStats = this.data.cardStats;
+    const maxValueAuto = cardStats.max;
+    const minValueAuto = Math.min(cardStats.min, 0);
+    const maxValue = _.isNil(this.panel.color.max) ? maxValueAuto : this.panel.color.max;
+    const minValue = _.isNil(this.panel.color.min) ? minValueAuto : this.panel.color.min;
     const colorScheme = _.find(this.ctrl.colorSchemes, {
     const colorScheme = _.find(this.ctrl.colorSchemes, {
       value: this.panel.color.colorScheme,
       value: this.panel.color.colorScheme,
     });
     });
     this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
     this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
-    this.opacityScale = getOpacityScale(this.panel.color, maxValue);
+    this.opacityScale = getOpacityScale(this.panel.color, maxValue, minValue);
     this.setCardSize();
     this.setCardSize();
 
 
     let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
     let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);

+ 4 - 3
public/app/plugins/panel/table/renderer.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import moment from 'moment';
 import moment from 'moment';
 import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, stringToJsRegex } from '@grafana/ui';
 import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, stringToJsRegex } from '@grafana/ui';
+import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
 
 
 export class TableRenderer {
 export class TableRenderer {
   formatters: any[];
   formatters: any[];
@@ -50,7 +51,7 @@ export class TableRenderer {
     }
     }
   }
   }
 
 
-  getColorForValue(value, style) {
+  getColorForValue(value, style: ColumnStyle) {
     if (!style.thresholds) {
     if (!style.thresholds) {
       return null;
       return null;
     }
     }
@@ -62,7 +63,7 @@ export class TableRenderer {
     return getColorFromHexRgbOrName(_.first(style.colors), this.theme);
     return getColorFromHexRgbOrName(_.first(style.colors), this.theme);
   }
   }
 
 
-  defaultCellFormatter(v, style) {
+  defaultCellFormatter(v, style: ColumnStyle) {
     if (v === null || v === void 0 || v === undefined) {
     if (v === null || v === void 0 || v === undefined) {
       return '';
       return '';
     }
     }
@@ -189,7 +190,7 @@ export class TableRenderer {
     };
     };
   }
   }
 
 
-  setColorState(value, style) {
+  setColorState(value, style: ColumnStyle) {
     if (!style.colorMode) {
     if (!style.colorMode) {
       return;
       return;
     }
     }

+ 9 - 0
public/app/plugins/panel/table2/README.md

@@ -0,0 +1,9 @@
+# Table Panel -  Native Plugin
+
+The Table Panel is **included** with Grafana.
+
+The table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
+
+Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here:
+
+[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/)

+ 29 - 0
public/app/plugins/panel/table2/TablePanel.tsx

@@ -0,0 +1,29 @@
+// Libraries
+import React, { Component } from 'react';
+
+// Types
+import { PanelProps, ThemeContext } from '@grafana/ui';
+import { Options } from './types';
+import Table from '@grafana/ui/src/components/Table/Table';
+
+interface Props extends PanelProps<Options> {}
+
+export class TablePanel extends Component<Props> {
+  constructor(props: Props) {
+    super(props);
+  }
+
+  render() {
+    const { panelData, options } = this.props;
+
+    if (!panelData || !panelData.tableData) {
+      return <div>No Table Data...</div>;
+    }
+
+    return (
+      <ThemeContext.Consumer>
+        {theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
+      </ThemeContext.Consumer>
+    );
+  }
+}

+ 55 - 0
public/app/plugins/panel/table2/TablePanelEditor.tsx

@@ -0,0 +1,55 @@
+//// Libraries
+import _ from 'lodash';
+import React, { PureComponent } from 'react';
+
+// Types
+import { PanelEditorProps, Switch, FormField } from '@grafana/ui';
+import { Options } from './types';
+
+export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
+  onToggleShowHeader = () => {
+    this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader });
+  };
+
+  onToggleFixedHeader = () => {
+    this.props.onOptionsChange({ ...this.props.options, fixedHeader: !this.props.options.fixedHeader });
+  };
+
+  onToggleRotate = () => {
+    this.props.onOptionsChange({ ...this.props.options, rotate: !this.props.options.rotate });
+  };
+
+  onFixedColumnsChange = ({ target }) => {
+    this.props.onOptionsChange({ ...this.props.options, fixedColumns: target.value });
+  };
+
+  render() {
+    const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options;
+
+    return (
+      <div>
+        <div className="section gf-form-group">
+          <h5 className="section-heading">Header</h5>
+          <Switch label="Show" labelClass="width-6" checked={showHeader} onChange={this.onToggleShowHeader} />
+          <Switch label="Fixed" labelClass="width-6" checked={fixedHeader} onChange={this.onToggleFixedHeader} />
+        </div>
+
+        <div className="section gf-form-group">
+          <h5 className="section-heading">Display</h5>
+          <Switch label="Rotate" labelClass="width-8" checked={rotate} onChange={this.onToggleRotate} />
+          <FormField
+            label="Fixed Columns"
+            labelWidth={8}
+            inputWidth={4}
+            type="number"
+            step="1"
+            min="0"
+            max="100"
+            onChange={this.onFixedColumnsChange}
+            value={fixedColumns}
+          />
+        </div>
+      </div>
+    );
+  }
+}

+ 67 - 0
public/app/plugins/panel/table2/img/icn-table-panel.svg

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, 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="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<g>
+	<g>
+		<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="15.8113" y1="25" x2="15.8113" y2="-2.5362">
+			<stop  offset="0" style="stop-color:#FFF33B"/>
+			<stop  offset="0.0595" style="stop-color:#FFE029"/>
+			<stop  offset="0.1303" style="stop-color:#FFD218"/>
+			<stop  offset="0.2032" style="stop-color:#FEC90F"/>
+			<stop  offset="0.2809" style="stop-color:#FDC70C"/>
+			<stop  offset="0.6685" style="stop-color:#F3903F"/>
+			<stop  offset="0.8876" style="stop-color:#ED683C"/>
+			<stop  offset="1" style="stop-color:#E93E3A"/>
+		</linearGradient>
+		<rect x="0" style="fill:url(#SVGID_1_);" width="31.623" height="15.049"/>
+		<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="50" y1="25" x2="50" y2="-2.5362">
+			<stop  offset="0" style="stop-color:#FFF33B"/>
+			<stop  offset="0.0595" style="stop-color:#FFE029"/>
+			<stop  offset="0.1303" style="stop-color:#FFD218"/>
+			<stop  offset="0.2032" style="stop-color:#FEC90F"/>
+			<stop  offset="0.2809" style="stop-color:#FDC70C"/>
+			<stop  offset="0.6685" style="stop-color:#F3903F"/>
+			<stop  offset="0.8876" style="stop-color:#ED683C"/>
+			<stop  offset="1" style="stop-color:#E93E3A"/>
+		</linearGradient>
+		<rect x="34.188" style="fill:url(#SVGID_2_);" width="31.623" height="15.049"/>
+		<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="84.1887" y1="25" x2="84.1887" y2="-2.5362">
+			<stop  offset="0" style="stop-color:#FFF33B"/>
+			<stop  offset="0.0595" style="stop-color:#FFE029"/>
+			<stop  offset="0.1303" style="stop-color:#FFD218"/>
+			<stop  offset="0.2032" style="stop-color:#FEC90F"/>
+			<stop  offset="0.2809" style="stop-color:#FDC70C"/>
+			<stop  offset="0.6685" style="stop-color:#F3903F"/>
+			<stop  offset="0.8876" style="stop-color:#ED683C"/>
+			<stop  offset="1" style="stop-color:#E93E3A"/>
+		</linearGradient>
+		<rect x="68.377" style="fill:url(#SVGID_3_);" width="31.623" height="15.049"/>
+	</g>
+	<g>
+		<rect x="0" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
+		<rect x="34.188" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
+		<rect x="68.377" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
+	</g>
+	<g>
+		<rect x="0" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
+		<rect x="34.188" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
+		<rect x="68.377" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
+	</g>
+	<g>
+		<rect x="0" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
+		<rect x="34.188" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
+		<rect x="68.377" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
+	</g>
+	<g>
+		<rect x="0" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
+		<rect x="34.188" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
+		<rect x="68.377" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
+	</g>
+	<g>
+		<rect x="0" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
+		<rect x="34.188" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
+		<rect x="68.377" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
+	</g>
+</g>
+</svg>

+ 9 - 0
public/app/plugins/panel/table2/module.tsx

@@ -0,0 +1,9 @@
+import { ReactPanelPlugin } from '@grafana/ui';
+
+import { TablePanelEditor } from './TablePanelEditor';
+import { TablePanel } from './TablePanel';
+import { Options, defaults } from './types';
+
+export const reactPanel = new ReactPanelPlugin<Options>(TablePanel);
+reactPanel.setEditor(TablePanelEditor);
+reactPanel.setDefaults(defaults);

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

@@ -0,0 +1,19 @@
+{
+  "type": "panel",
+  "name": "React Table",
+  "id": "table2",
+  "state": "alpha",
+
+  "dataFormats": ["table"],
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icn-table-panel.svg",
+      "large": "img/icn-table-panel.svg"
+    }
+  }
+}

+ 35 - 0
public/app/plugins/panel/table2/types.ts

@@ -0,0 +1,35 @@
+import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
+
+export interface Options {
+  showHeader: boolean;
+  fixedHeader: boolean;
+  fixedColumns: number;
+  rotate: boolean;
+
+  styles: ColumnStyle[];
+}
+
+export const defaults: Options = {
+  showHeader: true,
+  fixedHeader: true,
+  fixedColumns: 0,
+  rotate: false,
+  styles: [
+    {
+      type: 'date',
+      pattern: 'Time',
+      alias: 'Time',
+      dateFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    {
+      unit: 'short',
+      type: 'number',
+      alias: '',
+      decimals: 2,
+      colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
+      colorMode: null,
+      pattern: '/.*/',
+      thresholds: [],
+    },
+  ],
+};

+ 8 - 1
public/app/types/explore.ts

@@ -4,13 +4,14 @@ import {
   RawTimeRange,
   RawTimeRange,
   TimeRange,
   TimeRange,
   DataQuery,
   DataQuery,
+  DataQueryResponseData,
   DataSourceSelectItem,
   DataSourceSelectItem,
   DataSourceApi,
   DataSourceApi,
   QueryHint,
   QueryHint,
   ExploreStartPageProps,
   ExploreStartPageProps,
 } from '@grafana/ui';
 } from '@grafana/ui';
 
 
-import { Emitter } from 'app/core/core';
+import { Emitter, TimeSeries } from 'app/core/core';
 import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model';
 import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model';
 import TableModel from 'app/core/table_model';
 import TableModel from 'app/core/table_model';
 
 
@@ -322,6 +323,12 @@ export interface QueryTransaction {
 
 
 export type RangeScanner = () => RawTimeRange;
 export type RangeScanner = () => RawTimeRange;
 
 
+export type ResultGetter = (
+  result: DataQueryResponseData,
+  transaction: QueryTransaction,
+  allTransactions: QueryTransaction[]
+) => TimeSeries;
+
 export interface TextMatch {
 export interface TextMatch {
   text: string;
   text: string;
   start: number;
   start: number;

+ 4 - 18
public/sass/_variables.generated.scss

@@ -55,7 +55,6 @@ $spacers: (
     ),
     ),
   ),
   ),
 ) !default;
 ) !default;
-$border-width: 1px !default;
 
 
 // Grid breakpoints
 // Grid breakpoints
 //
 //
@@ -88,14 +87,11 @@ $container-max-widths: (
 $grid-columns: 12 !default;
 $grid-columns: 12 !default;
 $grid-gutter-width: 30px !default;
 $grid-gutter-width: 30px !default;
 
 
-$enable-flex: true;
-
 // Typography
 // Typography
 // -------------------------
 // -------------------------
 
 
 $font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
 $font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
 $font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
 $font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
-$font-family-base: $font-family-sans-serif !default;
 
 
 $font-size-root: 14px !default;
 $font-size-root: 14px !default;
 $font-size-base: 13px !default;
 $font-size-base: 13px !default;
@@ -106,7 +102,9 @@ $font-size-sm: 12px !default;
 $font-size-xs: 10px !default;
 $font-size-xs: 10px !default;
 
 
 $line-height-base: 1.5 !default;
 $line-height-base: 1.5 !default;
-$font-weight-semi-bold: 500;
+
+$font-weight-regular: 400 !default;
+$font-weight-semi-bold: 500 !default;
 
 
 $font-size-h1: 28px !default;
 $font-size-h1: 28px !default;
 $font-size-h2: 24px !default;
 $font-size-h2: 24px !default;
@@ -116,18 +114,13 @@ $font-size-h5: 16px !default;
 $font-size-h6: 14px !default;
 $font-size-h6: 14px !default;
 
 
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-$headings-font-weight: 400 !default;
 $headings-line-height: 1.1 !default;
 $headings-line-height: 1.1 !default;
 
 
-$hr-border-width: $border-width !default;
-$dt-font-weight: bold !default;
-
 // Components
 // Components
 //
 //
 // Define common padding and border radius sizes and more.
 // Define common padding and border radius sizes and more.
 
 
-$line-height-lg: (4 / 3) !default;
-$line-height-sm: 1.5 !default;
+$border-width: 1px !default;
 
 
 $border-radius: 3px !default;
 $border-radius: 3px !default;
 $border-radius-lg: 5px !default;
 $border-radius-lg: 5px !default;
@@ -154,22 +147,17 @@ $input-padding-x: 10px !default;
 $input-padding-y: 8px !default;
 $input-padding-y: 8px !default;
 $input-line-height: 18px !default;
 $input-line-height: 18px !default;
 
 
-$input-btn-border-width: 1px;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 
 
 $label-border-radius: $border-radius 0 0 $border-radius !default;
 $label-border-radius: $border-radius 0 0 $border-radius !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 
 
-$input-padding-y-sm: 4px !default;
-
 $input-padding-x-lg: 20px !default;
 $input-padding-x-lg: 20px !default;
 $input-padding-y-lg: 10px !default;
 $input-padding-y-lg: 10px !default;
 
 
 $input-height: 35px !default;
 $input-height: 35px !default;
 
 
-$gf-form-input-height: 35px;
-
 $cursor-disabled: not-allowed !default;
 $cursor-disabled: not-allowed !default;
 
 
 // Form validation icons
 // Form validation icons
@@ -206,8 +194,6 @@ $btn-padding-y-lg: 11px !default;
 $btn-padding-x-xl: 21px !default;
 $btn-padding-x-xl: 21px !default;
 $btn-padding-y-xl: 11px !default;
 $btn-padding-y-xl: 11px !default;
 
 
-$btn-border-radius: 2px;
-
 $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 
 
 // sidemenu
 // sidemenu

+ 1 - 1
public/sass/base/_forms.scss

@@ -37,7 +37,7 @@ input,
 button,
 button,
 select,
 select,
 textarea {
 textarea {
-  font-family: $font-family-base; // And only set font-family here for those that need it (note the missing label element)
+  font-family: $font-family-sans-serif; // And only set font-family here for those that need it (note the missing label element)
 }
 }
 
 
 // Identify controls by their labels
 // Identify controls by their labels

+ 2 - 2
public/sass/base/_reboot.scss

@@ -70,7 +70,7 @@ html {
 
 
 body {
 body {
   // Make the `body` use the `font-size-root`
   // Make the `body` use the `font-size-root`
-  font-family: $font-family-base;
+  font-family: $font-family-sans-serif;
   font-size: $font-size-base;
   font-size: $font-size-base;
   line-height: $line-height-base;
   line-height: $line-height-base;
   // Go easy on the eyes and use something other than `#000` for text
   // Go easy on the eyes and use something other than `#000` for text
@@ -145,7 +145,7 @@ ul ol {
 }
 }
 
 
 dt {
 dt {
-  font-weight: $dt-font-weight;
+  font-weight: $font-weight-semi-bold;
 }
 }
 
 
 dd {
 dd {

+ 2 - 2
public/sass/base/_type.scss

@@ -111,7 +111,7 @@ h6,
 .h6 {
 .h6 {
   margin-bottom: $space-sm;
   margin-bottom: $space-sm;
   font-family: $headings-font-family;
   font-family: $headings-font-family;
-  font-weight: $headings-font-weight;
+  font-weight: $font-weight-regular;
   line-height: $headings-line-height;
   line-height: $headings-line-height;
   color: $headings-color;
   color: $headings-color;
 }
 }
@@ -149,7 +149,7 @@ hr {
   margin-top: $spacer-y;
   margin-top: $spacer-y;
   margin-bottom: $spacer-y;
   margin-bottom: $spacer-y;
   border: 0;
   border: 0;
-  border-top: $hr-border-width solid $hr-border-color;
+  border-top: $border-width solid $hr-border-color;
 }
 }
 
 
 //
 //

+ 5 - 5
public/sass/components/_buttons.scss

@@ -16,7 +16,7 @@
   cursor: pointer;
   cursor: pointer;
   border: none;
   border: none;
 
 
-  @include button-size($btn-padding-y, $btn-padding-x, $font-size-base, $btn-border-radius);
+  @include button-size($btn-padding-y, $btn-padding-x, $font-size-base, $border-radius-sm);
 
 
   &,
   &,
   &:active,
   &:active,
@@ -53,7 +53,7 @@
 // --------------------------------------------------
 // --------------------------------------------------
 // XLarge
 // XLarge
 .btn-xlarge {
 .btn-xlarge {
-  @include button-size($btn-padding-y-xl, $btn-padding-x-xl, $font-size-lg, $btn-border-radius);
+  @include button-size($btn-padding-y-xl, $btn-padding-x-xl, $font-size-lg, $border-radius-sm);
   font-weight: normal;
   font-weight: normal;
   padding-bottom: $btn-padding-y-xl - 3;
   padding-bottom: $btn-padding-y-xl - 3;
   .gicon {
   .gicon {
@@ -64,16 +64,16 @@
 
 
 // Large
 // Large
 .btn-large {
 .btn-large {
-  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius);
+  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $border-radius-sm);
   font-weight: normal;
   font-weight: normal;
 }
 }
 
 
 .btn-small {
 .btn-small {
-  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius);
+  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $border-radius-sm);
 }
 }
 
 
 .btn-mini {
 .btn-mini {
-  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-xs, $btn-border-radius);
+  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-xs, $border-radius-sm);
 }
 }
 
 
 .btn-link {
 .btn-link {

+ 1 - 1
public/sass/components/_code_editor.scss

@@ -10,7 +10,7 @@
     min-height: 3.6rem; // Include space for horizontal scrollbar
     min-height: 3.6rem; // Include space for horizontal scrollbar
 
 
     @include border-radius($input-border-radius-sm);
     @include border-radius($input-border-radius-sm);
-    border: $input-btn-border-width solid $input-border-color;
+    border: $border-width solid $input-border-color;
   }
   }
 
 
   .ace_content {
   .ace_content {

+ 5 - 5
public/sass/components/_gf-form.scss

@@ -105,9 +105,9 @@ $input-border: 1px solid $input-border-color;
 
 
   background-color: $input-label-bg;
   background-color: $input-label-bg;
   display: block;
   display: block;
-  height: $gf-form-input-height;
+  height: $input-height;
 
 
-  border: $input-btn-border-width solid $input-label-border-color;
+  border: $border-width solid $input-label-border-color;
   border-right: none;
   border-right: none;
   border-radius: $label-border-radius;
   border-radius: $label-border-radius;
 
 
@@ -127,7 +127,7 @@ $input-border: 1px solid $input-border-color;
   }
   }
 
 
   &--btn {
   &--btn {
-    border-right: $input-btn-border-width solid $input-label-border-color;
+    border-right: $border-width solid $input-label-border-color;
     border-radius: $border-radius;
     border-radius: $border-radius;
 
 
     &:hover {
     &:hover {
@@ -154,7 +154,7 @@ $input-border: 1px solid $input-border-color;
   flex-grow: 1;
   flex-grow: 1;
   margin: 0;
   margin: 0;
   margin-right: $space-xs;
   margin-right: $space-xs;
-  border: $input-btn-border-width solid transparent;
+  border: $border-width solid transparent;
   border-left: none;
   border-left: none;
   @include border-radius($label-border-radius-sm);
   @include border-radius($label-border-radius-sm);
 }
 }
@@ -166,7 +166,7 @@ $input-border: 1px solid $input-border-color;
 .gf-form-input {
 .gf-form-input {
   display: block;
   display: block;
   width: 100%;
   width: 100%;
-  height: $gf-form-input-height;
+  height: $input-height;
   padding: $input-padding-y $input-padding-x;
   padding: $input-padding-y $input-padding-x;
   font-size: $font-size-md;
   font-size: $font-size-md;
   line-height: $input-line-height;
   line-height: $input-line-height;

+ 1 - 1
public/sass/components/_panel_gettingstarted.scss

@@ -117,7 +117,7 @@ $path-position: $marker-size-half - ($path-height / 2);
 }
 }
 
 
 .progress-step-cta {
 .progress-step-cta {
-  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius);
+  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $border-radius-sm);
   @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl);
   @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl);
   display: none;
   display: none;
 }
 }

+ 1 - 1
public/sass/components/_slate_editor.scss

@@ -9,7 +9,7 @@
   position: relative;
   position: relative;
   display: inline-block;
   display: inline-block;
   padding: $input-padding-y $input-padding-x;
   padding: $input-padding-y $input-padding-x;
-  min-height: $gf-form-input-height;
+  min-height: $input-height;
   width: 100%;
   width: 100%;
   cursor: text;
   cursor: text;
   line-height: $line-height-base;
   line-height: $line-height-base;

+ 1 - 1
public/sass/components/_submenu.scss

@@ -42,7 +42,7 @@
   border-radius: $input-border-radius;
   border-radius: $input-border-radius;
   display: inline-block;
   display: inline-block;
   color: $text-color;
   color: $text-color;
-  height: $gf-form-input-height;
+  height: $input-height;
 
 
   .label-tag {
   .label-tag {
     margin: 0 5px;
     margin: 0 5px;

+ 2 - 2
public/sass/components/_switch.scss

@@ -26,7 +26,7 @@ gf-form-switch[disabled] {
   display: flex;
   display: flex;
   position: relative;
   position: relative;
   width: 60px;
   width: 60px;
-  height: $gf-form-input-height;
+  height: $input-height;
   background: $switch-bg;
   background: $switch-bg;
   border: 1px solid $input-border-color;
   border: 1px solid $input-border-color;
   border-left: none;
   border-left: none;
@@ -82,7 +82,7 @@ input:checked + .gf-form-switch__slider::before {
   position: relative;
   position: relative;
   display: flex;
   display: flex;
   width: 50px;
   width: 50px;
-  height: $gf-form-input-height;
+  height: $input-height;
   background: $switch-bg;
   background: $switch-bg;
   border: 1px solid $input-border-color;
   border: 1px solid $input-border-color;
   border-left: none;
   border-left: none;

+ 1 - 1
public/sass/components/_toolbar.scss

@@ -27,7 +27,7 @@
   line-height: $input-line-height;
   line-height: $input-line-height;
   color: $input-color;
   color: $input-color;
   background-color: $input-bg;
   background-color: $input-bg;
-  height: $gf-form-input-height;
+  height: $input-height;
   border: $input-border;
   border: $input-border;
   border-radius: $input-border-radius;
   border-radius: $input-border-radius;
   display: flex;
   display: flex;

+ 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;

+ 19 - 0
scripts/ci-frontend-metrics.sh

@@ -0,0 +1,19 @@
+#!/bin/bash
+
+echo -e "Collecting code stats (typescript errors & more)"
+
+ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --noImplicitAny true | grep -oP 'Found \K(\d+)')"
+DIRECTIVES="$(grep -r -o  directive public/app/**/*  | wc -l)"
+CONTROLLERS="$(grep -r -oP 'class .*Ctrl' public/app/**/*  | wc -l)"
+
+echo -e "Typescript errors: $ERROR_COUNT"
+echo -e "Directives: $DIRECTIVES"
+echo -e "Controllers: $CONTROLLERS"
+
+./scripts/ci-metrics-publisher.sh \
+  grafana.ci-code.noImplicitAny=$ERROR_COUNT \
+  grafana.ci-code.directives=$DIRECTIVES \
+  grafana.ci-code.controllers=$CONTROLLERS \
+
+
+

+ 18 - 0
scripts/ci-metrics-publisher.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+echo "Publishing CI Metrics"
+
+data=""
+
+for ((i = 1; i <= $#; i++ )); do
+  remainder="${!i}"
+  first="${remainder%%=*}"; remainder="${remainder#*=}"
+  if [ -n "$data" ]; then
+    data="$data,"
+  fi
+  data=''$data'{"name": "'${first}'", "value": '${remainder}', "interval": 60, "mtype": "gauge", "time": '$(date +%s)'}'
+done
+
+curl https://6371:$GRAFANA_MISC_STATS_API_KEY@graphite-us-central1.grafana.net/metrics \
+  -H 'Content-type: application/json' \
+  -d "[$data]"

+ 0 - 30
scripts/circle-metrics.sh

@@ -1,30 +0,0 @@
-#!/bin/bash
-
-echo "Collecting code stats (typescript errors & more)"
-
-ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --noImplicitAny true | grep -oP 'Found \K(\d+)')"
-DIRECTIVES="$(grep -r -o  directive public/app/**/*  | wc -l)"
-CONTROLLERS="$(grep -r -oP 'class .*Ctrl' public/app/**/*  | wc -l)"
-
-echo "Typescript errors: $ERROR_COUNT"
-echo "Directives: $DIRECTIVES"
-echo "Controllers: $CONTROLLERS"
-
-curl \
-   -d "{\"metrics\": {
-        \"ci.code.noImplicitAny\": $ERROR_COUNT,
-        \"ci.code.directives\": $DIRECTIVES,
-        \"ci.code.controllers\": $CONTROLLERS
-      }
-    }" \
-   -H "Content-Type: application/json" \
-   -u ci:$CIRCLE_STATS_PWD \
-   -X POST https://stats.grafana.org/metric-receiver
-
-curl https://6371:$GRAFANA_MISC_STATS_API_KEY@graphite-us-central1.grafana.net/metrics \
-  -H 'Content-type: application/json' \
-  -d '[
-      {"name":"grafana.ci-code.noImplicitAny", "interval":60, "value": '$ERROR_COUNT', "mtype": "gauge", "time": '$(date +%s)'},
-      {"name":"grafana.ci-code.directives", "interval":60, "value": '$DIRECTIVES', "mtype": "gauge", "time": '$(date +%s)'},
-      {"name":"grafana.ci-code.controllers", "interval":60, "value": '$CONTROLLERS', "mtype": "gauge", "time": '$(date +%s)'}
-   ]'

+ 11 - 5
scripts/circle-test-frontend.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
 #!/bin/bash
+
 function exit_if_fail {
 function exit_if_fail {
     command=$@
     command=$@
     echo "Executing '$command'"
     echo "Executing '$command'"
@@ -10,11 +11,16 @@ function exit_if_fail {
     fi
     fi
 }
 }
 
 
+start=$(date +%s)
+
 exit_if_fail npm run prettier:check
 exit_if_fail npm run prettier:check
-exit_if_fail npm run test
+# exit_if_fail npm run test
 
 
-# On master also collect some and send some metrics
-branch="$(git rev-parse --abbrev-ref HEAD)"
-if [ "${branch}" == "master" ]; then
-  exit_if_fail ./scripts/circle-metrics.sh
+end=$(date +%s)
+seconds=$((end - start))
+
+if [ "${CIRCLE_BRANCH}" == "master" ]; then
+	exit_if_fail ./scripts/ci-frontend-metrics.sh
+	exit_if_fail ./scripts/ci-metrics-publisher.sh grafana.ci-performance.frontend-tests=$seconds
 fi
 fi
+