فهرست منبع

Merge branch 'master' into tooling/storybook-poc

Dominik Prokop 7 سال پیش
والد
کامیت
c8ec35ce1f
100فایلهای تغییر یافته به همراه3611 افزوده شده و 891 حذف شده
  1. 8 8
      .circleci/config.yml
  2. 1 0
      CHANGELOG.md
  3. 1250 0
      devenv/dev-dashboards/panel_tests_gauge.json
  4. 1 1
      docs/sources/auth/gitlab.md
  5. 1 1
      docs/sources/features/datasources/cloudwatch.md
  6. 38 4
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  7. 2 6
      packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  8. 24 0
      packages/grafana-ui/src/components/FormField/FormField.test.tsx
  9. 25 0
      packages/grafana-ui/src/components/FormField/FormField.tsx
  10. 12 0
      packages/grafana-ui/src/components/FormField/_FormField.scss
  11. 19 0
      packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
  12. 42 0
      packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
  13. 224 0
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  14. 284 0
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  15. 0 23
      packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx
  16. 1 1
      packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
  17. 3 3
      packages/grafana-ui/src/components/Select/Select.tsx
  18. 1 0
      packages/grafana-ui/src/components/Select/_Select.scss
  19. 126 16
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
  20. 101 112
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  21. 69 67
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  22. 48 53
      packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx
  23. 14 16
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx
  24. 29 23
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx
  25. 0 0
      packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss
  26. 6 6
      packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap
  27. 2 1
      packages/grafana-ui/src/components/index.scss
  28. 6 1
      packages/grafana-ui/src/components/index.ts
  29. 89 0
      packages/grafana-ui/src/types/datasource.ts
  30. 0 16
      packages/grafana-ui/src/types/gauge.ts
  31. 2 1
      packages/grafana-ui/src/types/index.ts
  32. 12 0
      packages/grafana-ui/src/types/panel.ts
  33. 118 0
      packages/grafana-ui/src/types/plugin.ts
  34. 3 2
      packages/grafana-ui/src/types/series.ts
  35. 9 8
      packages/grafana-ui/src/utils/processTimeSeries.ts
  36. 5 2
      pkg/components/imguploader/imguploader.go
  37. 2 20
      pkg/log/log.go
  38. 5 1
      pkg/login/ext_user.go
  39. 1 1
      pkg/services/alerting/notifiers/telegram.go
  40. 5 2
      pkg/services/alerting/test_notification.go
  41. 2 2
      public/app/core/components/Animations/FadeIn.tsx
  42. 50 0
      public/app/core/components/Footer/Footer.tsx
  43. 0 25
      public/app/core/components/Label/Label.tsx
  44. 2 2
      public/app/core/components/LayoutSelector/LayoutSelector.tsx
  45. 75 0
      public/app/core/components/Page/Page.tsx
  46. 26 0
      public/app/core/components/Page/PageContents.tsx
  47. 3 3
      public/app/core/components/PageHeader/PageHeader.tsx
  48. 3 3
      public/app/core/components/PageLoader/PageLoader.tsx
  49. 1 1
      public/app/core/components/Select/DataSourcePicker.tsx
  50. 3 4
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  51. 2 2
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  52. 2 2
      public/app/core/components/sidemenu/DropDownChild.tsx
  53. 2 2
      public/app/core/components/sidemenu/SideMenuDropDown.tsx
  54. 2 2
      public/app/core/components/sidemenu/SignIn.tsx
  55. 2 2
      public/app/core/components/sidemenu/TopSection.tsx
  56. 2 2
      public/app/core/components/sidemenu/TopSectionItem.tsx
  57. 2 0
      public/app/core/config.ts
  58. 1 0
      public/app/core/core.ts
  59. 35 0
      public/app/core/directives/autofill_event_fix.ts
  60. 20 9
      public/app/core/directives/dropdown_typeahead.ts
  61. 4 0
      public/app/core/selectors/navModel.ts
  62. 5 0
      public/app/core/services/context_srv.ts
  63. 22 50
      public/app/core/utils/explore.test.ts
  64. 95 25
      public/app/core/utils/explore.ts
  65. 8 1
      public/app/features/api-keys/ApiKeysPage.test.tsx
  66. 12 14
      public/app/features/api-keys/ApiKeysPage.tsx
  67. 130 110
      public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap
  68. 1 2
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  69. 7 1
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  70. 5 2
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  71. 2 2
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx
  72. 1 1
      public/app/features/dashboard/dashgrid/PanelResizer.tsx
  73. 2 2
      public/app/features/dashboard/panel_editor/DataSourceOption.tsx
  74. 9 10
      public/app/features/dashboard/panel_editor/EditorTabBody.tsx
  75. 54 86
      public/app/features/dashboard/panel_editor/QueriesTab.tsx
  76. 254 0
      public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
  77. 1 15
      public/app/features/dashboard/panel_editor/QueryInspector.tsx
  78. 4 3
      public/app/features/dashboard/panel_editor/QueryOptions.tsx
  79. 11 3
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  80. 5 5
      public/app/features/dashboard/panel_model.ts
  81. 2 2
      public/app/features/datasources/DashboardsTable.tsx
  82. 3 2
      public/app/features/datasources/DataSourceDashboards.test.tsx
  83. 10 2
      public/app/features/datasources/DataSourceDashboards.tsx
  84. 7 2
      public/app/features/datasources/DataSourcesList.tsx
  85. 2 2
      public/app/features/datasources/DataSourcesListItem.tsx
  86. 11 3
      public/app/features/datasources/DataSourcesListPage.test.tsx
  87. 36 28
      public/app/features/datasources/DataSourcesListPage.tsx
  88. 3 3
      public/app/features/datasources/__mocks__/dataSourcesMocks.ts
  89. 31 19
      public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap
  90. 5 5
      public/app/features/datasources/settings/BasicSettings.tsx
  91. 2 2
      public/app/features/datasources/settings/ButtonRow.tsx
  92. 5 4
      public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx
  93. 12 6
      public/app/features/datasources/settings/DataSourceSettingsPage.tsx
  94. 7 6
      public/app/features/datasources/settings/PluginSettings.tsx
  95. 0 0
      public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap
  96. 7 6
      public/app/features/datasources/state/actions.ts
  97. 3 2
      public/app/features/datasources/state/navModel.ts
  98. 5 4
      public/app/features/datasources/state/reducers.ts
  99. 3 3
      public/app/features/datasources/state/selectors.ts
  100. 2 2
      public/app/features/explore/Error.tsx

+ 8 - 8
.circleci/config.yml

@@ -323,7 +323,7 @@ jobs:
 
 
   deploy-enterprise-master:
   deploy-enterprise-master:
     docker:
     docker:
-      - image: grafana/grafana-ci-deploy:1.1.0
+      - image: grafana/grafana-ci-deploy:1.2.0
     steps:
     steps:
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
@@ -346,7 +346,7 @@ jobs:
 
 
   deploy-enterprise-release:
   deploy-enterprise-release:
     docker:
     docker:
-    - image: grafana/grafana-ci-deploy:1.1.0
+    - image: grafana/grafana-ci-deploy:1.2.0
     steps:
     steps:
       - attach_workspace:
       - attach_workspace:
          at: .
          at: .
@@ -370,15 +370,15 @@ jobs:
           command: './scripts/build/load-signing-key.sh'
           command: './scripts/build/load-signing-key.sh'
       - run:
       - run:
           name: Update Debian repository
           name: Update Debian repository
-          command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+          command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"'
       - run:
       - run:
           name: Update RPM repository
           name: Update RPM repository
-          command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+          command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"'
 
 
 
 
   deploy-master:
   deploy-master:
     docker:
     docker:
-      - image: grafana/grafana-ci-deploy:1.1.0
+      - image: grafana/grafana-ci-deploy:1.2.0
     steps:
     steps:
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
@@ -408,7 +408,7 @@ jobs:
 
 
   deploy-release:
   deploy-release:
     docker:
     docker:
-      - image: grafana/grafana-ci-deploy:1.1.0
+      - image: grafana/grafana-ci-deploy:1.2.0
     steps:
     steps:
       - checkout
       - checkout
       - attach_workspace:
       - attach_workspace:
@@ -433,10 +433,10 @@ jobs:
           command: './scripts/build/load-signing-key.sh'
           command: './scripts/build/load-signing-key.sh'
       - run:
       - run:
           name: Update Debian repository
           name: Update Debian repository
-          command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+          command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"'
       - run:
       - run:
           name: Update RPM repository
           name: Update RPM repository
-          command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+          command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"'
 
 
 workflows:
 workflows:
   version: 2
   version: 2

+ 1 - 0
CHANGELOG.md

@@ -23,6 +23,7 @@
 
 
 ### Bug fixes
 ### Bug fixes
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
+* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
 
 
 # 5.4.3 (2019-01-14)
 # 5.4.3 (2019-01-14)
 
 

+ 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
 ```bash
 [auth.gitlab]
 [auth.gitlab]
-enabled = false
+enabled = true
 allow_sign_up = false
 allow_sign_up = false
 client_id = GITLAB_APPLICATION_ID
 client_id = GITLAB_APPLICATION_ID
 client_secret = GITLAB_SECRET
 client_secret = GITLAB_SECRET

+ 1 - 1
docs/sources/features/datasources/cloudwatch.md

@@ -38,7 +38,7 @@ Name | Description
 
 
 ### IAM Roles
 ### IAM Roles
 
 
-Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If you grafana
+Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If your Grafana
 server is running on AWS you can use IAM Roles and authentication will be handled automatically.
 server is running on AWS you can use IAM Roles and authentication will be handled automatically.
 
 
 Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
 Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)

+ 38 - 4
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -1,4 +1,5 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
+import _ from 'lodash';
 import Scrollbars from 'react-custom-scrollbars';
 import Scrollbars from 'react-custom-scrollbars';
 
 
 interface Props {
 interface Props {
@@ -6,7 +7,11 @@ interface Props {
   autoHide?: boolean;
   autoHide?: boolean;
   autoHideTimeout?: number;
   autoHideTimeout?: number;
   autoHideDuration?: number;
   autoHideDuration?: number;
+  autoMaxHeight?: string;
   hideTracksWhenNotNeeded?: boolean;
   hideTracksWhenNotNeeded?: boolean;
+  scrollTop?: number;
+  setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
+  autoHeightMin?: number | string;
 }
 }
 
 
 /**
 /**
@@ -18,26 +23,55 @@ export class CustomScrollbar extends PureComponent<Props> {
     autoHide: true,
     autoHide: true,
     autoHideTimeout: 200,
     autoHideTimeout: 200,
     autoHideDuration: 200,
     autoHideDuration: 200,
+    autoMaxHeight: '100%',
     hideTracksWhenNotNeeded: false,
     hideTracksWhenNotNeeded: false,
+    setScrollTop: () => {},
+    autoHeightMin: '0'
   };
   };
 
 
+  private ref: React.RefObject<Scrollbars>;
+
+  constructor(props: Props) {
+    super(props);
+    this.ref = React.createRef<Scrollbars>();
+  }
+
+  updateScroll() {
+    const ref = this.ref.current;
+
+    if (ref && !_.isNil(this.props.scrollTop)) {
+      if (this.props.scrollTop > 10000) {
+        ref.scrollToBottom();
+      } else {
+        ref.scrollTop(this.props.scrollTop);
+      }
+   }
+  }
+
+  componentDidMount() {
+    this.updateScroll();
+  }
+
+  componentDidUpdate() {
+    this.updateScroll();
+  }
+
   render() {
   render() {
-    const { customClassName, children, ...scrollProps } = this.props;
+    const { customClassName, children, autoMaxHeight } = this.props;
 
 
     return (
     return (
       <Scrollbars
       <Scrollbars
+        ref={this.ref}
         className={customClassName}
         className={customClassName}
         autoHeight={true}
         autoHeight={true}
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
         // Before these where set to inhert but that caused problems with cut of legends in firefox
         // Before these where set to inhert but that caused problems with cut of legends in firefox
-        autoHeightMin={'0'}
-        autoHeightMax={'100%'}
+        autoHeightMax={autoMaxHeight}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
         renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
         renderView={props => <div {...props} className="view" />}
         renderView={props => <div {...props} className="view" />}
-        {...scrollProps}
       >
       >
         {children}
         {children}
       </Scrollbars>
       </Scrollbars>

+ 2 - 6
packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

@@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
     Object {
     Object {
       "height": "auto",
       "height": "auto",
       "maxHeight": "100%",
       "maxHeight": "100%",
-      "minHeight": "0",
+      "minHeight": 0,
       "overflow": "hidden",
       "overflow": "hidden",
       "position": "relative",
       "position": "relative",
       "width": "100%",
       "width": "100%",
@@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
         "marginBottom": 0,
         "marginBottom": 0,
         "marginRight": 0,
         "marginRight": 0,
         "maxHeight": "calc(100% + 0px)",
         "maxHeight": "calc(100% + 0px)",
-        "minHeight": "calc(0 + 0px)",
+        "minHeight": 0,
         "overflow": "scroll",
         "overflow": "scroll",
         "position": "relative",
         "position": "relative",
         "right": undefined,
         "right": undefined,
@@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
       Object {
       Object {
         "display": "none",
         "display": "none",
         "height": 6,
         "height": 6,
-        "opacity": 0,
         "position": "absolute",
         "position": "absolute",
-        "transition": "opacity 200ms",
       }
       }
     }
     }
   >
   >
@@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
     style={
     style={
       Object {
       Object {
         "display": "none",
         "display": "none",
-        "opacity": 0,
         "position": "absolute",
         "position": "absolute",
-        "transition": "opacity 200ms",
         "width": 6,
         "width": 6,
       }
       }
     }
     }

+ 24 - 0
packages/grafana-ui/src/components/FormField/FormField.test.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { FormField, Props } from './FormField';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    label: 'Test',
+    labelWidth: 11,
+    value: 10,
+    onChange: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<FormField {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 25 - 0
packages/grafana-ui/src/components/FormField/FormField.tsx

@@ -0,0 +1,25 @@
+import React, { InputHTMLAttributes, FunctionComponent } from 'react';
+import { FormLabel } from '..';
+
+export interface Props extends InputHTMLAttributes<HTMLInputElement> {
+  label: string;
+  labelWidth?: number;
+  inputWidth?: number;
+}
+
+const defaultProps = {
+  labelWidth: 6,
+  inputWidth: 12,
+};
+
+const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
+  return (
+    <div className="form-field">
+      <FormLabel width={labelWidth}>{label}</FormLabel>
+      <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
+    </div>
+  );
+};
+
+FormField.defaultProps = defaultProps;
+export { FormField };

+ 12 - 0
packages/grafana-ui/src/components/FormField/_FormField.scss

@@ -0,0 +1,12 @@
+.form-field {
+  margin-bottom: $gf-form-margin;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  text-align: left;
+  position: relative;
+
+  &--grow {
+    flex-grow: 1;
+  }
+}

+ 19 - 0
packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap

@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="form-field"
+>
+  <Component
+    width={11}
+  >
+    Test
+  </Component>
+  <input
+    className="gf-form-input width-12"
+    onChange={[MockFunction]}
+    type="text"
+    value={10}
+  />
+</div>
+`;

+ 42 - 0
packages/grafana-ui/src/components/FormLabel/FormLabel.tsx

@@ -0,0 +1,42 @@
+import React, { FunctionComponent, ReactNode } from 'react';
+import classNames from 'classnames';
+import { Tooltip } from '..';
+
+interface Props {
+  children: ReactNode;
+  className?: string;
+  htmlFor?: string;
+  isFocused?: boolean;
+  isInvalid?: boolean;
+  tooltip?: string;
+  width?: number;
+}
+
+export const FormLabel: FunctionComponent<Props> = ({
+  children,
+  isFocused,
+  isInvalid,
+  className,
+  htmlFor,
+  tooltip,
+  width,
+  ...rest
+}) => {
+  const classes = classNames(`gf-form-label width-${width ? width : '10'}`, className, {
+    'gf-form-label--is-focused': isFocused,
+    'gf-form-label--is-invalid': isInvalid,
+  });
+
+  return (
+    <label className={classes} {...rest} htmlFor={htmlFor}>
+      {children}
+      {tooltip && (
+        <Tooltip placement="auto" content={tooltip}>
+          <div className="gf-form-help-icon--right-normal">
+            <i className="gicon gicon-question gicon--has-hover" />
+          </div>
+        </Tooltip>
+      )}
+    </label>
+  );
+};

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

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

@@ -1,23 +0,0 @@
-import React, { SFC, ReactNode } from 'react';
-import classNames from 'classnames';
-
-interface Props {
-  children: ReactNode;
-  htmlFor?: string;
-  className?: string;
-  isFocused?: boolean;
-  isInvalid?: boolean;
-}
-
-export const GfFormLabel: SFC<Props> = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => {
-  const classes = classNames('gf-form-label', className, {
-    'gf-form-label--is-focused': isFocused,
-    'gf-form-label--is-invalid': isInvalid,
-  });
-
-  return (
-    <label className={classes} {...rest} htmlFor={htmlFor}>
-      {children}
-    </label>
-  );
-};

+ 1 - 1
packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss

@@ -6,7 +6,7 @@
 }
 }
 
 
 .panel-options-group__header {
 .panel-options-group__header {
-  padding: 4px 20px;
+  padding: 4px 8px;
   font-size: 1.1rem;
   font-size: 1.1rem;
   background: $panel-options-group-header-bg;
   background: $panel-options-group-header-bg;
   position: relative;
   position: relative;

+ 3 - 3
packages/grafana-ui/src/components/Select/Select.tsx

@@ -16,7 +16,7 @@ import SelectOptionGroup from './SelectOptionGroup';
 import IndicatorsContainer from './IndicatorsContainer';
 import IndicatorsContainer from './IndicatorsContainer';
 import NoOptionsMessage from './NoOptionsMessage';
 import NoOptionsMessage from './NoOptionsMessage';
 import resetSelectStyles from './resetSelectStyles';
 import resetSelectStyles from './resetSelectStyles';
-import { CustomScrollbar } from '@grafana/ui';
+import { CustomScrollbar } from '..';
 
 
 export interface SelectOptionItem {
 export interface SelectOptionItem {
   label?: string;
   label?: string;
@@ -61,7 +61,7 @@ interface AsyncProps {
 export const MenuList = (props: any) => {
 export const MenuList = (props: any) => {
   return (
   return (
     <components.MenuList {...props}>
     <components.MenuList {...props}>
-      <CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
+      <CustomScrollbar autoHide={false} autoMaxHeight="inherit">{props.children}</CustomScrollbar>
     </components.MenuList>
     </components.MenuList>
   );
   );
 };
 };
@@ -202,7 +202,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
         classNamePrefix="gf-form-select-box"
         classNamePrefix="gf-form-select-box"
         className={selectClassNames}
         className={selectClassNames}
         components={{
         components={{
-          Option,
+          Option: SelectOption,
           SingleValue,
           SingleValue,
           IndicatorsContainer,
           IndicatorsContainer,
           NoOptionsMessage,
           NoOptionsMessage,

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

@@ -102,6 +102,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 .gf-form-select-box__value-container {
 .gf-form-select-box__value-container {
   display: table-cell;
   display: table-cell;
   padding: 6px 10px;
   padding: 6px 10px;
+  vertical-align: middle;
   > div {
   > div {
     display: inline-block;
     display: inline-block;
   }
   }

+ 126 - 16
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx

@@ -2,7 +2,6 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 
 
 import { ThresholdsEditor, Props } from './ThresholdsEditor';
 import { ThresholdsEditor, Props } from './ThresholdsEditor';
-import { BasicGaugeColor } from '../../types';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
@@ -15,49 +14,160 @@ const setup = (propOverrides?: object) => {
   return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
   return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
 };
 };
 
 
+describe('Initialization', () => {
+  it('should add a base threshold if missing', () => {
+    const instance = setup();
+
+    expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
+  });
+});
+
 describe('Add threshold', () => {
 describe('Add threshold', () => {
-  it('should add threshold', () => {
+  it('should not add threshold at index 0', () => {
     const instance = setup();
     const instance = setup();
 
 
     instance.onAddThreshold(0);
     instance.onAddThreshold(0);
 
 
-    expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
+    expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
+  });
+
+  it('should add threshold', () => {
+    const instance = setup();
+
+    instance.onAddThreshold(1);
+
+    expect(instance.state.thresholds).toEqual([
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+    ]);
   });
   });
 
 
   it('should add another threshold above a first', () => {
   it('should add another threshold above a first', () => {
     const instance = setup({
     const instance = setup({
-      thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
+      thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
     });
     });
 
 
-    instance.onAddThreshold(1);
+    instance.onAddThreshold(2);
+
+    expect(instance.state.thresholds).toEqual([
+      { index: 2, value: 75, color: '#6ED0E0' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+    ]);
+  });
+
+  it('should add another threshold between first and second index', () => {
+    const instance = setup({
+      thresholds: [
+        { index: 0, value: -Infinity, color: '#7EB26D' },
+        { index: 1, value: 50, color: '#EAB839' },
+        { index: 2, value: 75, color: '#6ED0E0' },
+      ],
+    });
+
+    instance.onAddThreshold(2);
 
 
     expect(instance.state.thresholds).toEqual([
     expect(instance.state.thresholds).toEqual([
-      { index: 1, value: 75, color: 'rgb(170, 95, 61)' },
-      { index: 0, value: 50, color: 'rgb(127, 115, 64)' },
+      { index: 3, value: 75, color: '#6ED0E0' },
+      { index: 2, value: 62.5, color: '#EF843C' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+    ]);
+  });
+});
+
+describe('Remove threshold', () => {
+  it('should not remove threshold at index 0', () => {
+    const thresholds = [
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
+    ];
+    const instance = setup({ thresholds });
+
+    instance.onRemoveThreshold(thresholds[0]);
+
+    expect(instance.state.thresholds).toEqual(thresholds);
+  });
+
+  it('should remove threshold', () => {
+    const thresholds = [
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
+    ];
+    const instance = setup({
+      thresholds,
+    });
+
+    instance.onRemoveThreshold(thresholds[1]);
+
+    expect(instance.state.thresholds).toEqual([
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 75, color: '#6ED0E0' },
     ]);
     ]);
   });
   });
 });
 });
 
 
 describe('change threshold value', () => {
 describe('change threshold value', () => {
-  it('should update value and resort rows', () => {
+  it('should not change threshold at index 0', () => {
+    const thresholds = [
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
+    ];
+    const instance = setup({ thresholds });
+
+    const mockEvent = { target: { value: 12 } };
+
+    instance.onChangeThresholdValue(mockEvent, thresholds[0]);
+
+    expect(instance.state.thresholds).toEqual(thresholds);
+  });
+
+  it('should update value', () => {
     const instance = setup();
     const instance = setup();
-    const mockThresholds = [
-      { index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
-      { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
+    const thresholds = [
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
     ];
     ];
 
 
     instance.state = {
     instance.state = {
-      baseColor: BasicGaugeColor.Green,
-      thresholds: mockThresholds,
+      thresholds,
     };
     };
 
 
     const mockEvent = { target: { value: 78 } };
     const mockEvent = { target: { value: 78 } };
 
 
-    instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
+    instance.onChangeThresholdValue(mockEvent, thresholds[1]);
+
+    expect(instance.state.thresholds).toEqual([
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 78, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
+    ]);
+  });
+});
+
+describe('on blur threshold value', () => {
+  it('should resort rows and update indexes', () => {
+    const instance = setup();
+    const thresholds = [
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 78, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
+    ];
+
+    instance.state = {
+      thresholds,
+    };
+
+    instance.onBlur();
 
 
     expect(instance.state.thresholds).toEqual([
     expect(instance.state.thresholds).toEqual([
-      { index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
-      { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
+      { index: 2, value: 78, color: '#EAB839' },
+      { index: 1, value: 75, color: '#6ED0E0' },
+      { index: 0, value: -Infinity, color: '#7EB26D' },
     ]);
     ]);
   });
   });
 });
 });

+ 101 - 112
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

@@ -1,9 +1,10 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import tinycolor, { ColorInput } from 'tinycolor2';
+// import tinycolor, { ColorInput } from 'tinycolor2';
 
 
-import { Threshold, BasicGaugeColor } from '../../types';
+import { Threshold } from '../../types';
 import { ColorPicker } from '../ColorPicker/ColorPicker';
 import { ColorPicker } from '../ColorPicker/ColorPicker';
 import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
 import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
+import { colors } from '../../utils';
 
 
 export interface Props {
 export interface Props {
   thresholds: Threshold[];
   thresholds: Threshold[];
@@ -12,50 +13,49 @@ export interface Props {
 
 
 interface State {
 interface State {
   thresholds: Threshold[];
   thresholds: Threshold[];
-  baseColor: string;
 }
 }
 
 
 export class ThresholdsEditor extends PureComponent<Props, State> {
 export class ThresholdsEditor extends PureComponent<Props, State> {
   constructor(props: Props) {
   constructor(props: Props) {
     super(props);
     super(props);
 
 
-    this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
+    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) => {
   onAddThreshold = (index: number) => {
-    const maxValue = 100; // hardcoded for now before we add the base threshold
-    const minValue = 0; // hardcoded for now before we add the base threshold
     const { thresholds } = this.state;
     const { thresholds } = this.state;
+    const maxValue = 100;
+    const minValue = 0;
+
+    if (index === 0) {
+      return;
+    }
 
 
     const newThresholds = thresholds.map(threshold => {
     const newThresholds = thresholds.map(threshold => {
       if (threshold.index >= index) {
       if (threshold.index >= index) {
-        threshold = {
-          ...threshold,
-          index: threshold.index + 1,
-        };
+        const index = threshold.index + 1;
+        threshold = { ...threshold, index };
       }
       }
-
       return threshold;
       return threshold;
     });
     });
 
 
     // Setting value to a value between the previous thresholds
     // Setting value to a value between the previous thresholds
-    let value;
-
-    if (index === 0 && thresholds.length === 0) {
-      value = maxValue - (maxValue - minValue) / 2;
-    } else if (index === 0 && thresholds.length > 0) {
-      value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
-    } else if (index > newThresholds[newThresholds.length - 1].index) {
-      value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
-    }
+    const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
+    const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
+    const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
+    const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
+    const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
 
 
-    // Set a color that lies between the previous thresholds
-    let color;
-    if (index === 0 && thresholds.length === 0) {
-      color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
-    } else {
-      color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
-    }
+    // Set a color
+    const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
 
 
     this.setState(
     this.setState(
       {
       {
@@ -68,23 +68,45 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
           },
           },
         ]),
         ]),
       },
       },
-      () => this.updateGauge()
+      () => this.onChange()
     );
     );
   };
   };
 
 
   onRemoveThreshold = (threshold: Threshold) => {
   onRemoveThreshold = (threshold: Threshold) => {
+    if (threshold.index === 0) {
+      return;
+    }
+
     this.setState(
     this.setState(
-      prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
-      () => this.updateGauge()
+      prevState => {
+        const newThresholds = prevState.thresholds.map(t => {
+          if (t.index > threshold.index) {
+            const index = t.index - 1;
+            t = { ...t, index };
+          }
+          return t;
+        });
+
+        return {
+          thresholds: newThresholds.filter(t => t !== threshold),
+        };
+      },
+      () => this.onChange()
     );
     );
   };
   };
 
 
   onChangeThresholdValue = (event: any, threshold: Threshold) => {
   onChangeThresholdValue = (event: any, threshold: Threshold) => {
+    if (threshold.index === 0) {
+      return;
+    }
+
     const { thresholds } = this.state;
     const { thresholds } = this.state;
+    const parsedValue = parseInt(event.target.value, 10);
+    const value = isNaN(parsedValue) ? null : parsedValue;
 
 
     const newThresholds = thresholds.map(t => {
     const newThresholds = thresholds.map(t => {
-      if (t === threshold) {
-        t = { ...t, value: event.target.value };
+      if (t === threshold && t.index !== 0) {
+        t = { ...t, value: value as number };
       }
       }
 
 
       return t;
       return t;
@@ -108,18 +130,24 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
       {
       {
         thresholds: newThresholds,
         thresholds: newThresholds,
       },
       },
-      () => this.updateGauge()
+      () => this.onChange()
     );
     );
   };
   };
 
 
-  onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
   onBlur = () => {
   onBlur = () => {
-    this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
+    this.setState(prevState => {
+      const sortThresholds = this.sortThresholds([...prevState.thresholds]);
+      let index = sortThresholds.length - 1;
+      sortThresholds.forEach(t => {
+        t.index = index--;
+      });
+      return { thresholds: sortThresholds };
+    });
 
 
-    this.updateGauge();
+    this.onChange();
   };
   };
 
 
-  updateGauge = () => {
+  onChange = () => {
     this.props.onChange(this.state.thresholds);
     this.props.onChange(this.state.thresholds);
   };
   };
 
 
@@ -129,92 +157,53 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
     });
     });
   };
   };
 
 
-  renderThresholds() {
-    const { thresholds } = this.state;
-
-    return thresholds.map((threshold, index) => {
-      return (
-        <div className="threshold-row" key={`${threshold.index}-${index}`}>
-          <div className="threshold-row-inner">
-            <div className="threshold-row-color">
-              {threshold.color && (
-                <div className="threshold-row-color-inner">
-                  <ColorPicker
-                    color={threshold.color}
-                    onChange={color => this.onChangeThresholdColor(threshold, color)}
-                  />
-                </div>
-              )}
-            </div>
-            <input
-              className="threshold-row-input"
-              type="text"
-              onChange={event => this.onChangeThresholdValue(event, threshold)}
-              value={threshold.value}
-              onBlur={this.onBlur}
-            />
-            <div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
-              <i className="fa fa-times" />
+  renderInput = (threshold: Threshold) => {
+    const value = threshold.index === 0 ? 'Base' : threshold.value;
+    return (
+      <div className="thresholds-row-input-inner">
+        <span className="thresholds-row-input-inner-arrow" />
+        <div className="thresholds-row-input-inner-color">
+          {threshold.color && (
+            <div className="thresholds-row-input-inner-color-colorpicker">
+              <ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
             </div>
             </div>
-          </div>
+          )}
         </div>
         </div>
-      );
-    });
-  }
-
-  renderIndicator() {
-    const { thresholds } = this.state;
-
-    return thresholds.map((t, i) => {
-      return (
-        <div key={`${t.value}-${i}`} className="indicator-section">
-          <div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
-          <div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
+        <div className="thresholds-row-input-inner-value">
+          <input
+            type="text"
+            onChange={event => this.onChangeThresholdValue(event, threshold)}
+            value={value}
+            onBlur={this.onBlur}
+            readOnly={threshold.index === 0}
+          />
         </div>
         </div>
-      );
-    });
-  }
-
-  renderBaseIndicator() {
-    return (
-      <div className="indicator-section" style={{ height: '100%' }}>
-        <div
-          onClick={() => this.onAddThreshold(0)}
-          style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
-        />
-      </div>
-    );
-  }
-
-  renderBase() {
-    const baseColor = BasicGaugeColor.Green;
-
-    return (
-      <div className="threshold-row threshold-row-base">
-        <div className="threshold-row-inner threshold-row-inner--base">
-          <div className="threshold-row-color">
-            <div className="threshold-row-color-inner">
-              <ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
-            </div>
+        {threshold.index > 0 && (
+          <div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
+            <i className="fa fa-times" />
           </div>
           </div>
-          <div className="threshold-row-label">Base</div>
-        </div>
+        )}
       </div>
       </div>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
+    const { thresholds } = this.state;
+
     return (
     return (
       <PanelOptionsGroup title="Thresholds">
       <PanelOptionsGroup title="Thresholds">
         <div className="thresholds">
         <div className="thresholds">
-          <div className="color-indicators">
-            {this.renderIndicator()}
-            {this.renderBaseIndicator()}
-          </div>
-          <div className="threshold-rows">
-            {this.renderThresholds()}
-            {this.renderBase()}
-          </div>
+          {thresholds.map((threshold, index) => {
+            return (
+              <div className="thresholds-row" key={`${threshold.index}-${index}`}>
+                <div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
+                  <i className="fa fa-plus" />
+                </div>
+                <div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
+                <div className="thresholds-row-input">{this.renderInput(threshold)}</div>
+              </div>
+            );
+          })}
         </div>
         </div>
       </PanelOptionsGroup>
       </PanelOptionsGroup>
     );
     );

+ 69 - 67
packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss

@@ -1,103 +1,105 @@
 .thresholds {
 .thresholds {
+  margin-bottom: 10px;
+}
+
+.thresholds-row {
   display: flex;
   display: flex;
+  flex-direction: row;
+  height: 70px;
 }
 }
 
 
-.threshold-rows {
-  margin-left: 5px;
+.thresholds-row:first-child > .thresholds-row-color-indicator {
+  border-top-left-radius: $border-radius;
+  border-top-right-radius: $border-radius;
+  overflow: hidden;
 }
 }
 
 
-.threshold-row {
+.thresholds-row:last-child > .thresholds-row-color-indicator {
+  border-bottom-left-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
+  overflow: hidden;
+}
+
+.thresholds-row-add-button {
+  align-self: center;
+  margin-right: 5px;
+  color: $green;
+  height: 24px;
+  width: 24px;
+  background-color: $green;
+  border-radius: 50%;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  margin-top: 3px;
-  padding: 5px;
+  justify-content: center;
+  cursor: pointer;
+}
 
 
-  &::before {
-    font-family: 'FontAwesome';
-    content: '\f0d9';
-    color: $input-label-border-color;
-  }
+.thresholds-row-add-button > i {
+  color: $white;
 }
 }
 
 
-.threshold-row-inner {
-  border: 1px solid $input-label-border-color;
-  border-radius: $border-radius;
-  display: flex;
-  overflow: hidden;
-  height: 37px;
+.thresholds-row-color-indicator {
+  width: 10px;
+}
 
 
-  &--base {
-    width: auto;
-  }
+.thresholds-row-input {
+  margin-top: 49px;
+  margin-left: 2px;
 }
 }
 
 
-.threshold-row-color {
-  width: 36px;
-  border-right: 1px solid $input-label-border-color;
+.thresholds-row-input-inner {
   display: flex;
   display: flex;
-  align-items: center;
   justify-content: center;
   justify-content: center;
-  background-color: $input-bg;
+  flex-direction: row;
 }
 }
 
 
-.threshold-row-color-inner {
-  border-radius: 10px;
-  overflow: hidden;
-  display: flex;
-  align-items: center;
-  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
+.thresholds-row-input-inner > *:last-child {
+  border-top-right-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
 }
 }
 
 
-.threshold-row-input {
-  padding: 8px 10px;
-  width: 150px;
+.thresholds-row-input-inner-arrow {
+  align-self: center;
+  width: 0;
+  height: 0;
+  border-top: 6px solid transparent;
+  border-bottom: 6px solid transparent;
+  border-right: 6px solid $input-label-border-color;
 }
 }
 
 
-.threshold-row-label {
-  background-color: $input-label-bg;
-  padding: 5px;
-  display: flex;
-  align-items: center;
+.thresholds-row-input-inner-value > input {
+  height: $gf-form-input-height;
+  padding: $input-padding-y $input-padding-x;
+  width: 150px;
+  border-top: 1px solid $input-label-border-color;
+  border-bottom: 1px solid $input-label-border-color;
 }
 }
 
 
-.threshold-row-add-label {
-  align-items: center;
+.thresholds-row-input-inner-color {
+  width: 42px;
   display: flex;
   display: flex;
-  padding: 5px 8px;
+  align-items: center;
+  justify-content: center;
+  background-color: $input-bg;
+  border: 1px solid $input-label-border-color;
 }
 }
 
 
-.threshold-row-remove {
+.thresholds-row-input-inner-color-colorpicker {
+  border-radius: 10px;
+  overflow: hidden;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  justify-content: center;
-  height: 37px;
-  width: 37px;
-  cursor: pointer;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
 }
 }
 
 
-.threshold-row-add {
-  border-right: $border-width solid $input-label-border-color;
+.thresholds-row-input-inner-remove {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  width: 36px;
-  background-color: $green;
-}
-
-.threshold-row-label {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-}
-
-.indicator-section {
-  width: 100%;
-  height: 50px;
+  height: $gf-form-input-height;
+  padding: $input-padding-y $input-padding-x;
+  width: 42px;
+  background-color: $input-label-bg;
+  border: 1px solid $input-label-border-color;
   cursor: pointer;
   cursor: pointer;
 }
 }
-
-.color-indicators {
-  width: 15px;
-  border-bottom-left-radius: $border-radius;
-  border-bottom-right-radius: $border-radius;
-  overflow: hidden;
-}

+ 48 - 53
public/app/plugins/panel/gauge/MappingRow.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx

@@ -1,22 +1,22 @@
-import React, { PureComponent } from 'react';
-import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui';
+import React, { ChangeEvent, PureComponent } from 'react';
 
 
-import { Label } from 'app/core/components/Label/Label';
+import { MappingType, ValueMapping } from '../../types';
+import { FormField, FormLabel, Select } from '..';
 
 
-interface Props {
-  mapping: ValueMap | RangeMap;
-  updateMapping: (mapping) => void;
-  removeMapping: () => void;
+export interface Props {
+  valueMapping: ValueMapping;
+  updateValueMapping: (valueMapping: ValueMapping) => void;
+  removeValueMapping: () => void;
 }
 }
 
 
 interface State {
 interface State {
-  from: string;
+  from?: string;
   id: number;
   id: number;
   operator: string;
   operator: string;
   text: string;
   text: string;
-  to: string;
+  to?: string;
   type: MappingType;
   type: MappingType;
-  value: string;
+  value?: string;
 }
 }
 
 
 const mappingOptions = [
 const mappingOptions = [
@@ -25,36 +25,34 @@ const mappingOptions = [
 ];
 ];
 
 
 export default class MappingRow extends PureComponent<Props, State> {
 export default class MappingRow extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      ...props.mapping,
-    };
+    this.state = { ...props.valueMapping };
   }
   }
 
 
-  onMappingValueChange = event => {
+  onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ value: event.target.value });
     this.setState({ value: event.target.value });
   };
   };
 
 
-  onMappingFromChange = event => {
+  onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ from: event.target.value });
     this.setState({ from: event.target.value });
   };
   };
 
 
-  onMappingToChange = event => {
+  onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ to: event.target.value });
     this.setState({ to: event.target.value });
   };
   };
 
 
-  onMappingTextChange = event => {
+  onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ text: event.target.value });
     this.setState({ text: event.target.value });
   };
   };
 
 
-  onMappingTypeChange = mappingType => {
+  onMappingTypeChange = (mappingType: MappingType) => {
     this.setState({ type: mappingType });
     this.setState({ type: mappingType });
   };
   };
 
 
   updateMapping = () => {
   updateMapping = () => {
-    this.props.updateMapping({ ...this.state });
+    this.props.updateValueMapping({ ...this.state } as ValueMapping);
   };
   };
 
 
   renderRow() {
   renderRow() {
@@ -63,30 +61,28 @@ export default class MappingRow extends PureComponent<Props, State> {
     if (type === MappingType.RangeToText) {
     if (type === MappingType.RangeToText) {
       return (
       return (
         <>
         <>
-          <div className="gf-form">
-            <Label width={4}>From</Label>
-            <input
-              className="gf-form-input width-8"
-              value={from}
-              onBlur={this.updateMapping}
-              onChange={this.onMappingFromChange}
-            />
-          </div>
-          <div className="gf-form">
-            <Label width={4}>To</Label>
+          <FormField
+            label="From"
+            labelWidth={4}
+            inputWidth={8}
+            onBlur={this.updateMapping}
+            onChange={this.onMappingFromChange}
+            value={from}
+          />
+          <FormField
+            label="To"
+            labelWidth={4}
+            inputWidth={8}
+            onBlur={this.updateMapping}
+            onChange={this.onMappingToChange}
+            value={to}
+          />
+          <div className="gf-form gf-form--grow">
+            <FormLabel width={4}>Text</FormLabel>
             <input
             <input
-              className="gf-form-input width-8"
-              value={to}
+              className="gf-form-input"
               onBlur={this.updateMapping}
               onBlur={this.updateMapping}
-              onChange={this.onMappingToChange}
-            />
-          </div>
-          <div className="gf-form">
-            <Label width={4}>Text</Label>
-            <input
-              className="gf-form-input width-10"
               value={text}
               value={text}
-              onBlur={this.updateMapping}
               onChange={this.onMappingTextChange}
               onChange={this.onMappingTextChange}
             />
             />
           </div>
           </div>
@@ -96,17 +92,16 @@ export default class MappingRow extends PureComponent<Props, State> {
 
 
     return (
     return (
       <>
       <>
-        <div className="gf-form">
-          <Label width={4}>Value</Label>
-          <input
-            className="gf-form-input width-8"
-            onBlur={this.updateMapping}
-            onChange={this.onMappingValueChange}
-            value={value}
-          />
-        </div>
+        <FormField
+          label="Value"
+          labelWidth={4}
+          onBlur={this.updateMapping}
+          onChange={this.onMappingValueChange}
+          value={value}
+          inputWidth={8}
+        />
         <div className="gf-form gf-form--grow">
         <div className="gf-form gf-form--grow">
-          <Label width={4}>Text</Label>
+          <FormLabel width={4}>Text</FormLabel>
           <input
           <input
             className="gf-form-input"
             className="gf-form-input"
             onBlur={this.updateMapping}
             onBlur={this.updateMapping}
@@ -124,7 +119,7 @@ export default class MappingRow extends PureComponent<Props, State> {
     return (
     return (
       <div className="gf-form-inline">
       <div className="gf-form-inline">
         <div className="gf-form">
         <div className="gf-form">
-          <Label width={5}>Type</Label>
+          <FormLabel width={5}>Type</FormLabel>
           <Select
           <Select
             placeholder="Choose type"
             placeholder="Choose type"
             isSearchable={false}
             isSearchable={false}
@@ -136,7 +131,7 @@ export default class MappingRow extends PureComponent<Props, State> {
         </div>
         </div>
         {this.renderRow()}
         {this.renderRow()}
         <div className="gf-form">
         <div className="gf-form">
-          <button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
+          <button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
             <i className="fa fa-times" />
             <i className="fa fa-times" />
           </button>
           </button>
         </div>
         </div>

+ 14 - 16
public/app/plugins/panel/gauge/ValueMappings.test.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx

@@ -1,27 +1,23 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
-import { GaugeOptions, MappingType, PanelOptionsProps } from '@grafana/ui';
-import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions';
 
 
-import ValueMappings from './ValueMappings';
+import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
+import { MappingType } from '../../types/panel';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
-  const props: PanelOptionsProps<GaugeOptions> = {
+  const props: Props = {
     onChange: jest.fn(),
     onChange: jest.fn(),
-    options: {
-      ...defaultProps.options,
-      mappings: [
-        { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
-        { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
-      ],
-    },
+    valueMappings: [
+      { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
+      { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
+    ],
   };
   };
 
 
   Object.assign(props, propOverrides);
   Object.assign(props, propOverrides);
 
 
-  const wrapper = shallow(<ValueMappings {...props} />);
+  const wrapper = shallow(<ValueMappingsEditor {...props} />);
 
 
-  const instance = wrapper.instance() as ValueMappings;
+  const instance = wrapper.instance() as ValueMappingsEditor;
 
 
   return {
   return {
     instance,
     instance,
@@ -40,18 +36,20 @@ describe('Render', () => {
 describe('On remove mapping', () => {
 describe('On remove mapping', () => {
   it('Should remove mapping with id 0', () => {
   it('Should remove mapping with id 0', () => {
     const { instance } = setup();
     const { instance } = setup();
+
     instance.onRemoveMapping(1);
     instance.onRemoveMapping(1);
 
 
-    expect(instance.state.mappings).toEqual([
+    expect(instance.state.valueMappings).toEqual([
       { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
       { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
     ]);
     ]);
   });
   });
 
 
   it('should remove mapping with id 1', () => {
   it('should remove mapping with id 1', () => {
     const { instance } = setup();
     const { instance } = setup();
+
     instance.onRemoveMapping(2);
     instance.onRemoveMapping(2);
 
 
-    expect(instance.state.mappings).toEqual([
+    expect(instance.state.valueMappings).toEqual([
       { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
       { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
     ]);
     ]);
   });
   });
@@ -67,7 +65,7 @@ describe('Next id to add', () => {
   });
   });
 
 
   it('should default to 1', () => {
   it('should default to 1', () => {
-    const { instance } = setup({ options: { ...defaultProps.options } });
+    const { instance } = setup({ valueMappings: [] });
 
 
     expect(instance.state.nextIdToAdd).toEqual(1);
     expect(instance.state.nextIdToAdd).toEqual(1);
   });
   });

+ 29 - 23
public/app/plugins/panel/gauge/ValueMappings.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx

@@ -1,33 +1,39 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap, PanelOptionsGroup } from '@grafana/ui';
 
 
 import MappingRow from './MappingRow';
 import MappingRow from './MappingRow';
+import { MappingType, ValueMapping } from '../../types/panel';
+import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
+
+export interface Props {
+  valueMappings: ValueMapping[];
+  onChange: (valueMappings: ValueMapping[]) => void;
+}
 
 
 interface State {
 interface State {
-  mappings: Array<ValueMap | RangeMap>;
+  valueMappings: ValueMapping[];
   nextIdToAdd: number;
   nextIdToAdd: number;
 }
 }
 
 
-export default class ValueMappings extends PureComponent<PanelOptionsProps<GaugeOptions>, State> {
-  constructor(props) {
+export class ValueMappingsEditor extends PureComponent<Props, State> {
+  constructor(props: Props) {
     super(props);
     super(props);
 
 
-    const mappings = props.options.mappings;
+    const mappings = props.valueMappings;
 
 
     this.state = {
     this.state = {
-      mappings: mappings || [],
-      nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
+      valueMappings: mappings,
+      nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1,
     };
     };
   }
   }
 
 
-  getMaxIdFromMappings(mappings) {
+  getMaxIdFromValueMappings(mappings: ValueMapping[]) {
     return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
     return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
   }
   }
 
 
   addMapping = () =>
   addMapping = () =>
     this.setState(prevState => ({
     this.setState(prevState => ({
-      mappings: [
-        ...prevState.mappings,
+      valueMappings: [
+        ...prevState.valueMappings,
         {
         {
           id: prevState.nextIdToAdd,
           id: prevState.nextIdToAdd,
           operator: '',
           operator: '',
@@ -41,23 +47,23 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
       nextIdToAdd: prevState.nextIdToAdd + 1,
       nextIdToAdd: prevState.nextIdToAdd + 1,
     }));
     }));
 
 
-  onRemoveMapping = id => {
+  onRemoveMapping = (id: number) => {
     this.setState(
     this.setState(
       prevState => ({
       prevState => ({
-        mappings: prevState.mappings.filter(m => {
+        valueMappings: prevState.valueMappings.filter(m => {
           return m.id !== id;
           return m.id !== id;
         }),
         }),
       }),
       }),
       () => {
       () => {
-        this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
+        this.props.onChange(this.state.valueMappings);
       }
       }
     );
     );
   };
   };
 
 
-  updateGauge = mapping => {
+  updateGauge = (mapping: ValueMapping) => {
     this.setState(
     this.setState(
       prevState => ({
       prevState => ({
-        mappings: prevState.mappings.map(m => {
+        valueMappings: prevState.valueMappings.map(m => {
           if (m.id === mapping.id) {
           if (m.id === mapping.id) {
             return { ...mapping };
             return { ...mapping };
           }
           }
@@ -66,24 +72,24 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
         }),
         }),
       }),
       }),
       () => {
       () => {
-        this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
+        this.props.onChange(this.state.valueMappings);
       }
       }
     );
     );
   };
   };
 
 
   render() {
   render() {
-    const { mappings } = this.state;
+    const { valueMappings } = this.state;
 
 
     return (
     return (
       <PanelOptionsGroup title="Value Mappings">
       <PanelOptionsGroup title="Value Mappings">
         <div>
         <div>
-          {mappings.length > 0 &&
-            mappings.map((mapping, index) => (
+          {valueMappings.length > 0 &&
+            valueMappings.map((valueMapping, index) => (
               <MappingRow
               <MappingRow
-                key={`${mapping.text}-${index}`}
-                mapping={mapping}
-                updateMapping={this.updateGauge}
-                removeMapping={() => this.onRemoveMapping(mapping.id)}
+                key={`${valueMapping.text}-${index}`}
+                valueMapping={valueMapping}
+                updateValueMapping={this.updateGauge}
+                removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
               />
               />
             ))}
             ))}
         </div>
         </div>

+ 0 - 0
public/sass/components/_value-mappings.scss → packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss


+ 6 - 6
public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap → packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap

@@ -7,7 +7,9 @@ exports[`Render should render component 1`] = `
   <div>
   <div>
     <MappingRow
     <MappingRow
       key="Ok-0"
       key="Ok-0"
-      mapping={
+      removeValueMapping={[Function]}
+      updateValueMapping={[Function]}
+      valueMapping={
         Object {
         Object {
           "id": 1,
           "id": 1,
           "operator": "",
           "operator": "",
@@ -16,12 +18,12 @@ exports[`Render should render component 1`] = `
           "value": "20",
           "value": "20",
         }
         }
       }
       }
-      removeMapping={[Function]}
-      updateMapping={[Function]}
     />
     />
     <MappingRow
     <MappingRow
       key="Meh-1"
       key="Meh-1"
-      mapping={
+      removeValueMapping={[Function]}
+      updateValueMapping={[Function]}
+      valueMapping={
         Object {
         Object {
           "from": "21",
           "from": "21",
           "id": 2,
           "id": 2,
@@ -31,8 +33,6 @@ exports[`Render should render component 1`] = `
           "type": 2,
           "type": 2,
         }
         }
       }
       }
-      removeMapping={[Function]}
-      updateMapping={[Function]}
     />
     />
   </div>
   </div>
   <div
   <div

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

@@ -5,5 +5,6 @@
 @import 'Select/Select';
 @import 'Select/Select';
 @import 'PanelOptionsGroup/PanelOptionsGroup';
 @import 'PanelOptionsGroup/PanelOptionsGroup';
 @import 'PanelOptionsGrid/PanelOptionsGrid';
 @import 'PanelOptionsGrid/PanelOptionsGrid';
-@import 'PanelOptionsGrid/PanelOptionsGrid';
 @import 'ColorPicker/ColorPicker';
 @import 'ColorPicker/ColorPicker';
+@import 'ValueMappingsEditor/ValueMappingsEditor';
+@import "FormField/FormField";

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

@@ -9,12 +9,17 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer';
 export { NoOptionsMessage } from './Select/NoOptionsMessage';
 export { NoOptionsMessage } from './Select/NoOptionsMessage';
 export { default as resetSelectStyles } from './Select/resetSelectStyles';
 export { default as resetSelectStyles } from './Select/resetSelectStyles';
 
 
+// Forms
+export { FormLabel } from './FormLabel/FormLabel';
+export { FormField } from './FormField/FormField';
+
 export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { ColorPicker } from './ColorPicker/ColorPicker';
 export { ColorPicker } from './ColorPicker/ColorPicker';
 export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
 export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
 export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
 export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
-export { GfFormLabel } from './GfFormLabel/GfFormLabel';
 export { Graph } from './Graph/Graph';
 export { Graph } from './Graph/Graph';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
+export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
+export { Gauge } from './Gauge/Gauge';

+ 89 - 0
packages/grafana-ui/src/types/datasource.ts

@@ -0,0 +1,89 @@
+import { TimeRange, RawTimeRange } from './time';
+import { TimeSeries } from './series';
+import { PluginMeta } from './plugin';
+
+export interface DataQueryResponse {
+  data: TimeSeries[];
+}
+
+export interface DataQuery {
+  /**
+   * A - Z
+   */
+  refId: string;
+
+  /**
+   * 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<TQuery extends DataQuery = DataQuery> {
+  timezone: string;
+  range: TimeRange;
+  rangeRaw: RawTimeRange;
+  targets: TQuery[];
+  panelId: number;
+  dashboardId: number;
+  cacheTimeout?: string;
+  interval: string;
+  intervalMs: number;
+  maxDataPoints: number;
+  scopedVars: object;
+}
+
+export interface QueryFix {
+  type: string;
+  label: string;
+  action?: QueryFixAction;
+}
+
+export interface QueryFixAction {
+  type: string;
+  query?: string;
+  preventSubmit?: boolean;
+}
+
+export interface QueryHint {
+  type: string;
+  label: string;
+  fix?: QueryFix;
+}
+
+export interface DataSourceSettings {
+  id: number;
+  orgId: number;
+  name: string;
+  typeLogoUrl: string;
+  type: string;
+  access: string;
+  url: string;
+  password: string;
+  user: string;
+  database: string;
+  basicAuth: boolean;
+  basicAuthPassword: string;
+  basicAuthUser: string;
+  isDefault: boolean;
+  jsonData: { authType: string; defaultRegion: string };
+  readOnly: boolean;
+  withCredentials: boolean;
+}
+
+export interface DataSourceSelectItem {
+  name: string;
+  value: string | null;
+  meta: PluginMeta;
+  sort: string;
+}

+ 0 - 16
packages/grafana-ui/src/types/gauge.ts

@@ -1,16 +0,0 @@
-import { RangeMap, Threshold, ValueMap } from './panel';
-
-export interface GaugeOptions {
-  baseColor: string;
-  decimals: number;
-  mappings: Array<RangeMap | ValueMap>;
-  maxValue: number;
-  minValue: number;
-  prefix: string;
-  showThresholdLabels: boolean;
-  showThresholdMarkers: boolean;
-  stat: string;
-  suffix: string;
-  thresholds: Threshold[];
-  unit: string;
-}

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

@@ -1,4 +1,5 @@
 export * from './series';
 export * from './series';
 export * from './time';
 export * from './time';
 export * from './panel';
 export * from './panel';
-export * from './gauge';
+export * from './plugin';
+export * from './datasource';

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

@@ -1,6 +1,8 @@
 import { TimeSeries, LoadingState } from './series';
 import { TimeSeries, LoadingState } from './series';
 import { TimeRange } from './time';
 import { TimeRange } from './time';
 
 
+export type InterpolateFunction = (value: string, format?: string | Function) => string;
+
 export interface PanelProps<T = any> {
 export interface PanelProps<T = any> {
   timeSeries: TimeSeries[];
   timeSeries: TimeSeries[];
   timeRange: TimeRange;
   timeRange: TimeRange;
@@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
   renderCounter: number;
   renderCounter: number;
   width: number;
   width: number;
   height: number;
   height: number;
+  onInterpolate: InterpolateFunction;
 }
 }
 
 
 export interface PanelOptionsProps<T = any> {
 export interface PanelOptionsProps<T = any> {
@@ -53,6 +56,8 @@ interface BaseMap {
   type: MappingType;
   type: MappingType;
 }
 }
 
 
+export type ValueMapping = ValueMap | RangeMap;
+
 export interface ValueMap extends BaseMap {
 export interface ValueMap extends BaseMap {
   value: string;
   value: string;
 }
 }
@@ -61,3 +66,10 @@ export interface RangeMap extends BaseMap {
   from: string;
   from: string;
   to: string;
   to: string;
 }
 }
+
+export type ThemeName = 'dark' | 'light';
+
+export enum ThemeNames {
+  Dark = 'dark',
+  Light = 'light',
+}

+ 118 - 0
packages/grafana-ui/src/types/plugin.ts

@@ -0,0 +1,118 @@
+import { ComponentClass } from 'react';
+import { PanelProps, PanelOptionsProps } from './panel';
+import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
+
+export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
+  /**
+   *  min interval range
+   */
+  interval?: string;
+
+  /**
+   * Imports queries from a different datasource
+   */
+  importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
+
+  /**
+   * Initializes a datasource after instantiation
+   */
+  init?: () => void;
+
+  /**
+   * Main metrics / data query action
+   */
+  query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
+
+  /**
+   * Test & verify datasource settings & connection details
+   */
+  testDatasource(): Promise<any>;
+
+  /**
+   *  Get hints for query improvements
+   */
+  getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
+
+  /**
+   *  Set after constructor is called by Grafana
+   */
+  name?: string;
+  meta?: PluginMeta;
+  pluginExports?: PluginExports;
+}
+
+export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
+  datasource: DSType;
+  query: TQuery;
+  onExecuteQuery?: () => void;
+  onQueryChange?: (value: TQuery) => void;
+}
+
+export interface PluginExports {
+  Datasource?: DataSourceApi;
+  QueryCtrl?: any;
+  QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
+  ConfigCtrl?: any;
+  AnnotationsQueryCtrl?: any;
+  VariableQueryEditor?: any;
+  ExploreQueryField?: any;
+  ExploreStartPage?: any;
+
+  // Panel plugin
+  PanelCtrl?: any;
+  Panel?: ComponentClass<PanelProps>;
+  PanelOptions?: ComponentClass<PanelOptionsProps>;
+  PanelDefaults?: any;
+}
+
+export interface PluginMeta {
+  id: string;
+  name: string;
+  info: PluginMetaInfo;
+  includes: PluginInclude[];
+
+  // Datasource-specific
+  metrics?: boolean;
+  tables?: boolean;
+  logs?: boolean;
+  explore?: boolean;
+  annotations?: boolean;
+  mixed?: boolean;
+  hasQueryHelp?: boolean;
+  queryOptions?: PluginMetaQueryOptions;
+}
+
+interface PluginMetaQueryOptions {
+  cacheTimeout?: boolean;
+  maxDataPoints?: boolean;
+  minInterval?: boolean;
+}
+
+export interface PluginInclude {
+  type: string;
+  name: string;
+  path: string;
+}
+
+interface PluginMetaInfoLink {
+  name: string;
+  url: string;
+}
+
+export interface PluginMetaInfo {
+  author: {
+    name: string;
+    url?: string;
+  };
+  description: string;
+  links: PluginMetaInfoLink[];
+  logos: {
+    large: string;
+    small: string;
+  };
+  screenshots: any[];
+  updated: string;
+  version: string;
+}
+
+

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

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

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

@@ -1,18 +1,19 @@
 // Libraries
 // Libraries
 import _ from 'lodash';
 import _ from 'lodash';
 
 
+import { colors } from './colors';
+
 // Types
 // Types
 import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
 import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
 
 
 interface Options {
 interface Options {
   timeSeries: TimeSeries[];
   timeSeries: TimeSeries[];
   nullValueMode: NullValueMode;
   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 vmSeries = timeSeries.map((item, index) => {
-    const colorIndex = index % colorPalette.length;
+    const colorIndex = index % colors.length;
     const label = item.target;
     const label = item.target;
     const result = [];
     const result = [];
 
 
@@ -49,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
         continue;
         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
       // 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 {
     return {
       data: result,
       data: result,
       label: label,
       label: label,
-      color: colorPalette[colorIndex],
+      color: colors[colorIndex],
+      allIsZero,
+      allIsNull,
       stats: {
       stats: {
         total,
         total,
         min,
         min,
@@ -164,8 +167,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
         range,
         range,
         count,
         count,
         first,
         first,
-        allIsZero,
-        allIsNull,
       },
       },
     };
     };
   });
   });

+ 5 - 2
pkg/components/imguploader/imguploader.go

@@ -6,7 +6,6 @@ import (
 	"regexp"
 	"regexp"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
-
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
@@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error)
 	return "", nil
 	return "", nil
 }
 }
 
 
+var (
+	logger = log.New("imguploader")
+)
+
 func NewImageUploader() (ImageUploader, error) {
 func NewImageUploader() (ImageUploader, error) {
 
 
 	switch setting.ImageUploadProvider {
 	switch setting.ImageUploadProvider {
@@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) {
 	}
 	}
 
 
 	if setting.ImageUploadProvider != "" {
 	if setting.ImageUploadProvider != "" {
-		log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
+		logger.Error("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
 	}
 	}
 
 
 	return NopImageUploader{}, nil
 	return NopImageUploader{}, nil

+ 2 - 20
pkg/log/log.go

@@ -10,13 +10,11 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
-	"gopkg.in/ini.v1"
-
 	"github.com/go-stack/stack"
 	"github.com/go-stack/stack"
+	"github.com/grafana/grafana/pkg/util"
 	"github.com/inconshreveable/log15"
 	"github.com/inconshreveable/log15"
 	isatty "github.com/mattn/go-isatty"
 	isatty "github.com/mattn/go-isatty"
-
-	"github.com/grafana/grafana/pkg/util"
+	"gopkg.in/ini.v1"
 )
 )
 
 
 var Root log15.Logger
 var Root log15.Logger
@@ -58,10 +56,6 @@ func Debug(format string, v ...interface{}) {
 	Root.Debug(message)
 	Root.Debug(message)
 }
 }
 
 
-func Debug2(message string, v ...interface{}) {
-	Root.Debug(message, v...)
-}
-
 func Info(format string, v ...interface{}) {
 func Info(format string, v ...interface{}) {
 	var message string
 	var message string
 	if len(v) > 0 {
 	if len(v) > 0 {
@@ -73,10 +67,6 @@ func Info(format string, v ...interface{}) {
 	Root.Info(message)
 	Root.Info(message)
 }
 }
 
 
-func Info2(message string, v ...interface{}) {
-	Root.Info(message, v...)
-}
-
 func Warn(format string, v ...interface{}) {
 func Warn(format string, v ...interface{}) {
 	var message string
 	var message string
 	if len(v) > 0 {
 	if len(v) > 0 {
@@ -88,18 +78,10 @@ func Warn(format string, v ...interface{}) {
 	Root.Warn(message)
 	Root.Warn(message)
 }
 }
 
 
-func Warn2(message string, v ...interface{}) {
-	Root.Warn(message, v...)
-}
-
 func Error(skip int, format string, v ...interface{}) {
 func Error(skip int, format string, v ...interface{}) {
 	Root.Error(fmt.Sprintf(format, v...))
 	Root.Error(fmt.Sprintf(format, v...))
 }
 }
 
 
-func Error2(message string, v ...interface{}) {
-	Root.Error(message, v...)
-}
-
 func Critical(skip int, format string, v ...interface{}) {
 func Critical(skip int, format string, v ...interface{}) {
 	Root.Crit(fmt.Sprintf(format, v...))
 	Root.Crit(fmt.Sprintf(format, v...))
 }
 }

+ 5 - 1
pkg/login/ext_user.go

@@ -11,6 +11,10 @@ func init() {
 	bus.AddHandler("auth", UpsertUser)
 	bus.AddHandler("auth", UpsertUser)
 }
 }
 
 
+var (
+	logger = log.New("login.ext_user")
+)
+
 func UpsertUser(cmd *m.UpsertUserCommand) error {
 func UpsertUser(cmd *m.UpsertUserCommand) error {
 	extUser := cmd.ExternalUser
 	extUser := cmd.ExternalUser
 
 
@@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd)
+	logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
 	return bus.Dispatch(updateCmd)
 	return bus.Dispatch(updateCmd)
 }
 }
 
 

+ 1 - 1
pkg/services/alerting/notifiers/telegram.go

@@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
 	defer func() {
 	defer func() {
 		err := imageFile.Close()
 		err := imageFile.Close()
 		if err != nil {
 		if err != nil {
-			log.Error2("Could not close Telegram inline image.", "err", err)
+			this.log.Error("Could not close Telegram inline image.", "err", err)
 		}
 		}
 	}()
 	}()
 
 

+ 5 - 2
pkg/services/alerting/test_notification.go

@@ -18,9 +18,12 @@ type NotificationTestCommand struct {
 	Settings *simplejson.Json
 	Settings *simplejson.Json
 }
 }
 
 
+var (
+	logger = log.New("alerting.testnotification")
+)
+
 func init() {
 func init() {
 	bus.AddHandler("alerting", handleNotificationTestCommand)
 	bus.AddHandler("alerting", handleNotificationTestCommand)
-
 }
 }
 
 
 func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
 func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
@@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
 	notifiers, err := InitNotifier(model)
 	notifiers, err := InitNotifier(model)
 
 
 	if err != nil {
 	if err != nil {
-		log.Error2("Failed to create notifier", "error", err.Error())
+		logger.Error("Failed to create notifier", "error", err.Error())
 		return err
 		return err
 	}
 	}
 
 

+ 2 - 2
public/app/core/components/Animations/FadeIn.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import Transition from 'react-transition-group/Transition';
 import Transition from 'react-transition-group/Transition';
 
 
 interface Props {
 interface Props {
@@ -8,7 +8,7 @@ interface Props {
   unmountOnExit?: boolean;
   unmountOnExit?: boolean;
 }
 }
 
 
-export const FadeIn: SFC<Props> = props => {
+export const FadeIn: FC<Props> = props => {
   const defaultStyle = {
   const defaultStyle = {
     transition: `opacity ${props.duration}ms linear`,
     transition: `opacity ${props.duration}ms linear`,
     opacity: 0,
     opacity: 0,

+ 50 - 0
public/app/core/components/Footer/Footer.tsx

@@ -0,0 +1,50 @@
+import React, { FC } from 'react';
+import { Tooltip } from '@grafana/ui';
+
+interface Props {
+  appName: string;
+  buildVersion: string;
+  buildCommit: string;
+  newGrafanaVersionExists: boolean;
+  newGrafanaVersion: string;
+}
+
+export const Footer: FC<Props> = React.memo(({appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion}) => {
+  return (
+    <footer className="footer">
+      <div className="text-center">
+        <ul>
+          <li>
+            <a href="http://docs.grafana.org" target="_blank">
+              <i className="fa fa-file-code-o" /> Docs
+            </a>
+          </li>
+          <li>
+            <a href="https://grafana.com/services/support" target="_blank">
+              <i className="fa fa-support" /> Support Plans
+            </a>
+          </li>
+          <li>
+            <a href="https://community.grafana.com/" target="_blank">
+              <i className="fa fa-comments-o" /> Community
+            </a>
+          </li>
+          <li>
+            <a href="https://grafana.com" target="_blank">{appName}</a> <span>v{buildVersion} (commit: {buildCommit})</span>
+          </li>
+          {newGrafanaVersionExists && (
+            <li>
+              <Tooltip placement="auto" content={newGrafanaVersion}>
+                <a href="https://grafana.com/get" target="_blank">
+                  New version available!
+                </a>
+              </Tooltip>
+            </li>
+          )}
+        </ul>
+      </div>
+    </footer>
+  );
+});
+
+export default Footer;

+ 0 - 25
public/app/core/components/Label/Label.tsx

@@ -1,25 +0,0 @@
-import React, { SFC, ReactNode } from 'react';
-import { Tooltip } from '@grafana/ui';
-
-interface Props {
-  tooltip?: string;
-  for?: string;
-  children: ReactNode;
-  width?: number;
-  className?: string;
-}
-
-export const Label: SFC<Props> = props => {
-  return (
-    <span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
-      <span>{props.children}</span>
-      {props.tooltip && (
-        <Tooltip placement="auto" content={props.tooltip}>
-          <div className="gf-form-help-icon--right-normal">
-            <i className="gicon gicon-question gicon--has-hover" />
-          </div>
-        </Tooltip>
-      )}
-    </span>
-  );
-};

+ 2 - 2
public/app/core/components/LayoutSelector/LayoutSelector.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
 export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
 
 
@@ -12,7 +12,7 @@ interface Props {
   onLayoutModeChanged: (mode: LayoutMode) => {};
   onLayoutModeChanged: (mode: LayoutMode) => {};
 }
 }
 
 
-const LayoutSelector: SFC<Props> = props => {
+const LayoutSelector: FC<Props> = props => {
   const { mode, onLayoutModeChanged } = props;
   const { mode, onLayoutModeChanged } = props;
   return (
   return (
     <div className="layout-selector">
     <div className="layout-selector">

+ 75 - 0
public/app/core/components/Page/Page.tsx

@@ -0,0 +1,75 @@
+// Libraries
+import React, { Component } from 'react';
+import config from 'app/core/config';
+import { NavModel } from 'app/types';
+import { getTitleFromNavModel } from 'app/core/selectors/navModel';
+
+// Components
+import PageHeader from '../PageHeader/PageHeader';
+import Footer from '../Footer/Footer';
+import PageContents from './PageContents';
+import { CustomScrollbar } from '@grafana/ui';
+
+interface Props {
+  title?: string;
+  children: JSX.Element[] | JSX.Element;
+  navModel: NavModel;
+}
+
+class Page extends Component<Props> {
+  private bodyClass = 'is-react';
+  private body = document.body;
+  static Header = PageHeader;
+  static Contents = PageContents;
+
+  componentDidMount() {
+    this.body.classList.add(this.bodyClass);
+    this.updateTitle();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.title !== this.props.title) {
+      this.updateTitle();
+    }
+  }
+
+  componentWillUnmount() {
+    this.body.classList.remove(this.bodyClass);
+  }
+
+  updateTitle = () => {
+    const title = this.getPageTitle;
+    document.title = title ? title + ' - Grafana' : 'Grafana';
+  }
+
+  get getPageTitle () {
+    const { navModel } = this.props;
+    if (navModel) {
+      return getTitleFromNavModel(navModel) || undefined;
+    }
+    return undefined;
+  }
+
+  render() {
+    const { navModel } = this.props;
+    const { buildInfo } = config;
+    return (
+        <div className="page-scrollbar-wrapper">
+          <CustomScrollbar autoHeightMin={'100%'}>
+            <div className="page-scrollbar-content">
+              <PageHeader model={navModel} />
+              {this.props.children}
+              <Footer
+                appName="Grafana"
+                buildCommit={buildInfo.commit}
+                buildVersion={buildInfo.version}
+                newGrafanaVersion={buildInfo.latestVersion}
+                newGrafanaVersionExists={buildInfo.hasUpdate} />
+            </div>
+          </CustomScrollbar>
+        </div>
+    );
+  }
+}
+
+export default Page;

+ 26 - 0
public/app/core/components/Page/PageContents.tsx

@@ -0,0 +1,26 @@
+// Libraries
+import React, { Component } from 'react';
+
+// Components
+import PageLoader from '../PageLoader/PageLoader';
+
+interface Props {
+  isLoading?: boolean;
+  children: JSX.Element[] | JSX.Element;
+}
+
+class PageContents extends Component<Props> {
+
+  render() {
+    const { isLoading } = this.props;
+
+    return (
+      <div className="page-container page-body">
+        {isLoading && <PageLoader />}
+        {this.props.children}
+      </div>
+    );
+  }
+}
+
+export default PageContents;

+ 3 - 3
public/app/core/components/PageHeader/PageHeader.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { FormEvent } from 'react';
 import { NavModel, NavModelItem } from 'app/types';
 import { NavModel, NavModelItem } from 'app/types';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
@@ -12,8 +12,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string
     return navItem.active === true;
     return navItem.active === true;
   });
   });
 
 
-  const gotoUrl = evt => {
-    const element = evt.target;
+  const gotoUrl = (evt: FormEvent) => {
+    const element = evt.target as HTMLSelectElement;
     const url = element.options[element.selectedIndex].value;
     const url = element.options[element.selectedIndex].value;
     appEvents.emit('location-change', { href: url });
     appEvents.emit('location-change', { href: url });
   };
   };

+ 3 - 3
public/app/core/components/PageLoader/PageLoader.tsx

@@ -1,10 +1,10 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 interface Props {
 interface Props {
-  pageName: string;
+  pageName?: string;
 }
 }
 
 
-const PageLoader: SFC<Props> = ({ pageName }) => {
+const PageLoader: FC<Props> = ({ pageName }) => {
   const loadingText = `Loading ${pageName}...`;
   const loadingText = `Loading ${pageName}...`;
   return (
   return (
     <div className="page-loader-wrapper">
     <div className="page-loader-wrapper">

+ 1 - 1
public/app/core/components/Select/DataSourcePicker.tsx

@@ -6,7 +6,7 @@ import _ from 'lodash';
 import { Select } from '@grafana/ui';
 import { Select } from '@grafana/ui';
 
 
 // Types
 // Types
-import { DataSourceSelectItem } from 'app/types';
+import { DataSourceSelectItem } from '@grafana/ui/src/types';
 
 
 export interface Props {
 export interface Props {
   onChange: (ds: DataSourceSelectItem) => void;
   onChange: (ds: DataSourceSelectItem) => void;

+ 3 - 4
public/app/core/components/SharedPreferences/SharedPreferences.tsx

@@ -1,7 +1,6 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
-import { Label } from 'app/core/components/Label/Label';
-import { Select } from '@grafana/ui';
+import { FormLabel, Select } from '@grafana/ui';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 
 
 import { DashboardSearchHit } from 'app/types';
 import { DashboardSearchHit } from 'app/types';
@@ -100,12 +99,12 @@ export class SharedPreferences extends PureComponent<Props, State> {
           />
           />
         </div>
         </div>
         <div className="gf-form">
         <div className="gf-form">
-          <Label
+          <FormLabel
             width={11}
             width={11}
             tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
             tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
           >
           >
             Home Dashboard
             Home Dashboard
-          </Label>
+          </FormLabel>
           <Select
           <Select
             value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
             value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
             getOptionValue={i => i.id}
             getOptionValue={i => i.id}

+ 2 - 2
public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx

@@ -1,4 +1,4 @@
-import React, { SFC, ReactNode, PureComponent } from 'react';
+import React, { FC, ReactNode, PureComponent } from 'react';
 import { Tooltip } from '@grafana/ui';
 import { Tooltip } from '@grafana/ui';
 
 
 interface ToggleButtonGroupProps {
 interface ToggleButtonGroupProps {
@@ -29,7 +29,7 @@ interface ToggleButtonProps {
   tooltip?: string;
   tooltip?: string;
 }
 }
 
 
-export const ToggleButton: SFC<ToggleButtonProps> = ({
+export const ToggleButton: FC<ToggleButtonProps> = ({
   children,
   children,
   selected,
   selected,
   className = '',
   className = '',

+ 2 - 2
public/app/core/components/sidemenu/DropDownChild.tsx

@@ -1,10 +1,10 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export interface Props {
 export interface Props {
   child: any;
   child: any;
 }
 }
 
 
-const DropDownChild: SFC<Props> = props => {
+const DropDownChild: FC<Props> = props => {
   const { child } = props;
   const { child } = props;
   const listItemClassName = child.divider ? 'divider' : '';
   const listItemClassName = child.divider ? 'divider' : '';
 
 

+ 2 - 2
public/app/core/components/sidemenu/SideMenuDropDown.tsx

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import DropDownChild from './DropDownChild';
 import DropDownChild from './DropDownChild';
 
 
 interface Props {
 interface Props {
   link: any;
   link: any;
 }
 }
 
 
-const SideMenuDropDown: SFC<Props> = props => {
+const SideMenuDropDown: FC<Props> = props => {
   const { link } = props;
   const { link } = props;
   return (
   return (
     <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
     <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">

+ 2 - 2
public/app/core/components/sidemenu/SignIn.tsx

@@ -1,6 +1,6 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
-const SignIn: SFC<any> = () => {
+const SignIn: FC<any> = () => {
   const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
   const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
   return (
   return (
     <div className="sidemenu-item">
     <div className="sidemenu-item">

+ 2 - 2
public/app/core/components/sidemenu/TopSection.tsx

@@ -1,9 +1,9 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 import TopSectionItem from './TopSectionItem';
 import TopSectionItem from './TopSectionItem';
 import config from '../../config';
 import config from '../../config';
 
 
-const TopSection: SFC<any> = () => {
+const TopSection: FC<any> = () => {
   const navTree = _.cloneDeep(config.bootData.navTree);
   const navTree = _.cloneDeep(config.bootData.navTree);
   const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
   const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
 
 

+ 2 - 2
public/app/core/components/sidemenu/TopSectionItem.tsx

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import SideMenuDropDown from './SideMenuDropDown';
 import SideMenuDropDown from './SideMenuDropDown';
 
 
 export interface Props {
 export interface Props {
   link: any;
   link: any;
 }
 }
 
 
-const TopSectionItem: SFC<Props> = props => {
+const TopSectionItem: FC<Props> = props => {
   const { link } = props;
   const { link } = props;
   return (
   return (
     <div className="sidemenu-item dropdown">
     <div className="sidemenu-item dropdown">

+ 2 - 0
public/app/core/config.ts

@@ -6,6 +6,8 @@ export interface BuildInfo {
   commit: string;
   commit: string;
   isEnterprise: boolean;
   isEnterprise: boolean;
   env: string;
   env: string;
+  latestVersion: string;
+  hasUpdate: boolean;
 }
 }
 
 
 export class Settings {
 export class Settings {

+ 1 - 0
public/app/core/core.ts

@@ -1,5 +1,6 @@
 import './directives/dash_class';
 import './directives/dash_class';
 import './directives/dropdown_typeahead';
 import './directives/dropdown_typeahead';
+import './directives/autofill_event_fix';
 import './directives/metric_segment';
 import './directives/metric_segment';
 import './directives/misc';
 import './directives/misc';
 import './directives/ng_model_on_blur';
 import './directives/ng_model_on_blur';

+ 35 - 0
public/app/core/directives/autofill_event_fix.ts

@@ -0,0 +1,35 @@
+import coreModule from '../core_module';
+
+/** @ngInject */
+export function autofillEventFix($compile) {
+  return {
+    link: ($scope: any, elem: any) => {
+      const input = elem[0];
+      const dispatchChangeEvent = () => {
+        const event = new Event('change');
+        return input.dispatchEvent(event);
+      };
+      const onAnimationStart = ({ animationName }: AnimationEvent) => {
+        switch (animationName) {
+          case 'onAutoFillStart':
+            return dispatchChangeEvent();
+          case 'onAutoFillCancel':
+            return dispatchChangeEvent();
+        }
+        return null;
+      };
+
+      // const onChange = (evt: Event) => console.log(evt);
+
+      input.addEventListener('animationstart', onAnimationStart);
+      // input.addEventListener('change', onChange);
+
+      $scope.$on('$destroy', () => {
+        input.removeEventListener('animationstart', onAnimationStart);
+        // input.removeEventListener('change', onChange);
+      });
+    }
+  };
+}
+
+coreModule.directive('autofillEventFix', autofillEventFix);

+ 20 - 9
public/app/core/directives/dropdown_typeahead.ts

@@ -141,6 +141,9 @@ export function dropdownTypeahead2($compile) {
     link: ($scope, elem, attrs) => {
     link: ($scope, elem, attrs) => {
       const $input = $(inputTemplate);
       const $input = $(inputTemplate);
       const $button = $(buttonTemplate);
       const $button = $(buttonTemplate);
+      const timeoutId = {
+        blur: null
+      };
       $input.appendTo(elem);
       $input.appendTo(elem);
       $button.appendTo(elem);
       $button.appendTo(elem);
 
 
@@ -177,6 +180,14 @@ export function dropdownTypeahead2($compile) {
         []
         []
       );
       );
 
 
+      const closeDropdownMenu = () => {
+        $input.hide();
+        $input.val('');
+        $button.show();
+        $button.focus();
+        elem.removeClass('open');
+      };
+
       $scope.menuItemSelected = (index, subIndex) => {
       $scope.menuItemSelected = (index, subIndex) => {
         const menuItem = $scope.menuItems[index];
         const menuItem = $scope.menuItems[index];
         const payload: any = { $item: menuItem };
         const payload: any = { $item: menuItem };
@@ -184,6 +195,7 @@ export function dropdownTypeahead2($compile) {
           payload.$subItem = menuItem.submenu[subIndex];
           payload.$subItem = menuItem.submenu[subIndex];
         }
         }
         $scope.dropdownTypeaheadOnSelect(payload);
         $scope.dropdownTypeaheadOnSelect(payload);
+        closeDropdownMenu();
       };
       };
 
 
       $input.attr('data-provide', 'typeahead');
       $input.attr('data-provide', 'typeahead');
@@ -223,16 +235,15 @@ export function dropdownTypeahead2($compile) {
         elem.toggleClass('open', $input.val() === '');
         elem.toggleClass('open', $input.val() === '');
       });
       });
 
 
+      elem.mousedown((evt: Event) => {
+        evt.preventDefault();
+        timeoutId.blur = null;
+      });
+
       $input.blur(() => {
       $input.blur(() => {
-        $input.hide();
-        $input.val('');
-        $button.show();
-        $button.focus();
-        // clicking the function dropdown menu won't
-        // work if you remove class at once
-        setTimeout(() => {
-          elem.removeClass('open');
-        }, 200);
+        timeoutId.blur = setTimeout(() => {
+          closeDropdownMenu();
+        }, 1);
       });
       });
 
 
       $compile(elem.contents())($scope);
       $compile(elem.contents())($scope);

+ 4 - 0
public/app/core/selectors/navModel.ts

@@ -41,3 +41,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel)
 
 
   return getNotFoundModel();
   return getNotFoundModel();
 }
 }
+
+export const getTitleFromNavModel = (navModel: NavModel) => {
+  return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`;
+};

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

@@ -2,6 +2,7 @@ import config from 'app/core/config';
 import _ from 'lodash';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import store from 'app/core/store';
 import store from 'app/core/store';
+import { ThemeNames, ThemeName } from '@grafana/ui';
 
 
 export class User {
 export class User {
   isGrafanaAdmin: any;
   isGrafanaAdmin: any;
@@ -59,6 +60,10 @@ export class ContextSrv {
     this.sidemenu = !this.sidemenu;
     this.sidemenu = !this.sidemenu;
     store.set('grafana.sidemenu', this.sidemenu);
     store.set('grafana.sidemenu', this.sidemenu);
   }
   }
+
+  getTheme(): ThemeName {
+    return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark;
+  }
 }
 }
 
 
 const contextSrv = new ContextSrv();
 const contextSrv = new ContextSrv();

+ 22 - 50
public/app/core/utils/explore.test.ts

@@ -6,26 +6,13 @@ import {
   clearHistory,
   clearHistory,
   hasNonEmptyQuery,
   hasNonEmptyQuery,
 } from './explore';
 } from './explore';
-import { ExploreState } from 'app/types/explore';
+import { ExploreUrlState } from 'app/types/explore';
 import store from 'app/core/store';
 import store from 'app/core/store';
 
 
-const DEFAULT_EXPLORE_STATE: ExploreState = {
+const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
   datasource: null,
   datasource: null,
-  datasourceError: null,
-  datasourceLoading: null,
-  datasourceMissing: false,
-  exploreDatasources: [],
-  graphInterval: 1000,
-  history: [],
-  initialQueries: [],
-  queryTransactions: [],
+  queries: [],
   range: DEFAULT_RANGE,
   range: DEFAULT_RANGE,
-  showingGraph: true,
-  showingLogs: true,
-  showingTable: true,
-  supportsGraph: null,
-  supportsLogs: null,
-  supportsTable: null,
 };
 };
 
 
 describe('state functions', () => {
 describe('state functions', () => {
@@ -68,21 +55,19 @@ describe('state functions', () => {
     it('returns url parameter value for a state object', () => {
     it('returns url parameter value for a state object', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        initialDatasource: 'foo',
-        range: {
-          from: 'now-5h',
-          to: 'now',
-        },
-        initialQueries: [
+        datasource: 'foo',
+        queries: [
           {
           {
-            refId: '1',
             expr: 'metric{test="a/b"}',
             expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            refId: '2',
             expr: 'super{foo="x/z"}',
             expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
+        range: {
+          from: 'now-5h',
+          to: 'now',
+        },
       };
       };
       expect(serializeStateToUrlParam(state)).toBe(
       expect(serializeStateToUrlParam(state)).toBe(
         '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
         '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
@@ -93,21 +78,19 @@ describe('state functions', () => {
     it('returns url parameter value for a state object', () => {
     it('returns url parameter value for a state object', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        initialDatasource: 'foo',
-        range: {
-          from: 'now-5h',
-          to: 'now',
-        },
-        initialQueries: [
+        datasource: 'foo',
+        queries: [
           {
           {
-            refId: '1',
             expr: 'metric{test="a/b"}',
             expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            refId: '2',
             expr: 'super{foo="x/z"}',
             expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
+        range: {
+          from: 'now-5h',
+          to: 'now',
+        },
       };
       };
       expect(serializeStateToUrlParam(state, true)).toBe(
       expect(serializeStateToUrlParam(state, true)).toBe(
         '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
         '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
@@ -119,35 +102,24 @@ describe('state functions', () => {
     it('can parse the serialized state into the original state', () => {
     it('can parse the serialized state into the original state', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        initialDatasource: 'foo',
-        range: {
-          from: 'now - 5h',
-          to: 'now',
-        },
-        initialQueries: [
+        datasource: 'foo',
+        queries: [
           {
           {
-            refId: '1',
             expr: 'metric{test="a/b"}',
             expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            refId: '2',
             expr: 'super{foo="x/z"}',
             expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
+        range: {
+          from: 'now - 5h',
+          to: 'now',
+        },
       };
       };
       const serialized = serializeStateToUrlParam(state);
       const serialized = serializeStateToUrlParam(state);
       const parsed = parseUrlState(serialized);
       const parsed = parseUrlState(serialized);
 
 
-      // Account for datasource vs datasourceName
-      const { datasource, queries, ...rest } = parsed;
-      const resultState = {
-        ...rest,
-        datasource: DEFAULT_EXPLORE_STATE.datasource,
-        initialDatasource: datasource,
-        initialQueries: queries,
-      };
-
-      expect(state).toMatchObject(resultState);
+      expect(state).toMatchObject(parsed);
     });
     });
   });
   });
 });
 });

+ 95 - 25
public/app/core/utils/explore.ts

@@ -1,16 +1,26 @@
+// Libraries
 import _ from 'lodash';
 import _ from 'lodash';
-import { colors } from '@grafana/ui';
 
 
+// Services & Utils
+import * as dateMath from 'app/core/utils/datemath';
 import { renderUrl } from 'app/core/utils/url';
 import { renderUrl } from 'app/core/utils/url';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
 import store from 'app/core/store';
 import store from 'app/core/store';
 import { parse as parseDate } from 'app/core/utils/datemath';
 import { parse as parseDate } from 'app/core/utils/datemath';
+import { colors } from '@grafana/ui';
+import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
 
 
+// Types
+import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types';
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
-import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
-import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
-import { DataQuery, DataSourceApi } from 'app/types/series';
-import { RawTimeRange, IntervalValues } from '@grafana/ui';
+import {
+  ExploreUrlState,
+  HistoryItem,
+  QueryTransaction,
+  ResultType,
+  QueryIntervals,
+  QueryOptions,
+} from 'app/types/explore';
 
 
 export const DEFAULT_RANGE = {
 export const DEFAULT_RANGE = {
   from: 'now-6h',
   from: 'now-6h',
@@ -19,6 +29,8 @@ export const DEFAULT_RANGE = {
 
 
 const MAX_HISTORY_ITEMS = 100;
 const MAX_HISTORY_ITEMS = 100;
 
 
+export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
+
 /**
 /**
  * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  *
  *
@@ -77,7 +89,63 @@ export async function getExploreUrl(
   return url;
   return url;
 }
 }
 
 
-const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
+export function buildQueryTransaction(
+  query: DataQuery,
+  rowIndex: number,
+  resultType: ResultType,
+  queryOptions: QueryOptions,
+  range: RawTimeRange,
+  queryIntervals: QueryIntervals,
+  scanning: boolean
+): QueryTransaction {
+  const { interval, intervalMs } = queryIntervals;
+
+  const configuredQueries = [
+    {
+      ...query,
+      ...queryOptions,
+    },
+  ];
+
+  // Clone range for query request
+  // const queryRange: RawTimeRange = { ...range };
+  // const { from, to, raw } = this.timeSrv.timeRange();
+  // Most datasource is using `panelId + query.refId` for cancellation logic.
+  // Using `format` here because it relates to the view panel that the request is for.
+  // However, some datasources don't use `panelId + query.refId`, but only `panelId`.
+  // Therefore panel id has to be unique.
+  const panelId = `${queryOptions.format}-${query.key}`;
+
+  const options = {
+    interval,
+    intervalMs,
+    panelId,
+    targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
+    range: {
+      from: dateMath.parse(range.from, false),
+      to: dateMath.parse(range.to, true),
+      raw: range,
+    },
+    rangeRaw: range,
+    scopedVars: {
+      __interval: { text: interval, value: interval },
+      __interval_ms: { text: intervalMs, value: intervalMs },
+    },
+  };
+
+  return {
+    options,
+    query,
+    resultType,
+    rowIndex,
+    scanning,
+    id: generateKey(), // reusing for unique ID
+    done: false,
+    latency: 0,
+  };
+}
+
+export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
 
 
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
   if (initial) {
   if (initial) {
@@ -103,12 +171,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
   return { datasource: null, queries: [], range: DEFAULT_RANGE };
   return { datasource: null, queries: [], range: DEFAULT_RANGE };
 }
 }
 
 
-export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
-  const urlState: ExploreUrlState = {
-    datasource: state.initialDatasource,
-    queries: state.initialQueries.map(clearQueryKeys),
-    range: state.range,
-  };
+export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
   if (compact) {
   if (compact) {
     return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
     return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
   }
   }
@@ -123,7 +186,7 @@ export function generateRefId(index = 0): string {
   return `${index + 1}`;
   return `${index + 1}`;
 }
 }
 
 
-export function generateQueryKeys(index = 0): { refId: string; key: string } {
+export function generateEmptyQuery(index = 0): { refId: string; key: string } {
   return { refId: generateRefId(index), key: generateKey(index) };
   return { refId: generateRefId(index), key: generateKey(index) };
 }
 }
 
 
@@ -132,20 +195,23 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } {
  */
  */
 export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
 export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
   if (queries && typeof queries === 'object' && queries.length > 0) {
   if (queries && typeof queries === 'object' && queries.length > 0) {
-    return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
+    return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
   }
   }
-  return [{ ...generateQueryKeys() }];
+  return [{ ...generateEmptyQuery() }];
 }
 }
 
 
 /**
 /**
  * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  */
  */
-export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
-  return queries.some(
-    query =>
-      Object.keys(query)
-        .map(k => query[k])
-        .filter(v => v).length > 2
+export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean {
+  return (
+    queries &&
+    queries.some(
+      query =>
+        Object.keys(query)
+          .map(k => query[k])
+          .filter(v => v).length > 2
+    )
   );
   );
 }
 }
 
 
@@ -180,8 +246,8 @@ export function calculateResultsFromQueryTransactions(
   };
   };
 }
 }
 
 
-export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
-  if (!datasource || !resolution) {
+export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
+  if (!resolution) {
     return { interval: '1s', intervalMs: 1000 };
     return { interval: '1s', intervalMs: 1000 };
   }
   }
 
 
@@ -190,7 +256,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res
     to: parseDate(range.to, true),
     to: parseDate(range.to, true),
   };
   };
 
 
-  return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
+  return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
 }
 }
 
 
 export function makeTimeSeriesList(dataList) {
 export function makeTimeSeriesList(dataList) {
@@ -214,7 +280,11 @@ export function makeTimeSeriesList(dataList) {
 /**
 /**
  * Update the query history. Side-effect: store history in local storage
  * 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();
   const ts = Date.now();
   queries.forEach(query => {
   queries.forEach(query => {
     history = [{ query, ts }, ...history];
     history = [{ query, ts }, ...history];

+ 8 - 1
public/app/features/api-keys/ApiKeysPage.test.tsx

@@ -6,7 +6,14 @@ import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Api Keys'
+      }
+    } as NavModel,
     apiKeys: [] as ApiKey[],
     apiKeys: [] as ApiKey[],
     searchQuery: '',
     searchQuery: '',
     hasFetched: false,
     hasFetched: false,

+ 12 - 14
public/app/features/api-keys/ApiKeysPage.tsx

@@ -6,8 +6,7 @@ import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getApiKeys, getApiKeysCount } from './state/selectors';
 import { getApiKeys, getApiKeysCount } from './state/selectors';
 import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
 import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
+import Page from 'app/core/components/Page/Page';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import ApiKeysAddedModal from './ApiKeysAddedModal';
 import ApiKeysAddedModal from './ApiKeysAddedModal';
 import config from 'app/core/config';
 import config from 'app/core/config';
@@ -240,18 +239,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
     const { hasFetched, navModel, apiKeysCount } = this.props;
     const { hasFetched, navModel, apiKeysCount } = this.props;
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        {hasFetched ? (
-          apiKeysCount > 0 ? (
-            this.renderApiKeyList()
-          ) : (
-            this.renderEmptyList()
-          )
-        ) : (
-          <PageLoader pageName="Api keys" />
-        )}
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          {hasFetched && (
+            apiKeysCount > 0 ? (
+              this.renderApiKeyList()
+            ) : (
+              this.renderEmptyList()
+            )
+          )}
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }

+ 130 - 110
public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap

@@ -1,132 +1,152 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render API keys table if there are any keys 1`] = `
 exports[`Render should render API keys table if there are any keys 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Api Keys",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   />
   />
-  <PageLoader
-    pageName="Api keys"
-  />
-</div>
+</Page>
 `;
 `;
 
 
 exports[`Render should render CTA if there are no API keys 1`] = `
 exports[`Render should render CTA if there are no API keys 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Api Keys",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
-    <EmptyListCTA
-      model={
-        Object {
-          "buttonIcon": "fa fa-plus",
-          "buttonLink": "#",
-          "buttonTitle": " New API Key",
-          "onClick": [Function],
-          "proTip": "Remember you can provide view-only API access to other applications.",
-          "proTipLink": "",
-          "proTipLinkTitle": "",
-          "proTipTarget": "_blank",
-          "title": "You haven't added any API Keys yet.",
-        }
-      }
-    />
-    <Component
-      in={false}
+    <div
+      className="page-container page-body"
     >
     >
-      <div
-        className="cta-form"
+      <EmptyListCTA
+        model={
+          Object {
+            "buttonIcon": "fa fa-plus",
+            "buttonLink": "#",
+            "buttonTitle": " New API Key",
+            "onClick": [Function],
+            "proTip": "Remember you can provide view-only API access to other applications.",
+            "proTipLink": "",
+            "proTipLinkTitle": "",
+            "proTipTarget": "_blank",
+            "title": "You haven't added any API Keys yet.",
+          }
+        }
+      />
+      <Component
+        in={false}
       >
       >
-        <button
-          className="cta-form__close btn btn-transparent"
-          onClick={[Function]}
-        >
-          <i
-            className="fa fa-close"
-          />
-        </button>
-        <h5>
-          Add API Key
-        </h5>
-        <form
-          className="gf-form-group"
-          onSubmit={[Function]}
+        <div
+          className="cta-form"
         >
         >
-          <div
-            className="gf-form-inline"
+          <button
+            className="cta-form__close btn btn-transparent"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-close"
+            />
+          </button>
+          <h5>
+            Add API Key
+          </h5>
+          <form
+            className="gf-form-group"
+            onSubmit={[Function]}
           >
           >
             <div
             <div
-              className="gf-form max-width-21"
-            >
-              <span
-                className="gf-form-label"
-              >
-                Key name
-              </span>
-              <input
-                className="gf-form-input"
-                onChange={[Function]}
-                placeholder="Name"
-                type="text"
-                value=""
-              />
-            </div>
-            <div
-              className="gf-form"
+              className="gf-form-inline"
             >
             >
-              <span
-                className="gf-form-label"
+              <div
+                className="gf-form max-width-21"
               >
               >
-                Role
-              </span>
-              <span
-                className="gf-form-select-wrapper"
-              >
-                <select
-                  className="gf-form-input gf-size-auto"
+                <span
+                  className="gf-form-label"
+                >
+                  Key name
+                </span>
+                <input
+                  className="gf-form-input"
                   onChange={[Function]}
                   onChange={[Function]}
-                  value="Viewer"
+                  placeholder="Name"
+                  type="text"
+                  value=""
+                />
+              </div>
+              <div
+                className="gf-form"
+              >
+                <span
+                  className="gf-form-label"
                 >
                 >
-                  <option
-                    key="Viewer"
-                    label="Viewer"
+                  Role
+                </span>
+                <span
+                  className="gf-form-select-wrapper"
+                >
+                  <select
+                    className="gf-form-input gf-size-auto"
+                    onChange={[Function]}
                     value="Viewer"
                     value="Viewer"
                   >
                   >
-                    Viewer
-                  </option>
-                  <option
-                    key="Editor"
-                    label="Editor"
-                    value="Editor"
-                  >
-                    Editor
-                  </option>
-                  <option
-                    key="Admin"
-                    label="Admin"
-                    value="Admin"
-                  >
-                    Admin
-                  </option>
-                </select>
-              </span>
-            </div>
-            <div
-              className="gf-form"
-            >
-              <button
-                className="btn gf-form-btn btn-success"
+                    <option
+                      key="Viewer"
+                      label="Viewer"
+                      value="Viewer"
+                    >
+                      Viewer
+                    </option>
+                    <option
+                      key="Editor"
+                      label="Editor"
+                      value="Editor"
+                    >
+                      Editor
+                    </option>
+                    <option
+                      key="Admin"
+                      label="Admin"
+                      value="Admin"
+                    >
+                      Admin
+                    </option>
+                  </select>
+                </span>
+              </div>
+              <div
+                className="gf-form"
               >
               >
-                Add
-              </button>
+                <button
+                  className="btn gf-form-btn btn-success"
+                >
+                  Add
+                </button>
+              </div>
             </div>
             </div>
-          </div>
-        </form>
-      </div>
-    </Component>
-  </div>
-</div>
+          </form>
+        </div>
+      </Component>
+    </div>
+  </PageContents>
+</Page>
 `;
 `;

+ 1 - 2
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -12,8 +12,7 @@ import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
 
 
 // Types
 // Types
-import { DataQueryOptions, DataQueryResponse } from 'app/types';
-import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui';
+import { TimeRange, TimeSeries, LoadingState, DataQueryResponse, DataQueryOptions } from '@grafana/ui/src/types';
 
 
 const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
 const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
 
 

+ 7 - 1
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -20,6 +20,7 @@ import { PanelPlugin } from 'app/types';
 import { TimeRange } from '@grafana/ui';
 import { TimeRange } from '@grafana/ui';
 
 
 import variables from 'sass/_variables.scss';
 import variables from 'sass/_variables.scss';
+import templateSrv from 'app/features/templating/template_srv';
 
 
 export interface Props {
 export interface Props {
   panel: PanelModel;
   panel: PanelModel;
@@ -78,6 +79,10 @@ export class PanelChrome extends PureComponent<Props, State> {
     });
     });
   };
   };
 
 
+  onInterpolate = (value: string, format?: string) => {
+    return templateSrv.replace(value, this.props.panel.scopedVars, format);
+  };
+
   get isVisible() {
   get isVisible() {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
   }
@@ -124,9 +129,10 @@ export class PanelChrome extends PureComponent<Props, State> {
                         timeSeries={timeSeries}
                         timeSeries={timeSeries}
                         timeRange={timeRange}
                         timeRange={timeRange}
                         options={panel.getOptions(plugin.exports.PanelDefaults)}
                         options={panel.getOptions(plugin.exports.PanelDefaults)}
-                        width={width - 2 * variables.panelHorizontalPadding }
+                        width={width - 2 * variables.panelHorizontalPadding}
                         height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
                         height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
                         renderCounter={renderCounter}
                         renderCounter={renderCounter}
+                        onInterpolate={this.onInterpolate}
                       />
                       />
                     </div>
                     </div>
                   );
                   );

+ 5 - 2
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -3,6 +3,7 @@ import classNames from 'classnames';
 
 
 import PanelHeaderCorner from './PanelHeaderCorner';
 import PanelHeaderCorner from './PanelHeaderCorner';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
+import templateSrv from 'app/features/templating/template_srv';
 
 
 import { DashboardModel } from 'app/features/dashboard/dashboard_model';
 import { DashboardModel } from 'app/features/dashboard/dashboard_model';
 import { PanelModel } from 'app/features/dashboard/panel_model';
 import { PanelModel } from 'app/features/dashboard/panel_model';
@@ -45,7 +46,9 @@ export class PanelHeader extends Component<Props, State> {
     const isFullscreen = false;
     const isFullscreen = false;
     const isLoading = false;
     const isLoading = false;
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
-    const { panel, dashboard, timeInfo } = this.props;
+    const { panel, dashboard, timeInfo, scopedVars } = this.props;
+    const title = templateSrv.replaceWithText(panel.title, scopedVars);
+
     return (
     return (
       <>
       <>
         <PanelHeaderCorner
         <PanelHeaderCorner
@@ -65,7 +68,7 @@ export class PanelHeader extends Component<Props, State> {
             <div className="panel-title">
             <div className="panel-title">
               <span className="icon-gf panel-alert-icon" />
               <span className="icon-gf panel-alert-icon" />
               <span className="panel-title-text">
               <span className="panel-title-text">
-                {panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
+                {title} <span className="fa fa-caret-down panel-menu-toggle" />
               </span>
               </span>
 
 
               {this.state.panelMenuOpen && (
               {this.state.panelMenuOpen && (

+ 2 - 2
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { PanelMenuItem } from '@grafana/ui';
 import { PanelMenuItem } from '@grafana/ui';
 
 
 interface Props {
 interface Props {
   children: any;
   children: any;
 }
 }
 
 
-export const PanelHeaderMenuItem: SFC<Props & PanelMenuItem> = props => {
+export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
   const isSubMenu = props.type === 'submenu';
   const isSubMenu = props.type === 'submenu';
   const isDivider = props.type === 'divider';
   const isDivider = props.type === 'divider';
   return isDivider ? (
   return isDivider ? (

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

@@ -15,7 +15,7 @@ interface State {
 }
 }
 
 
 export class PanelResizer extends PureComponent<Props, 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;
   prevEditorHeight: number;
   throttledChangeHeight: (height: number) => void;
   throttledChangeHeight: (height: number) => void;
   throttledResizeDone: () => void;
   throttledResizeDone: () => void;

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

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { Tooltip } from '@grafana/ui';
 import { Tooltip } from '@grafana/ui';
 
 
 interface Props {
 interface Props {
@@ -10,7 +10,7 @@ interface Props {
   tooltipInfo?: any;
   tooltipInfo?: any;
 }
 }
 
 
-export const DataSourceOptions: SFC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
+export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
   const dsOption = (
   const dsOption = (
     <div className="gf-form gf-form--flex-end">
     <div className="gf-form gf-form--flex-end">
       <label className="gf-form-label">{label}</label>
       <label className="gf-form-label">{label}</label>

+ 9 - 10
public/app/features/dashboard/panel_editor/EditorTabBody.tsx

@@ -10,6 +10,8 @@ interface Props {
   heading: string;
   heading: string;
   renderToolbar?: () => JSX.Element;
   renderToolbar?: () => JSX.Element;
   toolbarItems?: EditorToolbarView[];
   toolbarItems?: EditorToolbarView[];
+  scrollTop?: number;
+  setScrollTop?: (value: React.MouseEvent<HTMLElement>) => void;
 }
 }
 
 
 export interface EditorToolbarView {
 export interface EditorToolbarView {
@@ -103,23 +105,20 @@ export class EditorTabBody extends PureComponent<Props, State> {
   }
   }
 
 
   render() {
   render() {
-    const { children, renderToolbar, heading, toolbarItems } = this.props;
+    const { children, renderToolbar, heading, toolbarItems, scrollTop, setScrollTop } = this.props;
     const { openView, fadeIn, isOpen } = this.state;
     const { openView, fadeIn, isOpen } = this.state;
 
 
     return (
     return (
       <>
       <>
         <div className="toolbar">
         <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>
         <div className="panel-editor__scroll">
         <div className="panel-editor__scroll">
-          <CustomScrollbar autoHide={false}>
+          <CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
             <div className="panel-editor__content">
             <div className="panel-editor__content">
               <FadeIn in={isOpen} duration={200} unmountOnExit={true}>
               <FadeIn in={isOpen} duration={200} unmountOnExit={true}>
                 {openView && this.renderOpenView(openView)}
                 {openView && this.renderOpenView(openView)}

+ 54 - 86
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -3,24 +3,22 @@ import React, { PureComponent } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 // Components
 // Components
-import 'app/features/panel/metrics_tab';
 import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
 import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { QueryInspector } from './QueryInspector';
 import { QueryInspector } from './QueryInspector';
 import { QueryOptions } from './QueryOptions';
 import { QueryOptions } from './QueryOptions';
-import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab';
 import { PanelOptionsGroup } from '@grafana/ui';
 import { PanelOptionsGroup } from '@grafana/ui';
+import { QueryEditorRow } from './QueryEditorRow';
 
 
 // Services
 // Services
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
 import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
-import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
 // Types
 // Types
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
 import { DashboardModel } from '../dashboard_model';
-import { DataQuery, DataSourceSelectItem } from 'app/types';
+import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 
 
 interface Props {
 interface Props {
@@ -34,66 +32,27 @@ interface State {
   isLoadingHelp: boolean;
   isLoadingHelp: boolean;
   isPickerOpen: boolean;
   isPickerOpen: boolean;
   isAddingMixed: boolean;
   isAddingMixed: boolean;
+  scrollTop: number;
 }
 }
 
 
 export class QueriesTab extends PureComponent<Props, State> {
 export class QueriesTab extends PureComponent<Props, State> {
-  element: HTMLElement;
-  component: AngularComponent;
   datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
   datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
   backendSrv: BackendSrv = getBackendSrv();
   backendSrv: BackendSrv = getBackendSrv();
 
 
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isLoadingHelp: false,
-      currentDS: this.findCurrentDataSource(),
-      helpContent: null,
-      isPickerOpen: false,
-      isAddingMixed: false,
-    };
-  }
+  state: State = {
+    isLoadingHelp: false,
+    currentDS: this.findCurrentDataSource(),
+    helpContent: null,
+    isPickerOpen: false,
+    isAddingMixed: false,
+    scrollTop: 0,
+  };
 
 
   findCurrentDataSource(): DataSourceSelectItem {
   findCurrentDataSource(): DataSourceSelectItem {
     const { panel } = this.props;
     const { panel } = this.props;
     return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0];
     return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0];
   }
   }
 
 
-  getAngularQueryComponentScope(): AngularQueryComponentScope {
-    const { panel, dashboard } = this.props;
-
-    return {
-      panel: panel,
-      dashboard: dashboard,
-      refresh: () => panel.refresh(),
-      render: () => panel.render,
-      addQuery: this.onAddQuery,
-      moveQuery: this.onMoveQuery,
-      removeQuery: this.onRemoveQuery,
-      events: panel.events,
-    };
-  }
-
-  componentDidMount() {
-    if (!this.element) {
-      return;
-    }
-
-    const loader = getAngularLoader();
-    const template = '<metrics-tab />';
-    const scopeProps = {
-      ctrl: this.getAngularQueryComponentScope(),
-    };
-
-    this.component = loader.load(this.element, scopeProps, template);
-  }
-
-  componentWillUnmount() {
-    if (this.component) {
-      this.component.destroy();
-    }
-  }
-
   onChangeDataSource = datasource => {
   onChangeDataSource = datasource => {
     const { panel } = this.props;
     const { panel } = this.props;
     const { currentDS } = this.state;
     const { currentDS } = this.state;
@@ -137,7 +96,7 @@ export class QueriesTab extends PureComponent<Props, State> {
 
 
   onAddQuery = (query?: Partial<DataQuery>) => {
   onAddQuery = (query?: Partial<DataQuery>) => {
     this.props.panel.addQuery(query);
     this.props.panel.addQuery(query);
-    this.forceUpdate();
+    this.setState({ scrollTop: this.state.scrollTop + 100000 });
   };
   };
 
 
   onAddQueryClick = () => {
   onAddQueryClick = () => {
@@ -146,9 +105,7 @@ export class QueriesTab extends PureComponent<Props, State> {
       return;
       return;
     }
     }
 
 
-    this.props.panel.addQuery();
-    this.component.digest();
-    this.forceUpdate();
+    this.onAddQuery();
   };
   };
 
 
   onRemoveQuery = (query: DataQuery) => {
   onRemoveQuery = (query: DataQuery) => {
@@ -171,9 +128,20 @@ export class QueriesTab extends PureComponent<Props, State> {
   };
   };
 
 
   renderToolbar = () => {
   renderToolbar = () => {
-    const { currentDS } = this.state;
+    const { currentDS, isAddingMixed } = this.state;
 
 
-    return <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />;
+    return (
+      <>
+        <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
+        <div className="flex-grow" />
+        {!isAddingMixed && (
+          <button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
+            Add Query
+          </button>
+        )}
+        {isAddingMixed && this.renderMixedPicker()}
+      </>
+    );
   };
   };
 
 
   renderMixedPicker = () => {
   renderMixedPicker = () => {
@@ -190,17 +158,21 @@ export class QueriesTab extends PureComponent<Props, State> {
 
 
   onAddMixedQuery = datasource => {
   onAddMixedQuery = datasource => {
     this.onAddQuery({ datasource: datasource.name });
     this.onAddQuery({ datasource: datasource.name });
-    this.component.digest();
-    this.setState({ isAddingMixed: false });
+    this.setState({ isAddingMixed: false, scrollTop: this.state.scrollTop + 10000 });
   };
   };
 
 
   onMixedPickerBlur = () => {
   onMixedPickerBlur = () => {
     this.setState({ isAddingMixed: false });
     this.setState({ isAddingMixed: false });
   };
   };
 
 
+  setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
+    const target = event.target as HTMLElement;
+    this.setState({ scrollTop: target.scrollTop });
+  };
+
   render() {
   render() {
     const { panel } = this.props;
     const { panel } = this.props;
-    const { currentDS, isAddingMixed } = this.state;
+    const { currentDS, scrollTop } = this.state;
 
 
     const queryInspector: EditorToolbarView = {
     const queryInspector: EditorToolbarView = {
       title: 'Query Inspector',
       title: 'Query Inspector',
@@ -214,32 +186,28 @@ export class QueriesTab extends PureComponent<Props, State> {
     };
     };
 
 
     return (
     return (
-      <EditorTabBody heading="Queries" renderToolbar={this.renderToolbar} toolbarItems={[queryInspector, dsHelp]}>
+      <EditorTabBody
+        heading="Queries to"
+        renderToolbar={this.renderToolbar}
+        toolbarItems={[queryInspector, dsHelp]}
+        setScrollTop={this.setScrollTop}
+        scrollTop={scrollTop}
+      >
         <>
         <>
-          <PanelOptionsGroup>
-            <div className="query-editor-rows">
-              <div ref={element => (this.element = element)} />
-
-              <div className="gf-form-query">
-                <div className="gf-form gf-form-query-letter-cell">
-                  <label className="gf-form-label">
-                    <span className="gf-form-query-letter-cell-carret muted">
-                      <i className="fa fa-caret-down" />
-                    </span>{' '}
-                    <span className="gf-form-query-letter-cell-letter">{panel.getNextQueryLetter()}</span>
-                  </label>
-                </div>
-                <div className="gf-form">
-                  {!isAddingMixed && (
-                    <button className="btn btn-secondary gf-form-btn" onClick={this.onAddQueryClick}>
-                      Add Query
-                    </button>
-                  )}
-                  {isAddingMixed && this.renderMixedPicker()}
-                </div>
-              </div>
-            </div>
-          </PanelOptionsGroup>
+          <div className="query-editor-rows">
+            {panel.targets.map((query, index) => (
+              <QueryEditorRow
+                datasourceName={query.datasource || panel.datasource}
+                key={query.refId}
+                panel={panel}
+                query={query}
+                onRemoveQuery={this.onRemoveQuery}
+                onAddQuery={this.onAddQuery}
+                onMoveQuery={this.onMoveQuery}
+                inMixedMode={currentDS.meta.mixed}
+              />
+            ))}
+          </div>
           <PanelOptionsGroup>
           <PanelOptionsGroup>
             <QueryOptions panel={panel} datasource={currentDS} />
             <QueryOptions panel={panel} datasource={currentDS} />
           </PanelOptionsGroup>
           </PanelOptionsGroup>

+ 254 - 0
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -0,0 +1,254 @@
+// Libraries
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import _ from 'lodash';
+
+// Utils & Services
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
+import { Emitter } from 'app/core/utils/emitter';
+
+// Types
+import { PanelModel } from '../panel_model';
+import { DataQuery, DataSourceApi } from '@grafana/ui';
+
+interface Props {
+  panel: PanelModel;
+  query: DataQuery;
+  onAddQuery: (query?: DataQuery) => void;
+  onRemoveQuery: (query: DataQuery) => void;
+  onMoveQuery: (query: DataQuery, direction: number) => void;
+  datasourceName: string | null;
+  inMixedMode: boolean;
+}
+
+interface State {
+  datasource: DataSourceApi | null;
+  isCollapsed: boolean;
+  angularScope: AngularQueryComponentScope | null;
+}
+
+export class QueryEditorRow extends PureComponent<Props, State> {
+  element: HTMLElement | null = null;
+  angularQueryEditor: AngularComponent | null = null;
+
+  state: State = {
+    datasource: null,
+    isCollapsed: false,
+    angularScope: null,
+  };
+
+  componentDidMount() {
+    this.loadDatasource();
+  }
+
+  getAngularQueryComponentScope(): AngularQueryComponentScope {
+    const { panel, query } = this.props;
+    const { datasource } = this.state;
+
+    return {
+      datasource: datasource,
+      target: query,
+      panel: panel,
+      refresh: () => panel.refresh(),
+      render: () => panel.render(),
+      events: panel.events,
+    };
+  }
+
+  async loadDatasource() {
+    const { query, panel } = this.props;
+    const dataSourceSrv = getDatasourceSrv();
+    const datasource = await dataSourceSrv.get(query.datasource || panel.datasource);
+
+    this.setState({ datasource });
+  }
+
+  componentDidUpdate() {
+    const { datasource } = this.state;
+
+    // check if we need to load another datasource
+    if (datasource && datasource.name !== this.props.datasourceName) {
+      if (this.angularQueryEditor) {
+        this.angularQueryEditor.destroy();
+        this.angularQueryEditor = null;
+      }
+      this.loadDatasource();
+      return;
+    }
+
+    if (!this.element || this.angularQueryEditor) {
+      return;
+    }
+
+    const loader = getAngularLoader();
+    const template = '<plugin-component type="query-ctrl" />';
+    const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
+
+    this.angularQueryEditor = loader.load(this.element, scopeProps, template);
+
+    // give angular time to compile
+    setTimeout(() => {
+      this.setState({ angularScope: scopeProps.ctrl });
+    }, 10);
+  }
+
+  componentWillUnmount() {
+    if (this.angularQueryEditor) {
+      this.angularQueryEditor.destroy();
+    }
+  }
+
+  onToggleCollapse = () => {
+    this.setState({ isCollapsed: !this.state.isCollapsed });
+  };
+
+  onQueryChange = (query: DataQuery) => {
+    Object.assign(this.props.query, query);
+    this.onExecuteQuery();
+  };
+
+  onExecuteQuery = () => {
+    this.props.panel.refresh();
+  };
+
+  renderPluginEditor() {
+    const { query } = this.props;
+    const { datasource } = this.state;
+
+    if (datasource.pluginExports.QueryCtrl) {
+      return <div ref={element => (this.element = element)} />;
+    }
+
+    if (datasource.pluginExports.QueryEditor) {
+      const QueryEditor = datasource.pluginExports.QueryEditor;
+      return (
+        <QueryEditor
+          query={query}
+          datasource={datasource}
+          onQueryChange={this.onQueryChange}
+          onExecuteQuery={this.onExecuteQuery}
+        />
+      );
+    }
+
+    return <div>Data source plugin does not export any Query Editor component</div>;
+  }
+
+  onToggleEditMode = () => {
+    const { angularScope } = this.state;
+
+    if (angularScope && angularScope.toggleEditorMode) {
+      angularScope.toggleEditorMode();
+      this.angularQueryEditor.digest();
+    }
+
+    if (this.state.isCollapsed) {
+      this.setState({ isCollapsed: false });
+    }
+  };
+
+  get hasTextEditMode() {
+    const { angularScope } = this.state;
+    return angularScope && angularScope.toggleEditorMode;
+  }
+
+  onRemoveQuery = () => {
+    this.props.onRemoveQuery(this.props.query);
+  };
+
+  onCopyQuery = () => {
+    const copy = _.cloneDeep(this.props.query);
+    this.props.onAddQuery(copy);
+  };
+
+  onDisableQuery = () => {
+    this.props.query.hide = !this.props.query.hide;
+    this.forceUpdate();
+  };
+
+  renderCollapsedText(): string | null {
+    const { angularScope } = this.state;
+
+    if (angularScope && angularScope.getCollapsedText) {
+      return angularScope.getCollapsedText();
+    }
+
+    return null;
+  }
+
+  render() {
+    const { query, datasourceName, inMixedMode } = this.props;
+    const { datasource, isCollapsed } = this.state;
+    const isDisabled = query.hide;
+
+    const bodyClasses = classNames('query-editor-row__body gf-form-query', {
+      'query-editor-row__body--collapsed': isCollapsed,
+    });
+
+    const rowClasses = classNames('query-editor-row', {
+      'query-editor-row--disabled': isDisabled,
+      'gf-form-disabled': isDisabled,
+    });
+
+    if (!datasource) {
+      return null;
+    }
+
+    return (
+      <div className={rowClasses}>
+        <div className="query-editor-row__header">
+          <div className="query-editor-row__ref-id" onClick={this.onToggleCollapse}>
+            {isCollapsed && <i className="fa fa-caret-right" />}
+            {!isCollapsed && <i className="fa fa-caret-down" />}
+            <span>{query.refId}</span>
+            {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" onClick={this.onToggleEditMode}>
+            {isCollapsed && <div>{this.renderCollapsedText()}</div>}
+          </div>
+          <div className="query-editor-row__actions">
+            {this.hasTextEditMode && (
+              <button
+                className="query-editor-row__action"
+                onClick={this.onToggleEditMode}
+                title="Toggle text edit mode"
+              >
+                <i className="fa fa-fw fa-pencil" />
+              </button>
+            )}
+            <button className="query-editor-row__action" onClick={() => this.props.onMoveQuery(query, 1)}>
+              <i className="fa fa-fw fa-arrow-down" />
+            </button>
+            <button className="query-editor-row__action" onClick={() => this.props.onMoveQuery(query, -1)}>
+              <i className="fa fa-fw fa-arrow-up" />
+            </button>
+            <button className="query-editor-row__action" onClick={this.onCopyQuery} title="Duplicate query">
+              <i className="fa fa-fw fa-copy" />
+            </button>
+            <button className="query-editor-row__action" onClick={this.onDisableQuery} title="Disable/enable query">
+              {isDisabled && <i className="fa fa-fw fa-eye-slash" />}
+              {!isDisabled && <i className="fa fa-fw fa-eye" />}
+            </button>
+            <button className="query-editor-row__action" onClick={this.onRemoveQuery} title="Remove query">
+              <i className="fa fa-fw fa-trash" />
+            </button>
+          </div>
+        </div>
+        <div className={bodyClasses}>{this.renderPluginEditor()}</div>
+      </div>
+    );
+  }
+}
+
+export interface AngularQueryComponentScope {
+  target: DataQuery;
+  panel: PanelModel;
+  events: Emitter;
+  refresh: () => void;
+  render: () => void;
+  datasource: DataSourceApi;
+  toggleEditorMode?: () => void;
+  getCollapsedText?: () => string;
+}

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

@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
 
 
   render() {
   render() {
     const { response, isLoading } = this.state.dsQuery;
     const { response, isLoading } = this.state.dsQuery;
-    const { isMocking } = this.state;
     const openNodes = this.getNrOfOpenNodes();
     const openNodes = this.getNrOfOpenNodes();
 
 
     if (isLoading) {
     if (isLoading) {
@@ -199,20 +198,7 @@ export class QueryInspector extends PureComponent<Props, State> {
           </CopyToClipboard>
           </CopyToClipboard>
         </div>
         </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} />
       </>
       </>
     );
     );
   }
   }

+ 4 - 3
public/app/features/dashboard/panel_editor/QueryOptions.tsx

@@ -10,11 +10,12 @@ import { Input } from 'app/core/components/Form';
 import { EventsWithValidation } from 'app/core/components/Form/Input';
 import { EventsWithValidation } from 'app/core/components/Form/Input';
 import { InputStatus } from 'app/core/components/Form/Input';
 import { InputStatus } from 'app/core/components/Form/Input';
 import DataSourceOption from './DataSourceOption';
 import DataSourceOption from './DataSourceOption';
-import { GfFormLabel } from '@grafana/ui';
+import { FormLabel } from '@grafana/ui';
 
 
 // Types
 // Types
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
-import { ValidationEvents, DataSourceSelectItem } from 'app/types';
+import { DataSourceSelectItem } from '@grafana/ui/src/types';
+import { ValidationEvents } from 'app/types';
 
 
 const timeRangeValidationEvents: ValidationEvents = {
 const timeRangeValidationEvents: ValidationEvents = {
   [EventsWithValidation.onBlur]: [
   [EventsWithValidation.onBlur]: [
@@ -164,7 +165,7 @@ export class QueryOptions extends PureComponent<Props, State> {
         {this.renderOptions()}
         {this.renderOptions()}
 
 
         <div className="gf-form">
         <div className="gf-form">
-          <GfFormLabel>Relative time</GfFormLabel>
+          <FormLabel>Relative time</FormLabel>
           <Input
           <Input
             type="text"
             type="text"
             className="width-6"
             className="width-6"

+ 11 - 3
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -26,6 +26,7 @@ interface Props {
 interface State {
 interface State {
   isVizPickerOpen: boolean;
   isVizPickerOpen: boolean;
   searchQuery: string;
   searchQuery: string;
+  scrollTop: number;
 }
 }
 
 
 export class VisualizationTab extends PureComponent<Props, State> {
 export class VisualizationTab extends PureComponent<Props, State> {
@@ -39,6 +40,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
     this.state = {
     this.state = {
       isVizPickerOpen: false,
       isVizPickerOpen: false,
       searchQuery: '',
       searchQuery: '',
+      scrollTop: 0,
     };
     };
   }
   }
 
 
@@ -143,7 +145,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
   };
   };
 
 
   onOpenVizPicker = () => {
   onOpenVizPicker = () => {
-    this.setState({ isVizPickerOpen: true });
+    this.setState({ isVizPickerOpen: true, scrollTop: 0 });
   };
   };
 
 
   onCloseVizPicker = () => {
   onCloseVizPicker = () => {
@@ -201,9 +203,14 @@ export class VisualizationTab extends PureComponent<Props, State> {
 
 
   renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />;
   renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />;
 
 
+  setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
+    const target = event.target as HTMLElement;
+    this.setState({ scrollTop: target.scrollTop });
+  };
+
   render() {
   render() {
     const { plugin } = this.props;
     const { plugin } = this.props;
-    const { isVizPickerOpen, searchQuery } = this.state;
+    const { isVizPickerOpen, searchQuery, scrollTop } = this.state;
 
 
     const pluginHelp: EditorToolbarView = {
     const pluginHelp: EditorToolbarView = {
       heading: 'Help',
       heading: 'Help',
@@ -212,7 +219,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
     };
     };
 
 
     return (
     return (
-      <EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}>
+      <EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}
+        scrollTop={scrollTop} setScrollTop={this.setScrollTop}>
         <>
         <>
           <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
           <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
             <VizTypePicker
             <VizTypePicker

+ 5 - 5
public/app/features/dashboard/panel_model.ts

@@ -1,7 +1,10 @@
-import { Emitter } from 'app/core/utils/emitter';
+// Libraries
 import _ from 'lodash';
 import _ from 'lodash';
+
+// Types
+import { Emitter } from 'app/core/utils/emitter';
 import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
 import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
-import { DataQuery } from 'app/types';
+import { DataQuery } from '@grafana/ui/src/types';
 
 
 export interface GridPos {
 export interface GridPos {
   x: number;
   x: number;
@@ -52,7 +55,6 @@ const mustKeepProps: { [str: string]: boolean } = {
   hasRefreshed: true,
   hasRefreshed: true,
   events: true,
   events: true,
   cacheTimeout: true,
   cacheTimeout: true,
-  nullPointMode: true,
   cachedPluginOptions: true,
   cachedPluginOptions: true,
   transparent: true,
   transparent: true,
 };
 };
@@ -241,8 +243,6 @@ export class PanelModel {
   addQuery(query?: Partial<DataQuery>) {
   addQuery(query?: Partial<DataQuery>) {
     query = query || { refId: 'A' };
     query = query || { refId: 'A' };
     query.refId = this.getNextQueryLetter();
     query.refId = this.getNextQueryLetter();
-    query.isNew = true;
-
     this.targets.push(query);
     this.targets.push(query);
   }
   }
 
 

+ 2 - 2
public/app/features/datasources/DashboardsTable.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { PluginDashboard } from '../../types';
 import { PluginDashboard } from '../../types';
 
 
 export interface Props {
 export interface Props {
@@ -7,7 +7,7 @@ export interface Props {
   onRemove: (dashboard) => void;
   onRemove: (dashboard) => void;
 }
 }
 
 
-const DashboardsTable: SFC<Props> = ({ dashboards, onImport, onRemove }) => {
+const DashboardsTable: FC<Props> = ({ dashboards, onImport, onRemove }) => {
   function buttonText(dashboard: PluginDashboard) {
   function buttonText(dashboard: PluginDashboard) {
     return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
     return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
   }
   }

+ 3 - 2
public/app/features/datasources/DataSourceDashboards.test.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import { DataSourceDashboards, Props } from './DataSourceDashboards';
 import { DataSourceDashboards, Props } from './DataSourceDashboards';
-import { DataSource, NavModel, PluginDashboard } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
+import { NavModel, PluginDashboard } from 'app/types';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
     navModel: {} as NavModel,
     navModel: {} as NavModel,
     dashboards: [] as PluginDashboard[],
     dashboards: [] as PluginDashboard[],
-    dataSource: {} as DataSource,
+    dataSource: {} as DataSourceSettings,
     pageId: 1,
     pageId: 1,
     importDashboard: jest.fn(),
     importDashboard: jest.fn(),
     loadDataSource: jest.fn(),
     loadDataSource: jest.fn(),

+ 10 - 2
public/app/features/datasources/DataSourceDashboards.tsx

@@ -1,9 +1,13 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
+
+// Components
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import DashboardTable from './DashboardsTable';
 import DashboardTable from './DashboardsTable';
-import { DataSource, NavModel, PluginDashboard } from 'app/types';
+
+// Actions & Selectors
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getRouteParamsId } from 'app/core/selectors/location';
 import { getRouteParamsId } from 'app/core/selectors/location';
 import { loadDataSource } from './state/actions';
 import { loadDataSource } from './state/actions';
@@ -11,10 +15,14 @@ import { loadPluginDashboards } from '../plugins/state/actions';
 import { importDashboard, removeDashboard } from '../dashboard/state/actions';
 import { importDashboard, removeDashboard } from '../dashboard/state/actions';
 import { getDataSource } from './state/selectors';
 import { getDataSource } from './state/selectors';
 
 
+// Types
+import { NavModel, PluginDashboard } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
+
 export interface Props {
 export interface Props {
   navModel: NavModel;
   navModel: NavModel;
   dashboards: PluginDashboard[];
   dashboards: PluginDashboard[];
-  dataSource: DataSource;
+  dataSource: DataSourceSettings;
   pageId: number;
   pageId: number;
   importDashboard: typeof importDashboard;
   importDashboard: typeof importDashboard;
   loadDataSource: typeof loadDataSource;
   loadDataSource: typeof loadDataSource;

+ 7 - 2
public/app/features/datasources/DataSourcesList.tsx

@@ -1,11 +1,16 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
+
+// Components
 import DataSourcesListItem from './DataSourcesListItem';
 import DataSourcesListItem from './DataSourcesListItem';
-import { DataSource } from 'app/types';
+
+// Types
+import { DataSourceSettings } from '@grafana/ui/src/types';
 import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
 import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
 
 
 export interface Props {
 export interface Props {
-  dataSources: DataSource[];
+  dataSources: DataSourceSettings[];
   layoutMode: LayoutMode;
   layoutMode: LayoutMode;
 }
 }
 
 

+ 2 - 2
public/app/features/datasources/DataSourcesListItem.tsx

@@ -1,8 +1,8 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { DataSource } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
 
 
 export interface Props {
 export interface Props {
-  dataSource: DataSource;
+  dataSource: DataSourceSettings;
 }
 }
 
 
 export class DataSourcesListItem extends PureComponent<Props> {
 export class DataSourcesListItem extends PureComponent<Props> {

+ 11 - 3
public/app/features/datasources/DataSourcesListPage.test.tsx

@@ -1,16 +1,24 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import { DataSourcesListPage, Props } from './DataSourcesListPage';
 import { DataSourcesListPage, Props } from './DataSourcesListPage';
-import { DataSource, NavModel } from 'app/types';
+import { NavModel } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
 import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
 import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
 import { getMockDataSources } from './__mocks__/dataSourcesMocks';
 import { getMockDataSources } from './__mocks__/dataSourcesMocks';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
-    dataSources: [] as DataSource[],
+    dataSources: [] as DataSourceSettings[],
     layoutMode: LayoutModes.Grid,
     layoutMode: LayoutModes.Grid,
     loadDataSources: jest.fn(),
     loadDataSources: jest.fn(),
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Data Sources'
+      }
+    } as NavModel,
     dataSourcesCount: 0,
     dataSourcesCount: 0,
     searchQuery: '',
     searchQuery: '',
     setDataSourcesSearchQuery: jest.fn(),
     setDataSourcesSearchQuery: jest.fn(),

+ 36 - 28
public/app/features/datasources/DataSourcesListPage.tsx

@@ -1,15 +1,23 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
-import PageHeader from '../../core/components/PageHeader/PageHeader';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
-import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar';
-import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
+
+// Components
+import Page from 'app/core/components/Page/Page';
+import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
+import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import DataSourcesList from './DataSourcesList';
 import DataSourcesList from './DataSourcesList';
-import { DataSource, NavModel } from 'app/types';
-import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
+
+// Types
+import { DataSourceSettings } from '@grafana/ui/src/types';
+import { NavModel, StoreState } from 'app/types';
+import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
+
+// Actions
 import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
 import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
-import { getNavModel } from '../../core/selectors/navModel';
+import { getNavModel } from 'app/core/selectors/navModel';
+
 import {
 import {
   getDataSources,
   getDataSources,
   getDataSourcesCount,
   getDataSourcesCount,
@@ -19,7 +27,7 @@ import {
 
 
 export interface Props {
 export interface Props {
   navModel: NavModel;
   navModel: NavModel;
-  dataSources: DataSource[];
+  dataSources: DataSourceSettings[];
   dataSourcesCount: number;
   dataSourcesCount: number;
   layoutMode: LayoutMode;
   layoutMode: LayoutMode;
   searchQuery: string;
   searchQuery: string;
@@ -67,30 +75,30 @@ export class DataSourcesListPage extends PureComponent<Props> {
     };
     };
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
-          {!hasFetched && <PageLoader pageName="Data sources" />}
-          {hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
-          {hasFetched &&
-            dataSourcesCount > 0 && [
-              <OrgActionBar
-                layoutMode={layoutMode}
-                searchQuery={searchQuery}
-                onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
-                setSearchQuery={query => setDataSourcesSearchQuery(query)}
-                linkButton={linkButton}
-                key="action-bar"
-              />,
-              <DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
-            ]}
-        </div>
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          <>
+            {hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
+            {hasFetched &&
+              dataSourcesCount > 0 && [
+                <OrgActionBar
+                  layoutMode={layoutMode}
+                  searchQuery={searchQuery}
+                  onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
+                  setSearchQuery={query => setDataSourcesSearchQuery(query)}
+                  linkButton={linkButton}
+                  key="action-bar"
+                />,
+                <DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
+              ]}
+          </>
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }
 
 
-function mapStateToProps(state) {
+function mapStateToProps(state: StoreState) {
   return {
   return {
     navModel: getNavModel(state.navIndex, 'datasources'),
     navModel: getNavModel(state.navIndex, 'datasources'),
     dataSources: getDataSources(state.dataSources),
     dataSources: getDataSources(state.dataSources),

+ 3 - 3
public/app/features/datasources/__mocks__/dataSourcesMocks.ts

@@ -1,6 +1,6 @@
-import { DataSource } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
 
 
-export const getMockDataSources = (amount: number): DataSource[] => {
+export const getMockDataSources = (amount: number): DataSourceSettings[] => {
   const dataSources = [];
   const dataSources = [];
 
 
   for (let i = 0; i <= amount; i++) {
   for (let i = 0; i <= amount; i++) {
@@ -25,7 +25,7 @@ export const getMockDataSources = (amount: number): DataSource[] => {
   return dataSources;
   return dataSources;
 };
 };
 
 
-export const getMockDataSource = (): DataSource => {
+export const getMockDataSource = (): DataSourceSettings => {
   return {
   return {
     access: '',
     access: '',
     basicAuth: false,
     basicAuth: false,

+ 31 - 19
public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap

@@ -1,12 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render action bar and datasources 1`] = `
 exports[`Render should render action bar and datasources 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Data Sources",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
     <OrgActionBar
     <OrgActionBar
       key="action-bar"
       key="action-bar"
@@ -143,21 +151,25 @@ exports[`Render should render action bar and datasources 1`] = `
       key="list"
       key="list"
       layoutMode="grid"
       layoutMode="grid"
     />
     />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Data Sources",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   />
   />
-  <div
-    className="page-container page-body"
-  >
-    <PageLoader
-      pageName="Data sources"
-    />
-  </div>
-</div>
+</Page>
 `;
 `;

+ 5 - 5
public/app/features/datasources/settings/BasicSettings.tsx

@@ -1,5 +1,5 @@
-import React, { SFC } from 'react';
-import { Label } from 'app/core/components/Label/Label';
+import React, { FC } from 'react';
+import { FormLabel } from '@grafana/ui';
 import { Switch } from '../../../core/components/Switch/Switch';
 import { Switch } from '../../../core/components/Switch/Switch';
 
 
 export interface Props {
 export interface Props {
@@ -9,19 +9,19 @@ export interface Props {
   onDefaultChange: (value: boolean) => void;
   onDefaultChange: (value: boolean) => void;
 }
 }
 
 
-const BasicSettings: SFC<Props> = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => {
+const BasicSettings: FC<Props> = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => {
   return (
   return (
     <div className="gf-form-group">
     <div className="gf-form-group">
       <div className="gf-form-inline">
       <div className="gf-form-inline">
         <div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
         <div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
-          <Label
+          <FormLabel
             tooltip={
             tooltip={
               'The name is used when you select the data source in panels. The Default data source is ' +
               'The name is used when you select the data source in panels. The Default data source is ' +
               'preselected in new panels.'
               'preselected in new panels.'
             }
             }
           >
           >
             Name
             Name
-          </Label>
+          </FormLabel>
           <input
           <input
             className="gf-form-input max-width-23"
             className="gf-form-input max-width-23"
             type="text"
             type="text"

+ 2 - 2
public/app/features/datasources/settings/ButtonRow.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export interface Props {
 export interface Props {
   isReadOnly: boolean;
   isReadOnly: boolean;
@@ -6,7 +6,7 @@ export interface Props {
   onSubmit: (event) => void;
   onSubmit: (event) => void;
 }
 }
 
 
-const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
+const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
   return (
   return (
     <div className="gf-form-button-row">
     <div className="gf-form-button-row">
       <button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>
       <button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>

+ 5 - 4
public/app/features/datasources/settings/DataSourceSettings.test.tsx → public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
-import { DataSourceSettings, Props } from './DataSourceSettings';
-import { DataSource, NavModel } from '../../../types';
+import { DataSourceSettingsPage, Props } from './DataSourceSettingsPage';
+import { NavModel } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui';
 import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
 import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
 import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
 import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
 
 
@@ -20,7 +21,7 @@ const setup = (propOverrides?: object) => {
 
 
   Object.assign(props, propOverrides);
   Object.assign(props, propOverrides);
 
 
-  return shallow(<DataSourceSettings {...props} />);
+  return shallow(<DataSourceSettingsPage {...props} />);
 };
 };
 
 
 describe('Render', () => {
 describe('Render', () => {
@@ -32,7 +33,7 @@ describe('Render', () => {
 
 
   it('should render loader', () => {
   it('should render loader', () => {
     const wrapper = setup({
     const wrapper = setup({
-      dataSource: {} as DataSource,
+      dataSource: {} as DataSourceSettings,
     });
     });
 
 
     expect(wrapper).toMatchSnapshot();
     expect(wrapper).toMatchSnapshot();

+ 12 - 6
public/app/features/datasources/settings/DataSourceSettings.tsx → public/app/features/datasources/settings/DataSourceSettingsPage.tsx

@@ -1,28 +1,34 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 
 
+// Components
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageLoader from 'app/core/components/PageLoader/PageLoader';
 import PageLoader from 'app/core/components/PageLoader/PageLoader';
 import PluginSettings from './PluginSettings';
 import PluginSettings from './PluginSettings';
 import BasicSettings from './BasicSettings';
 import BasicSettings from './BasicSettings';
 import ButtonRow from './ButtonRow';
 import ButtonRow from './ButtonRow';
 
 
+// Services & Utils
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 
 
+// Actions & selectors
 import { getDataSource, getDataSourceMeta } from '../state/selectors';
 import { getDataSource, getDataSourceMeta } from '../state/selectors';
 import { deleteDataSource, loadDataSource, setDataSourceName, setIsDefault, updateDataSource } from '../state/actions';
 import { deleteDataSource, loadDataSource, setDataSourceName, setIsDefault, updateDataSource } from '../state/actions';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getRouteParamsId } from 'app/core/selectors/location';
 import { getRouteParamsId } from 'app/core/selectors/location';
 
 
-import { DataSource, NavModel, Plugin } from 'app/types/';
+// Types
+import { NavModel, Plugin } from 'app/types/';
+import { DataSourceSettings } from '@grafana/ui/src/types/';
 import { getDataSourceLoadingNav } from '../state/navModel';
 import { getDataSourceLoadingNav } from '../state/navModel';
 
 
 export interface Props {
 export interface Props {
   navModel: NavModel;
   navModel: NavModel;
-  dataSource: DataSource;
+  dataSource: DataSourceSettings;
   dataSourceMeta: Plugin;
   dataSourceMeta: Plugin;
   pageId: number;
   pageId: number;
   deleteDataSource: typeof deleteDataSource;
   deleteDataSource: typeof deleteDataSource;
@@ -33,7 +39,7 @@ export interface Props {
 }
 }
 
 
 interface State {
 interface State {
-  dataSource: DataSource;
+  dataSource: DataSourceSettings;
   isTesting?: boolean;
   isTesting?: boolean;
   testingMessage?: string;
   testingMessage?: string;
   testingStatus?: string;
   testingStatus?: string;
@@ -44,12 +50,12 @@ enum DataSourceStates {
   Beta = 'beta',
   Beta = 'beta',
 }
 }
 
 
-export class DataSourceSettings extends PureComponent<Props, State> {
+export class DataSourceSettingsPage extends PureComponent<Props, State> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
-      dataSource: {} as DataSource,
+      dataSource: {} as DataSourceSettings,
     };
     };
   }
   }
 
 
@@ -246,4 +252,4 @@ const mapDispatchToProps = {
   setIsDefault,
   setIsDefault,
 };
 };
 
 
-export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettings));
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettingsPage));

+ 7 - 6
public/app/features/datasources/settings/PluginSettings.tsx

@@ -1,20 +1,21 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
-import { DataSource, Plugin } from 'app/types/';
+import { Plugin } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
 
 
 export interface Props {
 export interface Props {
-  dataSource: DataSource;
+  dataSource: DataSourceSettings;
   dataSourceMeta: Plugin;
   dataSourceMeta: Plugin;
-  onModelChange: (dataSource: DataSource) => void;
+  onModelChange: (dataSource: DataSourceSettings) => void;
 }
 }
 
 
 export class PluginSettings extends PureComponent<Props> {
 export class PluginSettings extends PureComponent<Props> {
   element: any;
   element: any;
   component: AngularComponent;
   component: AngularComponent;
   scopeProps: {
   scopeProps: {
-    ctrl: { datasourceMeta: Plugin; current: DataSource };
-    onModelChanged: (dataSource: DataSource) => void;
+    ctrl: { datasourceMeta: Plugin; current: DataSourceSettings };
+    onModelChanged: (dataSource: DataSourceSettings) => void;
   };
   };
 
 
   constructor(props) {
   constructor(props) {
@@ -51,7 +52,7 @@ export class PluginSettings extends PureComponent<Props> {
     }
     }
   }
   }
 
 
-  onModelChanged = (dataSource: DataSource) => {
+  onModelChanged = (dataSource: DataSourceSettings) => {
     this.props.onModelChange(dataSource);
     this.props.onModelChange(dataSource);
   };
   };
 
 

+ 0 - 0
public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap → public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap


+ 7 - 6
public/app/features/datasources/state/actions.ts

@@ -6,7 +6,8 @@ import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
 import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
 import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
 import { UpdateLocationAction } from 'app/core/actions/location';
 import { UpdateLocationAction } from 'app/core/actions/location';
 import { buildNavModel } from './navModel';
 import { buildNavModel } from './navModel';
-import { DataSource, Plugin, StoreState } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
+import { Plugin, StoreState } from 'app/types';
 
 
 export enum ActionTypes {
 export enum ActionTypes {
   LoadDataSources = 'LOAD_DATA_SOURCES',
   LoadDataSources = 'LOAD_DATA_SOURCES',
@@ -22,7 +23,7 @@ export enum ActionTypes {
 
 
 interface LoadDataSourcesAction {
 interface LoadDataSourcesAction {
   type: ActionTypes.LoadDataSources;
   type: ActionTypes.LoadDataSources;
-  payload: DataSource[];
+  payload: DataSourceSettings[];
 }
 }
 
 
 interface SetDataSourcesSearchQueryAction {
 interface SetDataSourcesSearchQueryAction {
@@ -47,7 +48,7 @@ interface SetDataSourceTypeSearchQueryAction {
 
 
 interface LoadDataSourceAction {
 interface LoadDataSourceAction {
   type: ActionTypes.LoadDataSource;
   type: ActionTypes.LoadDataSource;
-  payload: DataSource;
+  payload: DataSourceSettings;
 }
 }
 
 
 interface LoadDataSourceMetaAction {
 interface LoadDataSourceMetaAction {
@@ -65,12 +66,12 @@ interface SetIsDefaultAction {
   payload: boolean;
   payload: boolean;
 }
 }
 
 
-const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
+const dataSourcesLoaded = (dataSources: DataSourceSettings[]): LoadDataSourcesAction => ({
   type: ActionTypes.LoadDataSources,
   type: ActionTypes.LoadDataSources,
   payload: dataSources,
   payload: dataSources,
 });
 });
 
 
-const dataSourceLoaded = (dataSource: DataSource): LoadDataSourceAction => ({
+const dataSourceLoaded = (dataSource: DataSourceSettings): LoadDataSourceAction => ({
   type: ActionTypes.LoadDataSource,
   type: ActionTypes.LoadDataSource,
   payload: dataSource,
   payload: dataSource,
 });
 });
@@ -171,7 +172,7 @@ export function loadDataSourceTypes(): ThunkResult<void> {
   };
   };
 }
 }
 
 
-export function updateDataSource(dataSource: DataSource): ThunkResult<void> {
+export function updateDataSource(dataSource: DataSourceSettings): ThunkResult<void> {
   return async dispatch => {
   return async dispatch => {
     await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource);
     await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource);
     await updateFrontendSettings();
     await updateFrontendSettings();

+ 3 - 2
public/app/features/datasources/state/navModel.ts

@@ -1,7 +1,8 @@
-import { DataSource, NavModel, NavModelItem, PluginMeta } from 'app/types';
+import { NavModel, NavModelItem } from 'app/types';
+import { PluginMeta, DataSourceSettings } from '@grafana/ui/src/types';
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
-export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): NavModelItem {
+export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: PluginMeta): NavModelItem {
   const navModel = {
   const navModel = {
     img: pluginMeta.info.logos.large,
     img: pluginMeta.info.logos.large,
     id: 'datasource-' + dataSource.id,
     id: 'datasource-' + dataSource.id,

+ 5 - 4
public/app/features/datasources/state/reducers.ts

@@ -1,10 +1,11 @@
-import { DataSource, DataSourcesState, Plugin } from 'app/types';
+import { DataSourcesState, Plugin } from 'app/types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
 import { Action, ActionTypes } from './actions';
 import { Action, ActionTypes } from './actions';
-import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
+import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
 
 
 const initialState: DataSourcesState = {
 const initialState: DataSourcesState = {
-  dataSources: [] as DataSource[],
-  dataSource: {} as DataSource,
+  dataSources: [] as DataSourceSettings[],
+  dataSource: {} as DataSourceSettings,
   layoutMode: LayoutModes.List,
   layoutMode: LayoutModes.List,
   searchQuery: '',
   searchQuery: '',
   dataSourcesCount: 0,
   dataSourcesCount: 0,

+ 3 - 3
public/app/features/datasources/state/selectors.ts

@@ -1,4 +1,4 @@
-import { DataSource } from '../../../types';
+import { DataSourceSettings } from '@grafana/ui/src/types';
 
 
 export const getDataSources = state => {
 export const getDataSources = state => {
   const regex = new RegExp(state.searchQuery, 'i');
   const regex = new RegExp(state.searchQuery, 'i');
@@ -16,11 +16,11 @@ export const getDataSourceTypes = state => {
   });
   });
 };
 };
 
 
-export const getDataSource = (state, dataSourceId): DataSource | null => {
+export const getDataSource = (state, dataSourceId): DataSourceSettings | null => {
   if (state.dataSource.id === parseInt(dataSourceId, 10)) {
   if (state.dataSource.id === parseInt(dataSourceId, 10)) {
     return state.dataSource;
     return state.dataSource;
   }
   }
-  return {} as DataSource;
+  return {} as DataSourceSettings;
 };
 };
 
 
 export const getDataSourceMeta = (state, type): Plugin => {
 export const getDataSourceMeta = (state, type): Plugin => {

+ 2 - 2
public/app/features/explore/Error.tsx

@@ -1,10 +1,10 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 interface Props {
 interface Props {
   message: any;
   message: any;
 }
 }
 
 
-export const Alert: SFC<Props> = props => {
+export const Alert: FC<Props> = props => {
   const { message } = props;
   const { message } = props;
   return (
   return (
     <div className="gf-form-group section">
     <div className="gf-form-group section">

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است