Explorar el Código

Merge branch 'master' into hugoh/bug-viewers-can-edit-not-working-for-explore

Torkel Ödegaard hace 7 años
padre
commit
778e1f78c2
Se han modificado 44 ficheros con 2060 adiciones y 690 borrados
  1. 1250 0
      devenv/dev-dashboards/panel_tests_gauge.json
  2. 1 1
      docs/sources/auth/gitlab.md
  3. 0 1
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  4. 224 0
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  5. 284 0
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  6. 14 9
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  7. 1 0
      packages/grafana-ui/src/components/index.ts
  8. 21 3
      packages/grafana-ui/src/types/datasource.ts
  9. 7 0
      packages/grafana-ui/src/types/panel.ts
  10. 17 14
      packages/grafana-ui/src/types/plugin.ts
  11. 3 2
      packages/grafana-ui/src/types/series.ts
  12. 9 8
      packages/grafana-ui/src/utils/processTimeSeries.ts
  13. 5 0
      public/app/core/services/context_srv.ts
  14. 6 2
      public/app/core/utils/explore.ts
  15. 1 1
      public/app/features/dashboard/dashgrid/PanelResizer.tsx
  16. 5 8
      public/app/features/dashboard/panel_editor/EditorTabBody.tsx
  17. 8 9
      public/app/features/dashboard/panel_editor/QueriesTab.tsx
  18. 2 2
      public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
  19. 1 15
      public/app/features/dashboard/panel_editor/QueryInspector.tsx
  20. 0 3
      public/app/features/dashboard/panel_model.ts
  21. 13 6
      public/app/features/explore/Explore.tsx
  22. 3 7
      public/app/features/explore/QueryEditor.tsx
  23. 10 5
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  24. 10 4
      public/app/plugins/datasource/loki/datasource.test.ts
  25. 12 7
      public/app/plugins/datasource/loki/datasource.ts
  26. 11 5
      public/app/plugins/datasource/loki/language_provider.ts
  27. 6 0
      public/app/plugins/datasource/loki/types.ts
  28. 4 4
      public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
  29. 49 45
      public/app/plugins/datasource/prometheus/datasource.ts
  30. 6 0
      public/app/plugins/datasource/prometheus/types.ts
  31. 8 8
      public/app/plugins/datasource/testdata/QueryEditor.tsx
  32. 9 4
      public/app/plugins/datasource/testdata/datasource.ts
  33. 11 0
      public/app/plugins/datasource/testdata/types.ts
  34. 14 5
      public/app/plugins/panel/gauge/GaugePanel.tsx
  35. 0 2
      public/app/plugins/panel/gauge/GaugePanelOptions.tsx
  36. 0 1
      public/app/plugins/panel/gauge/types.ts
  37. 0 2
      public/app/plugins/panel/graph2/GraphPanel.tsx
  38. 2 2
      public/app/types/explore.ts
  39. 0 55
      public/app/viz/Gauge.test.tsx
  40. 0 216
      public/app/viz/Gauge.tsx
  41. 0 168
      public/app/viz/state/timeSeries.ts
  42. 2 66
      public/sass/components/_query_editor.scss
  43. 6 0
      public/sass/components/_toolbar.scss
  44. 25 0
      public/test/helpers/getQueryOptions.ts

+ 1250 - 0
devenv/dev-dashboards/panel_tests_gauge.json

@@ -0,0 +1,1250 @@
+{
+  "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,
+  "iteration": 1547810606599,
+  "links": [],
+  "panels": [
+    {
+      "collapsed": false,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 11,
+      "panels": [],
+      "title": "Value options tests",
+      "type": "row"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 5,
+        "x": 0,
+        "y": 1
+      },
+      "id": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "2",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "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": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "ms",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Average, 2 decimals, ms unit",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 5,
+        "y": 1
+      },
+      "id": 5,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "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": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "ms",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Max (90 ms), no decimals",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 5,
+        "x": 11,
+        "y": 1
+      },
+      "id": 6,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "p",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "s",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Current (10 ms), no unit, prefix (p), suffix (s)",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 4,
+        "w": 3,
+        "x": 16,
+        "y": 1
+      },
+      "id": 16,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 4,
+        "w": 5,
+        "x": 19,
+        "y": 1
+      },
+      "id": 18,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,91"
+        }
+      ],
+      "timeFrom": "1h",
+      "timeShift": null,
+      "title": "",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 4,
+        "w": 3,
+        "x": 16,
+        "y": 5
+      },
+      "id": 17,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 4,
+        "w": 5,
+        "x": 19,
+        "y": 5
+      },
+      "id": 19,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,81"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "",
+      "type": "gauge"
+    },
+    {
+      "collapsed": false,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 9
+      },
+      "id": 15,
+      "panels": [],
+      "title": "Value Mappings",
+      "type": "row"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 4,
+        "x": 0,
+        "y": 10
+      },
+      "id": 12,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": [
+          {
+            "from": "",
+            "id": 1,
+            "operator": "",
+            "text": "TEN",
+            "to": "",
+            "type": 1,
+            "value": "10"
+          }
+        ]
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "value mapping 10 -> TEN",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "description": "should read N/A",
+      "gridPos": {
+        "h": 8,
+        "w": 4,
+        "x": 4,
+        "y": 10
+      },
+      "id": 13,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": [
+          {
+            "from": "",
+            "id": 1,
+            "operator": "",
+            "text": "N/A",
+            "to": "",
+            "type": 1,
+            "value": "null"
+          }
+        ]
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,null,null,null,null"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "value mapping null -> N/A",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "description": "should read N/A",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 8,
+        "y": 10
+      },
+      "id": 20,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": [
+          {
+            "from": "0",
+            "id": 1,
+            "operator": "",
+            "text": "OK",
+            "to": "10",
+            "type": 2,
+            "value": "null"
+          }
+        ]
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,null,null,null,null,10"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "value mapping range, 0-10 -> OK, value 10",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "description": "should read N/A",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 14,
+        "y": 10
+      },
+      "id": 21,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "current",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": [
+          {
+            "from": "0",
+            "id": 1,
+            "operator": "",
+            "text": "OK",
+            "to": "90",
+            "type": 2,
+            "value": "null"
+          },
+          {
+            "from": "90",
+            "id": 2,
+            "operator": "",
+            "text": "BAD",
+            "to": "100",
+            "type": 2,
+            "value": ""
+          }
+        ]
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,null,null,null,null,10,95"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "value mapping range, 90-100 -> BAD, value 90",
+      "type": "gauge"
+    },
+    {
+      "collapsed": false,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 9,
+      "panels": [],
+      "title": "Templating & Repeat",
+      "type": "row"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 0,
+        "y": 19
+      },
+      "id": 7,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "2",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "$Servers",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "ms",
+        "valueMappings": []
+      },
+      "repeat": "Servers",
+      "repeatDirection": "h",
+      "scopedVars": {
+        "Servers": {
+          "selected": false,
+          "text": "server1",
+          "value": "server1"
+        }
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "repeat $Servers",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 6,
+        "y": 19
+      },
+      "id": 22,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "2",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "$Servers",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "ms",
+        "valueMappings": []
+      },
+      "repeat": null,
+      "repeatDirection": "h",
+      "repeatIteration": 1547810606599,
+      "repeatPanelId": 7,
+      "scopedVars": {
+        "Servers": {
+          "selected": false,
+          "text": "server2",
+          "value": "server2"
+        }
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "repeat $Servers",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 12,
+        "y": 19
+      },
+      "id": 23,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "2",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "$Servers",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "ms",
+        "valueMappings": []
+      },
+      "repeat": null,
+      "repeatDirection": "h",
+      "repeatIteration": 1547810606599,
+      "repeatPanelId": 7,
+      "scopedVars": {
+        "Servers": {
+          "selected": false,
+          "text": "server3",
+          "value": "server3"
+        }
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "repeat $Servers",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 18,
+        "y": 19
+      },
+      "id": 24,
+      "links": [],
+      "nullPointMode": "null",
+      "options-gauge": {
+        "baseColor": "#299c46",
+        "decimals": "2",
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "baseColor": "#299c46",
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "$Servers",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#e24d42",
+            "index": 2,
+            "value": 90
+          },
+          {
+            "color": "#ef843c",
+            "index": 1,
+            "value": 75
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "ms",
+        "valueMappings": []
+      },
+      "repeat": null,
+      "repeatDirection": "h",
+      "repeatIteration": 1547810606599,
+      "repeatPanelId": 7,
+      "scopedVars": {
+        "Servers": {
+          "selected": false,
+          "text": "server4",
+          "value": "server4"
+        }
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "repeat $Servers",
+      "type": "gauge"
+    }
+  ],
+  "refresh": false,
+  "schemaVersion": 17,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": [
+      {
+        "allValue": null,
+        "current": {
+          "selected": true,
+          "tags": [],
+          "text": "All",
+          "value": [
+            "$__all"
+          ]
+        },
+        "hide": 0,
+        "includeAll": true,
+        "label": null,
+        "multi": true,
+        "name": "Servers",
+        "options": [
+          {
+            "selected": true,
+            "text": "All",
+            "value": "$__all"
+          },
+          {
+            "selected": false,
+            "text": "server1",
+            "value": "server1"
+          },
+          {
+            "selected": false,
+            "text": "server2",
+            "value": "server2"
+          },
+          {
+            "selected": false,
+            "text": "server3",
+            "value": "server3"
+          },
+          {
+            "selected": false,
+            "text": "server4",
+            "value": "server4"
+          }
+        ],
+        "query": "server1,server2,server3,server4",
+        "skipUrlSync": false,
+        "type": "custom"
+      }
+    ]
+  },
+  "time": {
+    "from": "now-1h",
+    "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": "Panel Tests - Gauge",
+  "uid": "_5rDmaQiz",
+  "version": 5
+}

+ 1 - 1
docs/sources/auth/gitlab.md

@@ -47,7 +47,7 @@ authentication:
 
 ```bash
 [auth.gitlab]
-enabled = false
+enabled = true
 allow_sign_up = false
 client_id = GITLAB_APPLICATION_ID
 client_secret = GITLAB_SECRET

+ 0 - 1
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -25,7 +25,6 @@ export class CustomScrollbar extends PureComponent<Props> {
     autoHideDuration: 200,
     autoMaxHeight: '100%',
     hideTracksWhenNotNeeded: false,
-    scrollTop: 0,
     setScrollTop: () => {},
     autoHeightMin: '0'
   };

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

@@ -0,0 +1,224 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Gauge, Props } from './Gauge';
+import { TimeSeriesVMs } from '../../types/series';
+import { ValueMapping, MappingType } from '../../types';
+
+jest.mock('jquery', () => ({
+  plot: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    maxValue: 100,
+    valueMappings: [],
+    minValue: 0,
+    prefix: '',
+    showThresholdMarkers: true,
+    showThresholdLabels: false,
+    suffix: '',
+    thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
+    unit: 'none',
+    stat: 'avg',
+    height: 300,
+    width: 300,
+    timeSeries: {} as TimeSeriesVMs,
+    decimals: 0,
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<Gauge {...props} />);
+  const instance = wrapper.instance() as Gauge;
+
+  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.getFontColor(49)).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: 50, color: '#EAB839' },
+        { index: 0, value: -Infinity, color: '#7EB26D' },
+      ],
+    });
+
+    expect(instance.getFontColor(50)).toEqual('#EAB839');
+  });
+
+  it('should get the nearest threshold color between thresholds', () => {
+    const { instance } = setup({
+      thresholds: [
+        { index: 2, value: 75, color: '#6ED0E0' },
+        { index: 1, value: 50, color: '#EAB839' },
+        { index: 0, value: -Infinity, color: '#7EB26D' },
+      ],
+    });
+
+    expect(instance.getFontColor(55)).toEqual('#EAB839');
+  });
+});
+
+describe('Get thresholds formatted', () => {
+  it('should return first thresholds color for min and max', () => {
+    const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
+
+    expect(instance.getFormattedThresholds()).toEqual([
+      { value: 0, color: '#7EB26D' },
+      { value: 100, color: '#7EB26D' },
+    ]);
+  });
+
+  it('should get the correct formatted values when thresholds are added', () => {
+    const { instance } = setup({
+      thresholds: [
+        { index: 2, value: 75, color: '#6ED0E0' },
+        { index: 1, value: 50, color: '#EAB839' },
+        { index: 0, value: -Infinity, color: '#7EB26D' },
+      ],
+    });
+
+    expect(instance.getFormattedThresholds()).toEqual([
+      { value: 0, color: '#7EB26D' },
+      { value: 50, color: '#7EB26D' },
+      { value: 75, color: '#EAB839' },
+      { value: 100, color: '#6ED0E0' },
+    ]);
+  });
+});
+
+describe('Format value with value mappings', () => {
+  it('should return undefined with no valuemappings', () => {
+    const valueMappings: ValueMapping[] = [];
+    const value = '10';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.getFirstFormattedValueMapping(valueMappings, value);
+
+    expect(result).toBeUndefined();
+  });
+
+  it('should return undefined with no matching valuemappings', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+      { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
+    ];
+    const value = '10';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.getFirstFormattedValueMapping(valueMappings, value);
+
+    expect(result).toBeUndefined();
+  });
+
+  it('should return first matching mapping with lowest id', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+      { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
+    ];
+    const value = '10';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.getFirstFormattedValueMapping(valueMappings, value);
+
+    expect(result.text).toEqual('1-20');
+  });
+
+  it('should return rangeToText mapping where value equals to', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
+      { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+    ];
+    const value = '10';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.getFirstFormattedValueMapping(valueMappings, value);
+
+    expect(result.text).toEqual('1-10');
+  });
+
+  it('should return rangeToText mapping where value equals from', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
+      { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+    ];
+    const value = '10';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.getFirstFormattedValueMapping(valueMappings, value);
+
+    expect(result.text).toEqual('10-20');
+  });
+
+  it('should return rangeToText mapping where value is between from and to', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+      { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+    ];
+    const value = '10';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.getFirstFormattedValueMapping(valueMappings, value);
+
+    expect(result.text).toEqual('1-20');
+  });
+});
+
+describe('Format value', () => {
+  it('should return if value isNaN', () => {
+    const valueMappings: ValueMapping[] = [];
+    const value = 'N/A';
+    const { instance } = setup({ valueMappings });
+
+    const result = instance.formatValue(value);
+
+    expect(result).toEqual('N/A');
+  });
+
+  it('should return formatted value if there are no value mappings', () => {
+    const valueMappings: ValueMapping[] = [];
+    const value = '6';
+    const { instance } = setup({ valueMappings, decimals: 1 });
+
+    const result = instance.formatValue(value);
+
+    expect(result).toEqual(' 6.0 ');
+  });
+
+  it('should return formatted value if there are no matching value mappings', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+      { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
+    ];
+    const value = '10';
+    const { instance } = setup({ valueMappings, decimals: 1 });
+
+    const result = instance.formatValue(value);
+
+    expect(result).toEqual(' 10.0 ');
+  });
+
+  it('should return mapped value if there are matching value mappings', () => {
+    const valueMappings: ValueMapping[] = [
+      { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+      { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+    ];
+    const value = '11';
+    const { instance } = setup({ valueMappings, decimals: 1 });
+
+    const result = instance.formatValue(value);
+
+    expect(result).toEqual(' 1-20 ');
+  });
+});

+ 284 - 0
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -0,0 +1,284 @@
+import React, { PureComponent } from 'react';
+import $ from 'jquery';
+
+import {
+  ValueMapping,
+  Threshold,
+  ThemeName,
+  MappingType,
+  BasicGaugeColor,
+  ThemeNames,
+  ValueMap,
+  RangeMap,
+} from '../../types/panel';
+import { TimeSeriesVMs } from '../../types/series';
+import { getValueFormat } from '../../utils/valueFormats/valueFormats';
+
+type TimeSeriesValue = string | number | null;
+
+export interface Props {
+  decimals: number;
+  height: number;
+  valueMappings: ValueMapping[];
+  maxValue: number;
+  minValue: number;
+  prefix: string;
+  timeSeries: TimeSeriesVMs;
+  thresholds: Threshold[];
+  showThresholdMarkers: boolean;
+  showThresholdLabels: boolean;
+  stat: string;
+  suffix: string;
+  unit: string;
+  width: number;
+  theme?: ThemeName;
+}
+
+export class Gauge extends PureComponent<Props> {
+  canvasElement: any;
+
+  static defaultProps = {
+    maxValue: 100,
+    valueMappings: [],
+    minValue: 0,
+    prefix: '',
+    showThresholdMarkers: true,
+    showThresholdLabels: false,
+    suffix: '',
+    thresholds: [],
+    unit: 'none',
+    stat: 'avg',
+    theme: ThemeNames.Dark,
+  };
+
+  componentDidMount() {
+    this.draw();
+  }
+
+  componentDidUpdate() {
+    this.draw();
+  }
+
+  addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) {
+    if (!valueToTextMapping.value) {
+      return allValueMappings;
+    }
+
+    const valueAsNumber = parseFloat(value as string);
+    const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
+
+    if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
+      return allValueMappings;
+    }
+
+    if (valueAsNumber !== valueToTextMappingAsNumber) {
+      return allValueMappings;
+    }
+
+    return allValueMappings.concat(valueToTextMapping);
+  }
+
+  addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) {
+    if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) {
+      return allValueMappings;
+    }
+
+    const valueAsNumber = parseFloat(value as string);
+    const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
+    const toAsNumber = parseFloat(rangeToTextMapping.to as string);
+
+    if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
+      return allValueMappings;
+    }
+
+    if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
+      return allValueMappings.concat(rangeToTextMapping);
+    }
+
+    return allValueMappings;
+  }
+
+  getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) {
+    const allFormattedValueMappings = valueMappings.reduce(
+      (allValueMappings, valueMapping) => {
+        if (valueMapping.type === MappingType.ValueToText) {
+          allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
+        } else if (valueMapping.type === MappingType.RangeToText) {
+          allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
+        }
+
+        return allValueMappings;
+      },
+      [] as ValueMapping[]
+    );
+
+    allFormattedValueMappings.sort((t1, t2) => {
+      return t1.id - t2.id;
+    });
+
+    return allFormattedValueMappings;
+  }
+
+  getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) {
+    return this.getAllFormattedValueMappings(valueMappings, value)[0];
+  }
+
+  formatValue(value: TimeSeriesValue) {
+    const { decimals, valueMappings, prefix, suffix, unit } = this.props;
+
+    if (isNaN(value as number)) {
+      return value;
+    }
+
+    if (valueMappings.length > 0) {
+      const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value);
+      if (valueMappedValue) {
+        return `${prefix} ${valueMappedValue.text} ${suffix}`;
+      }
+    }
+
+    const formatFunc = getValueFormat(unit);
+    const formattedValue = formatFunc(value as number, decimals);
+
+    return `${prefix} ${formattedValue} ${suffix}`;
+  }
+
+  getFontColor(value: TimeSeriesValue) {
+    const { thresholds } = this.props;
+
+    if (thresholds.length === 1) {
+      return thresholds[0].color;
+    }
+
+    const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
+    if (atThreshold) {
+      return atThreshold.color;
+    }
+
+    const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
+
+    if (belowThreshold.length > 0) {
+      const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
+      return nearestThreshold.color;
+    }
+
+    return BasicGaugeColor.Red;
+  }
+
+  getFormattedThresholds() {
+    const { maxValue, minValue, thresholds } = this.props;
+
+    const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
+    const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
+
+    const formattedThresholds = [
+      ...thresholdsSortedByIndex.map(threshold => {
+        if (threshold.index === 0) {
+          return { value: minValue, color: threshold.color };
+        }
+
+        const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
+        return { value: threshold.value, color: previousThreshold.color };
+      }),
+      { value: maxValue, color: lastThreshold.color },
+    ];
+
+    return formattedThresholds;
+  }
+
+  draw() {
+    const {
+      maxValue,
+      minValue,
+      timeSeries,
+      showThresholdLabels,
+      showThresholdMarkers,
+      width,
+      height,
+      stat,
+      theme,
+    } = this.props;
+
+    let value: TimeSeriesValue = '';
+
+    if (timeSeries[0]) {
+      value = timeSeries[0].stats[stat];
+    } else {
+      value = 'N/A';
+    }
+
+    const dimension = Math.min(width, height * 1.3);
+    const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
+    const fontScale = parseInt('80', 10) / 100;
+    const fontSize = Math.min(dimension / 5, 100) * fontScale;
+    const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
+    const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
+    const thresholdMarkersWidth = gaugeWidth / 5;
+    const thresholdLabelFontSize = fontSize / 2.5;
+
+    const options = {
+      series: {
+        gauges: {
+          gauge: {
+            min: minValue,
+            max: maxValue,
+            background: { color: backgroundColor },
+            border: { color: null },
+            shadow: { show: false },
+            width: gaugeWidth,
+          },
+          frame: { show: false },
+          label: { show: false },
+          layout: { margin: 0, thresholdWidth: 0 },
+          cell: { border: { width: 0 } },
+          threshold: {
+            values: this.getFormattedThresholds(),
+            label: {
+              show: showThresholdLabels,
+              margin: thresholdMarkersWidth + 1,
+              font: { size: thresholdLabelFontSize },
+            },
+            show: showThresholdMarkers,
+            width: thresholdMarkersWidth,
+          },
+          value: {
+            color: this.getFontColor(value),
+            formatter: () => {
+              return this.formatValue(value);
+            },
+            font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
+          },
+          show: true,
+        },
+      },
+    };
+
+    const plotSeries = { data: [[0, value]] };
+
+    try {
+      $.plot(this.canvasElement, [plotSeries], options);
+    } catch (err) {
+      console.log('Gauge rendering error', err, options, timeSeries);
+    }
+  }
+
+  render() {
+    const { height, width } = this.props;
+
+    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>
+    );
+  }
+}
+
+export default Gauge;

+ 14 - 9
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

@@ -19,9 +19,15 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
 
-    const thresholds: Threshold[] =
-      props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
+    const addDefaultThreshold = this.props.thresholds.length === 0;
+    const thresholds: Threshold[] = addDefaultThreshold
+      ? [{ index: 0, value: -Infinity, color: colors[0] }]
+      : props.thresholds;
     this.state = { thresholds };
+
+    if (addDefaultThreshold) {
+      this.onChange();
+    }
   }
 
   onAddThreshold = (index: number) => {
@@ -62,7 +68,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
           },
         ]),
       },
-      () => this.updateGauge()
+      () => this.onChange()
     );
   };
 
@@ -85,7 +91,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
           thresholds: newThresholds.filter(t => t !== threshold),
         };
       },
-      () => this.updateGauge()
+      () => this.onChange()
     );
   };
 
@@ -99,7 +105,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
     const value = isNaN(parsedValue) ? null : parsedValue;
 
     const newThresholds = thresholds.map(t => {
-      if (t === threshold) {
+      if (t === threshold && t.index !== 0) {
         t = { ...t, value: value as number };
       }
 
@@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
       {
         thresholds: newThresholds,
       },
-      () => this.updateGauge()
+      () => this.onChange()
     );
   };
 
-  onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
   onBlur = () => {
     this.setState(prevState => {
       const sortThresholds = this.sortThresholds([...prevState.thresholds]);
@@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
       return { thresholds: sortThresholds };
     });
 
-    this.updateGauge();
+    this.onChange();
   };
 
-  updateGauge = () => {
+  onChange = () => {
     this.props.onChange(this.state.thresholds);
   };
 

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

@@ -22,3 +22,4 @@ export { Graph } from './Graph/Graph';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
 export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
+export { Gauge } from './Gauge/Gauge';

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

@@ -7,15 +7,33 @@ export interface DataQueryResponse {
 }
 
 export interface DataQuery {
+  /**
+   * A - Z
+   */
   refId: string;
-  [key: string]: any;
+
+  /**
+   * true if query is disabled (ie not executed / sent to TSDB)
+   */
+  hide?: boolean;
+
+  /**
+   * Unique, guid like, string used in explore mode
+   */
+  key?: string;
+
+  /**
+   * For mixed data sources the selected datasource is on the query level.
+   * For non mixed scenarios this is undefined.
+   */
+  datasource?: string | null;
 }
 
-export interface DataQueryOptions {
+export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
   timezone: string;
   range: TimeRange;
   rangeRaw: RawTimeRange;
-  targets: DataQuery[];
+  targets: TQuery[];
   panelId: number;
   dashboardId: number;
   cacheTimeout?: string;

+ 7 - 0
packages/grafana-ui/src/types/panel.ts

@@ -66,3 +66,10 @@ export interface RangeMap extends BaseMap {
   from: string;
   to: string;
 }
+
+export type ThemeName = 'dark' | 'light';
+
+export enum ThemeNames {
+  Dark = 'dark',
+  Light = 'light',
+}

+ 17 - 14
packages/grafana-ui/src/types/plugin.ts

@@ -2,11 +2,7 @@ import { ComponentClass } from 'react';
 import { PanelProps, PanelOptionsProps } from './panel';
 import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
 
-export interface DataSourceApi {
-  name: string;
-  meta: PluginMeta;
-  pluginExports: PluginExports;
-
+export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
   /**
    *  min interval range
    */
@@ -15,7 +11,7 @@ export interface DataSourceApi {
   /**
    * Imports queries from a different datasource
    */
-  importQueries?(queries: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]>;
+  importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
 
   /**
    * Initializes a datasource after instantiation
@@ -25,7 +21,7 @@ export interface DataSourceApi {
   /**
    * Main metrics / data query action
    */
-  query(options: DataQueryOptions): Promise<DataQueryResponse>;
+  query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
 
   /**
    * Test & verify datasource settings & connection details
@@ -35,20 +31,27 @@ export interface DataSourceApi {
   /**
    *  Get hints for query improvements
    */
-  getQueryHints(query: DataQuery, results: any[], ...rest: any): QueryHint[];
+  getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
+
+  /**
+   *  Set after constructor is called by Grafana
+   */
+  name?: string;
+  meta?: PluginMeta;
+  pluginExports?: PluginExports;
 }
 
-export interface QueryEditorProps {
-  datasource: DataSourceApi;
-  query: DataQuery;
+export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
+  datasource: DSType;
+  query: TQuery;
   onExecuteQuery?: () => void;
-  onQueryChange?: (value: DataQuery) => void;
+  onQueryChange?: (value: TQuery) => void;
 }
 
 export interface PluginExports {
-  Datasource?: any;
+  Datasource?: DataSourceApi;
   QueryCtrl?: any;
-  QueryEditor?: ComponentClass<QueryEditorProps>;
+  QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
   ConfigCtrl?: any;
   AnnotationsQueryCtrl?: any;
   VariableQueryEditor?: any;

+ 3 - 2
packages/grafana-ui/src/types/series.ts

@@ -21,9 +21,12 @@ export interface TimeSeriesVM {
   color: string;
   data: TimeSeriesValue[][];
   stats: TimeSeriesStats;
+  allIsNull: boolean;
+  allIsZero: boolean;
 }
 
 export interface TimeSeriesStats {
+  [key: string]: number | null;
   total: number | null;
   max: number | null;
   min: number | null;
@@ -36,8 +39,6 @@ export interface TimeSeriesStats {
   range: number | null;
   timeStep: number;
   count: number;
-  allIsNull: boolean;
-  allIsZero: boolean;
 }
 
 export enum NullValueMode {

+ 9 - 8
packages/grafana-ui/src/utils/processTimeSeries.ts

@@ -1,18 +1,19 @@
 // Libraries
 import _ from 'lodash';
 
+import { colors } from './colors';
+
 // Types
 import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
 
 interface Options {
   timeSeries: TimeSeries[];
   nullValueMode: NullValueMode;
-  colorPalette: string[];
 }
 
-export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs {
+export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
   const vmSeries = timeSeries.map((item, index) => {
-    const colorIndex = index % colorPalette.length;
+    const colorIndex = index % colors.length;
     const label = item.target;
     const result = [];
 
@@ -49,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
         continue;
       }
 
-      if (typeof currentValue !== 'number') {
-        continue;
+      if (currentValue !== null && typeof currentValue !== 'number') {
+        throw {message: 'Time series contains non number values'};
       }
 
       // Due to missing values we could have different timeStep all along the series
@@ -150,7 +151,9 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
     return {
       data: result,
       label: label,
-      color: colorPalette[colorIndex],
+      color: colors[colorIndex],
+      allIsZero,
+      allIsNull,
       stats: {
         total,
         min,
@@ -164,8 +167,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
         range,
         count,
         first,
-        allIsZero,
-        allIsNull,
       },
     };
   });

+ 5 - 0
public/app/core/services/context_srv.ts

@@ -2,6 +2,7 @@ import config from 'app/core/config';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import store from 'app/core/store';
+import { ThemeNames, ThemeName } from '@grafana/ui';
 
 export class User {
   isGrafanaAdmin: any;
@@ -63,6 +64,10 @@ export class ContextSrv {
   hasAccessToExplore() {
     return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
   }
+  
+  getTheme(): ThemeName {
+    return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark;
+  }
 }
 
 const contextSrv = new ContextSrv();

+ 6 - 2
public/app/core/utils/explore.ts

@@ -203,7 +203,7 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
 /**
  * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  */
-export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
+export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean {
   return (
     queries &&
     queries.some(
@@ -280,7 +280,11 @@ export function makeTimeSeriesList(dataList) {
 /**
  * Update the query history. Side-effect: store history in local storage
  */
-export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] {
+export function updateHistory<T extends DataQuery = any>(
+  history: Array<HistoryItem<T>>,
+  datasourceId: string,
+  queries: T[]
+): Array<HistoryItem<T>> {
   const ts = Date.now();
   queries.forEach(query => {
     history = [{ query, ts }, ...history];

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelResizer.tsx

@@ -15,7 +15,7 @@ interface State {
 }
 
 export class PanelResizer extends PureComponent<Props, State> {
-  initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.4);
+  initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.3);
   prevEditorHeight: number;
   throttledChangeHeight: (height: number) => void;
   throttledResizeDone: () => void;

+ 5 - 8
public/app/features/dashboard/panel_editor/EditorTabBody.tsx

@@ -111,14 +111,11 @@ export class EditorTabBody extends PureComponent<Props, State> {
     return (
       <>
         <div className="toolbar">
-          <div className="toolbar__heading">{heading}</div>
-          {renderToolbar && renderToolbar()}
-          {toolbarItems.length > 0 && (
-            <>
-              <div className="gf-form--grow" />
-              {toolbarItems.map(item => this.renderButton(item))}
-            </>
-          )}
+          <div className="toolbar__left">
+            <div className="toolbar__heading">{heading}</div>
+            {renderToolbar && renderToolbar()}
+          </div>
+          {toolbarItems.map(item => this.renderButton(item))}
         </div>
         <div className="panel-editor__scroll">
           <CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>

+ 8 - 9
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -18,7 +18,7 @@ import config from 'app/core/config';
 // Types
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
-import { DataQuery, DataSourceSelectItem  } from '@grafana/ui/src/types';
+import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 
 interface Props {
@@ -133,14 +133,13 @@ export class QueriesTab extends PureComponent<Props, State> {
     return (
       <>
         <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
-        <div className="m-l-2">
-          {!isAddingMixed && (
-            <button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
-              Add Query
-            </button>
-          )}
-          {isAddingMixed && this.renderMixedPicker()}
-        </div>
+        <div className="flex-grow" />
+        {!isAddingMixed && (
+          <button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
+            Add Query
+          </button>
+        )}
+        {isAddingMixed && this.renderMixedPicker()}
       </>
     );
   };

+ 2 - 2
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -51,7 +51,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
       target: query,
       panel: panel,
       refresh: () => panel.refresh(),
-      render: () => panel.render,
+      render: () => panel.render(),
       events: panel.events,
     };
   }
@@ -205,7 +205,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
             {inMixedMode && <em className="query-editor-row__context-info"> ({datasourceName})</em>}
             {isDisabled && <em className="query-editor-row__context-info"> Disabled</em>}
           </div>
-          <div className="query-editor-row__collapsed-text">
+          <div className="query-editor-row__collapsed-text" onClick={this.onToggleEditMode}>
             {isCollapsed && <div>{this.renderCollapsedText()}</div>}
           </div>
           <div className="query-editor-row__actions">

+ 1 - 15
public/app/features/dashboard/panel_editor/QueryInspector.tsx

@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
 
   render() {
     const { response, isLoading } = this.state.dsQuery;
-    const { isMocking } = this.state;
     const openNodes = this.getNrOfOpenNodes();
 
     if (isLoading) {
@@ -199,20 +198,7 @@ export class QueryInspector extends PureComponent<Props, State> {
           </CopyToClipboard>
         </div>
 
-        {!isMocking && <JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />}
-        {isMocking && (
-          <div className="query-troubleshooter__body">
-            <div className="gf-form p-l-1 gf-form--v-stretch">
-              <textarea
-                className="gf-form-input"
-                style={{ width: '95%' }}
-                rows={10}
-                onInput={this.setMockedResponse}
-                placeholder="JSON"
-              />
-            </div>
-          </div>
-        )}
+        <JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
       </>
     );
   }

+ 0 - 3
public/app/features/dashboard/panel_model.ts

@@ -55,7 +55,6 @@ const mustKeepProps: { [str: string]: boolean } = {
   hasRefreshed: true,
   events: true,
   cacheTimeout: true,
-  nullPointMode: true,
   cachedPluginOptions: true,
   transparent: true,
 };
@@ -244,8 +243,6 @@ export class PanelModel {
   addQuery(query?: Partial<DataQuery>) {
     query = query || { refId: 'A' };
     query.refId = this.getNextQueryLetter();
-    query.isNew = true;
-
     this.targets.push(query);
   }
 

+ 13 - 6
public/app/features/explore/Explore.tsx

@@ -242,11 +242,14 @@ export class Explore extends React.PureComponent<ExploreProps> {
               </a>
             </div>
           ) : (
-            <div className="navbar-buttons explore-first-button">
-              <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
-                Close Split
-              </button>
-            </div>
+            <>
+              <div className="navbar-page-btn" />
+              <div className="navbar-buttons explore-first-button">
+                <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
+                  Close Split
+                </button>
+              </div>
+            </>
           )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
@@ -274,7 +277,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
           <div className="navbar-buttons relative">
             <button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
               Run Query{' '}
-              {loading ? <i className="fa fa-spinner fa-fw fa-spin run-icon" /> : <i className="fa fa-level-down fa-fw run-icon" />}
+              {loading ? (
+                <i className="fa fa-spinner fa-fw fa-spin run-icon" />
+              ) : (
+                <i className="fa fa-level-down fa-fw run-icon" />
+              )}
             </button>
           </div>
         </div>

+ 3 - 7
public/app/features/explore/QueryEditor.tsx

@@ -3,7 +3,6 @@ import React, { PureComponent } from 'react';
 
 // Services
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
-import { getIntervals } from 'app/core/utils/explore';
 import { getTimeSrv } from 'app/features/dashboard/time_srv';
 
 // Types
@@ -37,8 +36,9 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
     const template = '<plugin-component type="query-ctrl"> </plugin-component>';
     const target = { datasource: datasource.name, ...initialQuery };
     const scopeProps = {
-      target,
       ctrl: {
+        datasource,
+        target,
         refresh: () => {
           this.props.onQueryChange(target, false);
           this.props.onExecuteQuery();
@@ -48,11 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
           datasource,
           targets: [target],
         },
-        dashboard: {
-          getNextQueryLetter: x => '',
-        },
-        hideEditorRowActions: true,
-        ...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
+        dashboard: {},
       },
     };
 

+ 10 - 5
public/app/plugins/datasource/loki/components/LokiQueryField.tsx

@@ -1,16 +1,21 @@
+// Libraries
 import React from 'react';
 import Cascader from 'rc-cascader';
 import PluginPrism from 'slate-prism';
 import Prism from 'prismjs';
 
-import { DataQuery } from '@grafana/ui/src/types';
-import { TypeaheadOutput } from 'app/types/explore';
+// Components
+import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 
+// Utils & Services
 // dom also includes Element polyfills
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
-import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
+
+// Types
+import { LokiQuery } from '../types';
+import { TypeaheadOutput } from 'app/types/explore';
 
 const PRISM_SYNTAX = 'promql';
 
@@ -63,10 +68,10 @@ interface LokiQueryFieldProps {
   error?: string | JSX.Element;
   hint?: any;
   history?: any[];
-  initialQuery?: DataQuery;
+  initialQuery?: LokiQuery;
   onClickHintFix?: (action: any) => void;
   onPressEnter?: () => void;
-  onQueryChange?: (value: DataQuery, override?: boolean) => void;
+  onQueryChange?: (value: LokiQuery, override?: boolean) => void;
 }
 
 interface LokiQueryFieldState {

+ 10 - 4
public/app/plugins/datasource/loki/datasource.test.ts

@@ -1,4 +1,6 @@
 import LokiDatasource from './datasource';
+import { LokiQuery } from './types';
+import { getQueryOptions } from 'test/helpers/getQueryOptions';
 
 describe('LokiDatasource', () => {
   const instanceSettings: any = {
@@ -13,12 +15,13 @@ describe('LokiDatasource', () => {
       replace: a => a,
     };
 
-    const range = { from: 'now-6h', to: 'now' };
-
     test('should use default max lines when no limit given', () => {
       const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
       backendSrvMock.datasourceRequest = jest.fn();
-      ds.query({ range, targets: [{ expr: 'foo' }] });
+      const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
+
+      ds.query(options);
+
       expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
       expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000');
     });
@@ -28,7 +31,10 @@ describe('LokiDatasource', () => {
       const customSettings = { ...instanceSettings, jsonData: customData };
       const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
       backendSrvMock.datasourceRequest = jest.fn();
-      ds.query({ range, targets: [{ expr: 'foo' }] });
+
+      const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
+      ds.query(options);
+
       expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
       expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
     });

+ 12 - 7
public/app/plugins/datasource/loki/datasource.ts

@@ -1,13 +1,18 @@
+// Libraries
 import _ from 'lodash';
 
+// Services & Utils
 import * as dateMath from 'app/core/utils/datemath';
-import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
-import { PluginMeta, DataQuery } from '@grafana/ui/src/types';
 import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
-
 import LanguageProvider from './language_provider';
 import { mergeStreamsToLogs } from './result_transformer';
 import { formatQuery, parseQuery } from './query_utils';
+import { makeSeriesForLogs } from 'app/core/logs_model';
+
+// Types
+import { LogsStream, LogsModel } from 'app/core/logs_model';
+import { PluginMeta, DataQueryOptions } from '@grafana/ui/src/types';
+import { LokiQuery } from './types';
 
 export const DEFAULT_MAX_LINES = 1000;
 
@@ -68,7 +73,7 @@ export default class LokiDatasource {
     };
   }
 
-  query(options): Promise<{ data: LogsStream[] }> {
+  query(options: DataQueryOptions<LokiQuery>): Promise<{ data: LogsStream[] }> {
     const queryTargets = options.targets
       .filter(target => target.expr)
       .map(target => this.prepareQueryTarget(target, options));
@@ -96,7 +101,7 @@ export default class LokiDatasource {
     });
   }
 
-  async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]> {
+  async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise<LokiQuery[]> {
     return this.languageProvider.importQueries(queries, originMeta.id);
   }
 
@@ -109,7 +114,7 @@ export default class LokiDatasource {
     });
   }
 
-  modifyQuery(query: DataQuery, action: any): DataQuery {
+  modifyQuery(query: LokiQuery, action: any): LokiQuery {
     const parsed = parseQuery(query.expr || '');
     let selector = parsed.query;
     switch (action.type) {
@@ -124,7 +129,7 @@ export default class LokiDatasource {
     return { ...query, expr: expression };
   }
 
-  getHighlighterExpression(query: DataQuery): string {
+  getHighlighterExpression(query: LokiQuery): string {
     return parseQuery(query.expr).regexp;
   }
 

+ 11 - 5
public/app/plugins/datasource/loki/language_provider.ts

@@ -1,6 +1,12 @@
+// Libraries
 import _ from 'lodash';
 import moment from 'moment';
 
+// Services & Utils
+import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
+import syntax from './syntax';
+
+// Types
 import {
   CompletionItem,
   CompletionItemGroup,
@@ -9,9 +15,7 @@ import {
   TypeaheadOutput,
   HistoryItem,
 } from 'app/types/explore';
-import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
-import syntax from './syntax';
-import { DataQuery } from '@grafana/ui/src/types';
+import { LokiQuery } from './types';
 
 const DEFAULT_KEYS = ['job', 'namespace'];
 const EMPTY_SELECTOR = '{}';
@@ -20,7 +24,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
 
 const wrapLabel = (label: string) => ({ label });
 
-export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]): CompletionItem {
+type LokiHistoryItem = HistoryItem<LokiQuery>;
+
+export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem {
   const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
   const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label);
   const count = historyForItem.length;
@@ -155,7 +161,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
     return { context, refresher, suggestions };
   }
 
-  async importQueries(queries: DataQuery[], datasourceType: string): Promise<DataQuery[]> {
+  async importQueries(queries: LokiQuery[], datasourceType: string): Promise<LokiQuery[]> {
     if (datasourceType === 'prometheus') {
       return Promise.all(
         queries.map(async query => {

+ 6 - 0
public/app/plugins/datasource/loki/types.ts

@@ -0,0 +1,6 @@
+import { DataQuery } from '@grafana/ui/src/types';
+
+export interface LokiQuery extends DataQuery {
+  expr: string;
+}
+

+ 4 - 4
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -11,7 +11,7 @@ import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
-import { DataQuery } from '@grafana/ui/src/types';
+import { PromQuery } from '../types';
 
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
@@ -88,13 +88,13 @@ interface CascaderOption {
 interface PromQueryFieldProps {
   datasource: any;
   error?: string | JSX.Element;
-  initialQuery: DataQuery;
+  initialQuery: PromQuery;
   hint?: any;
   history?: any[];
   metricsByPrefix?: CascaderOption[];
   onClickHintFix?: (action: any) => void;
   onPressEnter?: () => void;
-  onQueryChange?: (value: DataQuery, override?: boolean) => void;
+  onQueryChange?: (value: PromQuery, override?: boolean) => void;
 }
 
 interface PromQueryFieldState {
@@ -166,7 +166,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     // Send text change to parent
     const { initialQuery, onQueryChange } = this.props;
     if (onQueryChange) {
-      const query: DataQuery = {
+      const query: PromQuery = {
         ...initialQuery,
         expr: value,
       };

+ 49 - 45
public/app/plugins/datasource/prometheus/datasource.ts

@@ -1,57 +1,24 @@
+// Libraries
 import _ from 'lodash';
-
 import $ from 'jquery';
+
+// Services & Utils
 import kbn from 'app/core/utils/kbn';
 import * as dateMath from 'app/core/utils/datemath';
 import PrometheusMetricFindQuery from './metric_find_query';
 import { ResultTransformer } from './result_transformer';
 import PrometheusLanguageProvider from './language_provider';
 import { BackendSrv } from 'app/core/services/backend_srv';
-
 import addLabelToQuery from './add_label_to_query';
 import { getQueryHints } from './query_hints';
 import { expandRecordingRules } from './language_utils';
-import { DataQuery } from '@grafana/ui/src/types';
-import { ExploreUrlState } from 'app/types/explore';
-
-export function alignRange(start, end, step) {
-  const alignedEnd = Math.ceil(end / step) * step;
-  const alignedStart = Math.floor(start / step) * step;
-  return {
-    end: alignedEnd,
-    start: alignedStart,
-  };
-}
 
-export function extractRuleMappingFromGroups(groups: any[]) {
-  return groups.reduce(
-    (mapping, group) =>
-      group.rules.filter(rule => rule.type === 'recording').reduce(
-        (acc, rule) => ({
-          ...acc,
-          [rule.name]: rule.query,
-        }),
-        mapping
-      ),
-    {}
-  );
-}
-
-export function prometheusRegularEscape(value) {
-  if (typeof value === 'string') {
-    return value.replace(/'/g, "\\\\'");
-  }
-  return value;
-}
-
-export function prometheusSpecialRegexEscape(value) {
-  if (typeof value === 'string') {
-    return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
-  }
-  return value;
-}
+// Types
+import { PromQuery } from './types';
+import { DataQueryOptions, DataSourceApi } from '@grafana/ui/src/types';
+import { ExploreUrlState } from 'app/types/explore';
 
-export class PrometheusDatasource {
+export class PrometheusDatasource implements DataSourceApi<PromQuery> {
   type: string;
   editorSrc: string;
   name: string;
@@ -149,7 +116,7 @@ export class PrometheusDatasource {
     return this.templateSrv.variableExists(target.expr);
   }
 
-  query(options) {
+  query(options: DataQueryOptions<PromQuery>) {
     const start = this.getPrometheusTime(options.range.from, false);
     const end = this.getPrometheusTime(options.range.to, true);
 
@@ -423,7 +390,7 @@ export class PrometheusDatasource {
     });
   }
 
-  getExploreState(queries: DataQuery[]): Partial<ExploreUrlState> {
+  getExploreState(queries: PromQuery[]): Partial<ExploreUrlState> {
     let state: Partial<ExploreUrlState> = { datasource: this.name };
     if (queries && queries.length > 0) {
       const expandedQueries = queries.map(query => ({
@@ -438,7 +405,7 @@ export class PrometheusDatasource {
     return state;
   }
 
-  getQueryHints(query: DataQuery, result: any[]) {
+  getQueryHints(query: PromQuery, result: any[]) {
     return getQueryHints(query.expr || '', result, this);
   }
 
@@ -457,7 +424,7 @@ export class PrometheusDatasource {
       });
   }
 
-  modifyQuery(query: DataQuery, action: any): DataQuery {
+  modifyQuery(query: PromQuery, action: any): PromQuery {
     let expression = query.expr || '';
     switch (action.type) {
       case 'ADD_FILTER': {
@@ -507,3 +474,40 @@ export class PrometheusDatasource {
     return this.resultTransformer.getOriginalMetricName(labelData);
   }
 }
+
+export function alignRange(start, end, step) {
+  const alignedEnd = Math.ceil(end / step) * step;
+  const alignedStart = Math.floor(start / step) * step;
+  return {
+    end: alignedEnd,
+    start: alignedStart,
+  };
+}
+
+export function extractRuleMappingFromGroups(groups: any[]) {
+  return groups.reduce(
+    (mapping, group) =>
+      group.rules.filter(rule => rule.type === 'recording').reduce(
+        (acc, rule) => ({
+          ...acc,
+          [rule.name]: rule.query,
+        }),
+        mapping
+      ),
+    {}
+  );
+}
+
+export function prometheusRegularEscape(value) {
+  if (typeof value === 'string') {
+    return value.replace(/'/g, "\\\\'");
+  }
+  return value;
+}
+
+export function prometheusSpecialRegexEscape(value) {
+  if (typeof value === 'string') {
+    return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
+  }
+  return value;
+}

+ 6 - 0
public/app/plugins/datasource/prometheus/types.ts

@@ -0,0 +1,6 @@
+import { DataQuery } from '@grafana/ui/src/types';
+
+export interface PromQuery extends DataQuery {
+  expr: string;
+}
+

+ 8 - 8
public/app/plugins/datasource/testdata/QueryEditor.tsx

@@ -10,18 +10,17 @@ import { FormLabel, Select, SelectOptionItem } from '@grafana/ui';
 
 // Types
 import { QueryEditorProps } from '@grafana/ui/src/types';
-
-interface Scenario {
-  id: string;
-  name: string;
-}
+import { TestDataDatasource } from './datasource';
+import { TestDataQuery, Scenario } from './types';
 
 interface State {
   scenarioList: Scenario[];
   current: Scenario | null;
 }
 
-export class QueryEditor extends PureComponent<QueryEditorProps> {
+type Props = QueryEditorProps<TestDataDatasource, TestDataQuery>;
+
+export class QueryEditor extends PureComponent<Props> {
   backendSrv: BackendSrv = getBackendSrv();
 
   state: State = {
@@ -30,11 +29,12 @@ export class QueryEditor extends PureComponent<QueryEditorProps> {
   };
 
   async componentDidMount() {
-    const { query } = this.props;
+    const { query, datasource } = this.props;
 
     query.scenarioId = query.scenarioId || 'random_walk';
 
-    const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios');
+    // const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios');
+    const scenarioList = await datasource.getScenarios();
     const current = _.find(scenarioList, { id: query.scenarioId });
 
     this.setState({ scenarioList: scenarioList, current: current });

+ 9 - 4
public/app/plugins/datasource/testdata/datasource.ts

@@ -1,15 +1,17 @@
 import _ from 'lodash';
 import TableModel from 'app/core/table_model';
+import { DataSourceApi, DataQueryOptions } from '@grafana/ui';
+import { TestDataQuery, Scenario } from './types';
 
-class TestDataDatasource {
-  id: any;
+export class TestDataDatasource implements DataSourceApi<TestDataQuery> {
+  id: number;
 
   /** @ngInject */
   constructor(instanceSettings, private backendSrv, private $q) {
     this.id = instanceSettings.id;
   }
 
-  query(options) {
+  query(options: DataQueryOptions<TestDataQuery>) {
     const queries = _.filter(options.targets, item => {
       return item.hide !== true;
     }).map(item => {
@@ -91,6 +93,9 @@ class TestDataDatasource {
       message: 'Data source is working',
     });
   }
+
+  getScenarios(): Promise<Scenario[]> {
+    return this.backendSrv.get('/api/tsdb/testdata/scenarios');
+  }
 }
 
-export { TestDataDatasource };

+ 11 - 0
public/app/plugins/datasource/testdata/types.ts

@@ -0,0 +1,11 @@
+import { DataQuery } from '@grafana/ui/src/types';
+
+export interface TestDataQuery extends DataQuery {
+  scenarioId: string;
+}
+
+export interface Scenario {
+  id: string;
+  name: string;
+}
+

+ 14 - 5
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -1,22 +1,30 @@
+// Libraries
 import React, { PureComponent } from 'react';
-import { PanelProps, NullValueMode } from '@grafana/ui';
 
-import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
-import Gauge from 'app/viz/Gauge';
+// Services & Utils
+import { contextSrv } from 'app/core/core';
+import { processTimeSeries } from '@grafana/ui';
+
+// Components
+import { Gauge } from '@grafana/ui';
+
+// Types
 import { GaugeOptions } from './types';
+import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
 
 interface Props extends PanelProps<GaugeOptions> {}
 
 export class GaugePanel extends PureComponent<Props> {
+
   render() {
     const { timeSeries, width, height, onInterpolate, options } = this.props;
 
     const prefix = onInterpolate(options.prefix);
     const suffix = onInterpolate(options.suffix);
 
-    const vmSeries = getTimeSeriesVMs({
+    const vmSeries = processTimeSeries({
       timeSeries: timeSeries,
-      nullValueMode: NullValueMode.Ignore,
+      nullValueMode: NullValueMode.Null,
     });
 
     return (
@@ -27,6 +35,7 @@ export class GaugePanel extends PureComponent<Props> {
         height={height}
         prefix={prefix}
         suffix={suffix}
+        theme={contextSrv.getTheme()}
       />
     );
   }

+ 0 - 2
public/app/plugins/panel/gauge/GaugePanelOptions.tsx

@@ -1,6 +1,5 @@
 import React, { PureComponent } from 'react';
 import {
-  BasicGaugeColor,
   PanelOptionsProps,
   ThresholdsEditor,
   Threshold,
@@ -15,7 +14,6 @@ import { GaugeOptions } from './types';
 
 export const defaultProps = {
   options: {
-    baseColor: BasicGaugeColor.Green,
     minValue: 0,
     maxValue: 100,
     prefix: '',

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

@@ -1,7 +1,6 @@
 import { Threshold, ValueMapping } from '@grafana/ui';
 
 export interface GaugeOptions {
-  baseColor: string;
   decimals: number;
   valueMappings: ValueMapping[];
   maxValue: number;

+ 0 - 2
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -1,7 +1,6 @@
 // Libraries
 import _ from 'lodash';
 import React, { PureComponent } from 'react';
-import { colors } from '@grafana/ui';
 
 // Utils
 import { processTimeSeries } from '@grafana/ui/src/utils';
@@ -23,7 +22,6 @@ export class GraphPanel extends PureComponent<Props> {
     const vmSeries = processTimeSeries({
       timeSeries: timeSeries,
       nullValueMode: NullValueMode.Ignore,
-      colorPalette: colors,
     });
 
     return (

+ 2 - 2
public/app/types/explore.ts

@@ -243,9 +243,9 @@ export interface ExploreUrlState {
   range: RawTimeRange;
 }
 
-export interface HistoryItem {
+export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
   ts: number;
-  query: DataQuery;
+  query: TQuery;
 }
 
 export abstract class LanguageProvider {

+ 0 - 55
public/app/viz/Gauge.test.tsx

@@ -1,55 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui';
-
-import { Gauge, Props } from './Gauge';
-
-jest.mock('jquery', () => ({
-  plot: jest.fn(),
-}));
-
-const setup = (propOverrides?: object) => {
-  const props: Props = {
-    baseColor: BasicGaugeColor.Green,
-    maxValue: 100,
-    valueMappings: [],
-    minValue: 0,
-    prefix: '',
-    showThresholdMarkers: true,
-    showThresholdLabels: false,
-    suffix: '',
-    thresholds: [],
-    unit: 'none',
-    stat: 'avg',
-    height: 300,
-    width: 300,
-    timeSeries: {} as TimeSeriesVMs,
-    decimals: 0,
-  };
-
-  Object.assign(props, propOverrides);
-
-  const wrapper = shallow(<Gauge {...props} />);
-  const instance = wrapper.instance() as Gauge;
-
-  return {
-    instance,
-    wrapper,
-  };
-};
-
-describe('Get font color', () => {
-  it('should get base color if no threshold', () => {
-    const { instance } = setup();
-
-    expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green);
-  });
-
-  it('should be f2f2f2', () => {
-    const { instance } = setup({
-      thresholds: [{ value: 59, color: '#f2f2f2' }],
-    });
-
-    expect(instance.getFontColor(58)).toEqual('#f2f2f2');
-  });
-});

+ 0 - 216
public/app/viz/Gauge.tsx

@@ -1,216 +0,0 @@
-import React, { PureComponent } from 'react';
-import $ from 'jquery';
-import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
-
-import config from '../core/config';
-import kbn from '../core/utils/kbn';
-
-export interface Props {
-  baseColor: string;
-  decimals: number;
-  height: number;
-  valueMappings: ValueMapping[];
-  maxValue: number;
-  minValue: number;
-  prefix: string;
-  timeSeries: TimeSeriesVMs;
-  thresholds: Threshold[];
-  showThresholdMarkers: boolean;
-  showThresholdLabels: boolean;
-  stat: string;
-  suffix: string;
-  unit: string;
-  width: number;
-}
-
-export class Gauge extends PureComponent<Props> {
-  canvasElement: any;
-
-  static defaultProps = {
-    baseColor: BasicGaugeColor.Green,
-    maxValue: 100,
-    valueMappings: [],
-    minValue: 0,
-    prefix: '',
-    showThresholdMarkers: true,
-    showThresholdLabels: false,
-    suffix: '',
-    thresholds: [],
-    unit: 'none',
-    stat: 'avg',
-  };
-
-  componentDidMount() {
-    this.draw();
-  }
-
-  componentDidUpdate() {
-    this.draw();
-  }
-
-  formatWithMappings(mappings, value) {
-    const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
-    const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
-
-    const valueMap = valueMaps.map(mapping => {
-      if (mapping.value && value === mapping.value) {
-        return mapping.text;
-      }
-    })[0];
-
-    const rangeMap = rangeMaps.map(mapping => {
-      if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
-        return mapping.text;
-      }
-    })[0];
-
-    return { rangeMap, valueMap };
-  }
-
-  formatValue(value) {
-    const { decimals, valueMappings, prefix, suffix, unit } = this.props;
-
-    const formatFunc = kbn.valueFormats[unit];
-    const formattedValue = formatFunc(value, decimals);
-
-    if (valueMappings.length > 0) {
-      const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
-
-      if (valueMap) {
-        return `${prefix} ${valueMap} ${suffix}`;
-      } else if (rangeMap) {
-        return `${prefix} ${rangeMap} ${suffix}`;
-      }
-    }
-
-    if (isNaN(value)) {
-      return '-';
-    }
-
-    return `${prefix} ${formattedValue} ${suffix}`;
-  }
-
-  getFontColor(value) {
-    const { baseColor, maxValue, thresholds } = this.props;
-
-    if (thresholds.length > 0) {
-      const atThreshold = thresholds.filter(threshold => value <= threshold.value);
-
-      if (atThreshold.length > 0) {
-        return atThreshold[0].color;
-      } else if (value <= maxValue) {
-        return BasicGaugeColor.Red;
-      }
-    }
-
-    return baseColor;
-  }
-
-  draw() {
-    const {
-      baseColor,
-      maxValue,
-      minValue,
-      timeSeries,
-      showThresholdLabels,
-      showThresholdMarkers,
-      thresholds,
-      width,
-      height,
-      stat,
-    } = this.props;
-
-    let value: string | number = '';
-
-    if (timeSeries[0]) {
-      value = timeSeries[0].stats[stat];
-    } else {
-      value = 'N/A';
-    }
-
-    const dimension = Math.min(width, height * 1.3);
-    const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
-    const fontScale = parseInt('80', 10) / 100;
-    const fontSize = Math.min(dimension / 5, 100) * fontScale;
-    const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
-    const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
-    const thresholdMarkersWidth = gaugeWidth / 5;
-    const thresholdLabelFontSize = fontSize / 2.5;
-
-    const formattedThresholds = [
-      { value: minValue, color: BasicGaugeColor.Green },
-      ...thresholds.map((threshold, index) => {
-        return {
-          value: threshold.value,
-          color: index === 0 ? threshold.color : thresholds[index].color,
-        };
-      }),
-      { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
-    ];
-
-    const options = {
-      series: {
-        gauges: {
-          gauge: {
-            min: minValue,
-            max: maxValue,
-            background: { color: backgroundColor },
-            border: { color: null },
-            shadow: { show: false },
-            width: gaugeWidth,
-          },
-          frame: { show: false },
-          label: { show: false },
-          layout: { margin: 0, thresholdWidth: 0 },
-          cell: { border: { width: 0 } },
-          threshold: {
-            values: formattedThresholds,
-            label: {
-              show: showThresholdLabels,
-              margin: thresholdMarkersWidth + 1,
-              font: { size: thresholdLabelFontSize },
-            },
-            show: showThresholdMarkers,
-            width: thresholdMarkersWidth,
-          },
-          value: {
-            color: this.getFontColor(value),
-            formatter: () => {
-              return this.formatValue(value);
-            },
-            font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
-          },
-          show: true,
-        },
-      },
-    };
-
-    const plotSeries = { data: [[0, value]] };
-
-    try {
-      $.plot(this.canvasElement, [plotSeries], options);
-    } catch (err) {
-      console.log('Gauge rendering error', err, options, timeSeries);
-    }
-  }
-
-  render() {
-    const { height, width } = this.props;
-
-    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>
-    );
-  }
-}
-
-export default Gauge;

+ 0 - 168
public/app/viz/state/timeSeries.ts

@@ -1,168 +0,0 @@
-// Libraries
-import _ from 'lodash';
-
-// Utils
-import { colors } from '@grafana/ui';
-
-// Types
-import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';
-
-interface Options {
-  timeSeries: TimeSeries[];
-  nullValueMode: NullValueMode;
-}
-
-export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
-  const vmSeries = timeSeries.map((item, index) => {
-    const colorIndex = index % colors.length;
-    const label = item.target;
-    const result = [];
-
-    // stat defaults
-    let total = 0;
-    let max = -Number.MAX_VALUE;
-    let min = Number.MAX_VALUE;
-    let logmin = Number.MAX_VALUE;
-    let avg = null;
-    let current = null;
-    let first = null;
-    let delta = 0;
-    let diff = null;
-    let range = null;
-    let timeStep = Number.MAX_VALUE;
-    let allIsNull = true;
-    let allIsZero = true;
-
-    const ignoreNulls = nullValueMode === NullValueMode.Ignore;
-    const nullAsZero = nullValueMode === NullValueMode.AsZero;
-
-    let currentTime;
-    let currentValue;
-    let nonNulls = 0;
-    let previousTime;
-    let previousValue = 0;
-    let previousDeltaUp = true;
-
-    for (let i = 0; i < item.datapoints.length; i++) {
-      currentValue = item.datapoints[i][0];
-      currentTime = item.datapoints[i][1];
-
-      // Due to missing values we could have different timeStep all along the series
-      // so we have to find the minimum one (could occur with aggregators such as ZimSum)
-      if (previousTime !== undefined) {
-        const currentStep = currentTime - previousTime;
-        if (currentStep < timeStep) {
-          timeStep = currentStep;
-        }
-      }
-
-      previousTime = currentTime;
-
-      if (currentValue === null) {
-        if (ignoreNulls) {
-          continue;
-        }
-        if (nullAsZero) {
-          currentValue = 0;
-        }
-      }
-
-      if (currentValue !== null) {
-        if (_.isNumber(currentValue)) {
-          total += currentValue;
-          allIsNull = false;
-          nonNulls++;
-        }
-
-        if (currentValue > max) {
-          max = currentValue;
-        }
-
-        if (currentValue < min) {
-          min = currentValue;
-        }
-
-        if (first === null) {
-          first = currentValue;
-        } else {
-          if (previousValue > currentValue) {
-            // counter reset
-            previousDeltaUp = false;
-            if (i === item.datapoints.length - 1) {
-              // reset on last
-              delta += currentValue;
-            }
-          } else {
-            if (previousDeltaUp) {
-              delta += currentValue - previousValue; // normal increment
-            } else {
-              delta += currentValue; // account for counter reset
-            }
-            previousDeltaUp = true;
-          }
-        }
-        previousValue = currentValue;
-
-        if (currentValue < logmin && currentValue > 0) {
-          logmin = currentValue;
-        }
-
-        if (currentValue !== 0) {
-          allIsZero = false;
-        }
-      }
-
-      result.push([currentTime, currentValue]);
-    }
-
-    if (max === -Number.MAX_VALUE) {
-      max = null;
-    }
-
-    if (min === Number.MAX_VALUE) {
-      min = null;
-    }
-
-    if (result.length && !allIsNull) {
-      avg = total / nonNulls;
-      current = result[result.length - 1][1];
-      if (current === null && result.length > 1) {
-        current = result[result.length - 2][1];
-      }
-    }
-
-    if (max !== null && min !== null) {
-      range = max - min;
-    }
-
-    if (current !== null && first !== null) {
-      diff = current - first;
-    }
-
-    const count = result.length;
-
-    return {
-      data: result,
-      label: label,
-      color: colors[colorIndex],
-      stats: {
-        total,
-        min,
-        max,
-        current,
-        logmin,
-        avg,
-        diff,
-        delta,
-        timeStep,
-        range,
-        count,
-        first,
-        allIsZero,
-        allIsNull,
-      },
-    };
-  });
-
-  return vmSeries;
-}

+ 2 - 66
public/sass/components/_query_editor.scss

@@ -31,48 +31,6 @@
   }
 }
 
-.gf-form-query-content {
-  flex-grow: 2;
-
-  &--collapsed {
-    overflow: hidden;
-
-    .gf-form-label {
-      overflow: hidden;
-      text-overflow: ellipsis;
-      width: 100%;
-      white-space: nowrap;
-    }
-  }
-}
-
-.gf-form-query-letter-cell {
-  flex-shrink: 0;
-
-  .gf-form-query-letter-cell-carret {
-    display: inline-block;
-    width: 0.7rem;
-    position: relative;
-    left: -2px;
-  }
-  .gf-form-query-letter-cell-letter {
-    font-weight: bold;
-    color: $blue;
-  }
-  .gf-form-query-letter-cell-ds {
-    color: $text-color-weak;
-  }
-}
-
-.gf-query-ds-label {
-  text-align: center;
-  width: 44px;
-}
-
-.grafana-metric-options {
-  margin-top: 25px;
-}
-
 .tight-form-func {
   background: $tight-form-func-bg;
 
@@ -124,28 +82,6 @@ input[type='text'].tight-form-func-param {
   }
 }
 
-.query-troubleshooter {
-  font-size: $font-size-sm;
-  margin: $gf-form-margin;
-  border: 1px solid $btn-secondary-bg;
-  min-height: 100px;
-  border-radius: 3px;
-}
-
-.query-troubleshooter__header {
-  float: right;
-  font-size: $font-size-sm;
-  text-align: right;
-  padding: $input-padding-y $input-padding-x;
-  a {
-    margin-left: $spacer;
-  }
-}
-
-.query-troubleshooter__body {
-  padding: $spacer 0;
-}
-
 .rst-text::before {
   content: ' ';
 }
@@ -202,8 +138,8 @@ input[type='text'].tight-form-func-param {
   background: $page-bg;
   flex-wrap: nowrap;
   align-items: center;
-}
 
+}
 .query-editor-row__ref-id {
   font-weight: $font-weight-semi-bold;
   color: $blue;
@@ -256,7 +192,7 @@ input[type='text'].tight-form-func-param {
 }
 
 .query-editor-row__body {
-  margin: 0 0 10px 40px;
+  margin: 2px 0 10px 40px;
   background: $page-bg;
 
   &--collapsed {

+ 6 - 0
public/sass/components/_toolbar.scss

@@ -16,6 +16,12 @@
   padding-right: 20px;
 }
 
+.toolbar__left {
+  display: flex;
+  flex-grow: 1;
+  align-items: center;
+}
+
 .toolbar__main {
   padding: 0 $input-padding-x;
   font-size: $font-size-md;

+ 25 - 0
public/test/helpers/getQueryOptions.ts

@@ -0,0 +1,25 @@
+import { DataQueryOptions, DataQuery } from '@grafana/ui';
+import moment from 'moment';
+
+
+export function getQueryOptions<TQuery extends DataQuery>(options: Partial<DataQueryOptions<TQuery>>): DataQueryOptions<TQuery> {
+  const raw = {from: 'now', to: 'now-1h'};
+  const range = { from: moment(), to: moment(), raw: raw};
+
+  const defaults: DataQueryOptions<TQuery> = {
+    range: range,
+    rangeRaw: raw,
+    targets: [],
+    scopedVars: {},
+    timezone: 'browser',
+    panelId: 1,
+    dashboardId: 1,
+    interval: '60s',
+    intervalMs: 60000,
+    maxDataPoints: 500,
+  };
+
+  Object.assign(defaults, options);
+
+  return defaults;
+}