utkarshcmu 10 лет назад
Родитель
Сommit
0201b9769c
100 измененных файлов с 4446 добавлено и 2615 удалено
  1. 9 5
      CHANGELOG.md
  2. 1 1
      docs/sources/guides/basic_concepts.md
  3. 1534 0
      docs/sources/reference/http_api.md
  4. 4 4
      docs/sources/reference/table_panel.md
  5. 1 1
      package.json
  6. 8 1
      pkg/api/api.go
  7. 163 8
      pkg/api/cloudwatch/metrics.go
  8. 63 0
      pkg/api/cloudwatch/metrics_test.go
  9. 22 13
      pkg/api/dashboard.go
  10. 40 1
      pkg/api/dashboard_snapshot.go
  11. 2 0
      pkg/api/dtos/models.go
  12. 6 0
      pkg/api/index.go
  13. 26 0
      pkg/models/dashboard_snapshot.go
  14. 7 3
      pkg/models/dashboards.go
  15. 16 0
      pkg/services/sqlstore/dashboard_snapshot.go
  16. 5 0
      pkg/services/sqlstore/migrations/dashboard_mig.go
  17. 2 1
      public/app/core/core.ts
  18. 0 46
      public/app/core/directives/config_modal.js
  19. 0 1
      public/app/core/directives/misc.js
  20. 199 0
      public/app/core/directives/plugin_component.ts
  21. 75 0
      public/app/core/directives/rebuild_on_change.ts
  22. 0 0
      public/app/core/plugins/directive.ts
  23. 5 0
      public/app/core/routes/all.js
  24. 1 0
      public/app/features/all.js
  25. 0 1
      public/app/features/annotations/annotations_srv.js
  26. 4 2
      public/app/features/annotations/partials/editor.html
  27. 0 25
      public/app/features/annotations/query_editor.ts
  28. 27 37
      public/app/features/dashboard/dashboardSrv.js
  29. 8 8
      public/app/features/dashboard/dynamicDashboardSrv.js
  30. 29 7
      public/app/features/dashboard/partials/settings.html
  31. 7 0
      public/app/features/dashboard/rowCtrl.js
  32. 5 0
      public/app/features/dashboard/viewStateSrv.js
  33. 0 1
      public/app/features/datasources/all.js
  34. 0 25
      public/app/features/datasources/config_view.ts
  35. 4 1
      public/app/features/datasources/edit_ctrl.js
  36. 4 1
      public/app/features/datasources/partials/edit.html
  37. 2 2
      public/app/features/panel/all.js
  38. 24 38
      public/app/features/panel/metrics_panel_ctrl.ts
  39. 2 40
      public/app/features/panel/panel.ts
  40. 22 13
      public/app/features/panel/panel_ctrl.ts
  41. 0 104
      public/app/features/panel/panel_directive.js
  42. 101 0
      public/app/features/panel/panel_directive.ts
  43. 0 88
      public/app/features/panel/panel_loader.ts
  44. 1 2
      public/app/features/panel/panel_menu.js
  45. 1 1
      public/app/features/panel/partials/panel.html
  46. 56 0
      public/app/features/panel/partials/query_editor_row.html
  47. 57 0
      public/app/features/panel/query_ctrl.ts
  48. 0 48
      public/app/features/panel/query_editor.ts
  49. 18 0
      public/app/features/panel/query_editor_row.ts
  50. 1 0
      public/app/features/snapshot/all.ts
  51. 39 0
      public/app/features/snapshot/partials/snapshots.html
  52. 42 0
      public/app/features/snapshot/snapshot_ctrl.ts
  53. 3 3
      public/app/partials/dashboard.html
  54. 3 3
      public/app/partials/inspector.html
  55. 11 3
      public/app/partials/metrics.html
  56. 2 2
      public/app/plugins/datasource/cloudwatch/datasource.d.ts
  57. 11 7
      public/app/plugins/datasource/cloudwatch/datasource.js
  58. 0 27
      public/app/plugins/datasource/cloudwatch/module.js
  59. 20 0
      public/app/plugins/datasource/cloudwatch/module.ts
  60. 1 1
      public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html
  61. 3 3
      public/app/plugins/datasource/cloudwatch/partials/config.html
  62. 3 37
      public/app/plugins/datasource/cloudwatch/partials/query.editor.html
  63. 0 27
      public/app/plugins/datasource/cloudwatch/query_ctrl.js
  64. 17 0
      public/app/plugins/datasource/cloudwatch/query_ctrl.ts
  65. 3 3
      public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js
  66. 2 2
      public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
  67. 34 0
      public/app/plugins/datasource/elasticsearch/config_ctrl.ts
  68. 2 2
      public/app/plugins/datasource/elasticsearch/datasource.d.ts
  69. 3 1
      public/app/plugins/datasource/elasticsearch/datasource.js
  70. 0 39
      public/app/plugins/datasource/elasticsearch/edit_view.ts
  71. 0 30
      public/app/plugins/datasource/elasticsearch/module.js
  72. 19 0
      public/app/plugins/datasource/elasticsearch/module.ts
  73. 7 7
      public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html
  74. 7 6
      public/app/plugins/datasource/elasticsearch/partials/config.html
  75. 28 73
      public/app/plugins/datasource/elasticsearch/partials/query.editor.html
  76. 3 3
      public/app/plugins/datasource/elasticsearch/partials/query.options.html
  77. 0 46
      public/app/plugins/datasource/elasticsearch/query_ctrl.js
  78. 45 0
      public/app/plugins/datasource/elasticsearch/query_ctrl.ts
  79. 2 2
      public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts
  80. 0 29
      public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts
  81. 4 6
      public/app/plugins/datasource/grafana/module.ts
  82. 5 56
      public/app/plugins/datasource/grafana/partials/query.editor.html
  83. 2 1
      public/app/plugins/datasource/graphite/add_graphite_func.js
  84. 0 3
      public/app/plugins/datasource/graphite/datasource.d.ts
  85. 0 296
      public/app/plugins/datasource/graphite/datasource.js
  86. 281 0
      public/app/plugins/datasource/graphite/datasource.ts
  87. 10 7
      public/app/plugins/datasource/graphite/func_editor.js
  88. 0 682
      public/app/plugins/datasource/graphite/lexer.js
  89. 678 0
      public/app/plugins/datasource/graphite/lexer.ts
  90. 0 33
      public/app/plugins/datasource/graphite/module.js
  91. 23 0
      public/app/plugins/datasource/graphite/module.ts
  92. 0 265
      public/app/plugins/datasource/graphite/parser.js
  93. 258 0
      public/app/plugins/datasource/graphite/parser.ts
  94. 2 2
      public/app/plugins/datasource/graphite/partials/annotations.editor.html
  95. 2 1
      public/app/plugins/datasource/graphite/partials/config.html
  96. 18 70
      public/app/plugins/datasource/graphite/partials/query.editor.html
  97. 13 14
      public/app/plugins/datasource/graphite/partials/query.options.html
  98. 0 292
      public/app/plugins/datasource/graphite/query_ctrl.js
  99. 276 0
      public/app/plugins/datasource/graphite/query_ctrl.ts
  100. 2 2
      public/app/plugins/datasource/graphite/specs/datasource_specs.ts

+ 9 - 5
CHANGELOG.md

@@ -1,23 +1,27 @@
 # 3.0.0 (unrelased master branch)
 
 ### New Features
-* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
+* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655)
 * **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
 * **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
+* **Snapshots UI**: Dashboard snapshots list can be managed through UI, closes[#1984](https://github.com/grafana/grafana/issues/1984)
 
 ### Breaking changes
-* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
+* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
 * **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
 * **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
 
 ### Enhancements
-* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
-* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
-* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
+* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/issues/3458)
+* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/issues/3584)
+* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/issues/3635)
 * **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
 
 ### Bug fixes
 * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
+* **InfluxDB**: Fix for InfluxDB and table panel when using Format As Table and having group by time, fixes [#3928](https://github.com/grafana/grafana/issues/3928)
+* **Panel Time shift**: Fix for panel time range and using dashboard times liek `Today` and `This Week`, fixes [#3941](https://github.com/grafana/grafana/issues/3941)
+* **Row repeat**: Repeated rows will now appear next to each other and not by the bottom of the dashboard, fixes [#3942](https://github.com/grafana/grafana/issues/3942)
 
 # 2.6.1 (unrelased, 2.6.x branch)
 

+ 1 - 1
docs/sources/guides/basic_concepts.md

@@ -77,7 +77,7 @@ The Query Editor exposes capabilities of your Data Source and allows you to quer
 
 Use the Query Editor to build one or more queries (for one or more series) in your time series database. The panel will instantly update allowing you to effectively explore your data in real time and build a perfect query for that particular Panel.
 
-You can utilize [Template variables]((reference/templating/) in the Query Editor within the queries themselves. This provides a powerful way to explore data dynamically based on the Templating variables selected on the Dashboard.
+You can utilize [Template variables](/reference/templating/) in the Query Editor within the queries themselves. This provides a powerful way to explore data dynamically based on the Templating variables selected on the Dashboard.
 
 Grafana allows you to reference queries in the Query Editor by the row that they’re on. If you add a second query to graph, you can reference the first query simply by typing in #A. This provides an easy and convenient way to build compounded queries.
 

+ 1534 - 0
docs/sources/reference/http_api.md

@@ -0,0 +1,1534 @@
+----
+page_title: HTTP API
+page_description: Grafana HTTP API Reference
+page_keywords: grafana, admin, http, api, documentation
+---
+
+# HTTP API Reference
+
+The Grafana backend exposes an HTTP API, the same API is used by the frontend to do everything from saving
+dashboards, creating users and updating data sources.
+
+## Authorization
+
+### Tokens
+
+Currently you can authenticate via an `API Token` or via a `Session cookie` (acquired using regular login or oauth).
+
+### Basic Auth
+
+If basic auth is enabled (it is enabled by default) you can authenticate your HTTP request via
+standard basic auth.
+
+curl example:
+```
+?curl http://admin:admin@localhost:3000/api/org
+{"id":1,"name":"Main Org."}
+```
+
+### Create API Token
+
+Open the sidemenu and click the organization dropdown and select the `API Keys` option.
+
+![](/img/v2/orgdropdown_api_keys.png)
+
+You use the token in all requests in the `Authorization` header, like this:
+
+**Example**:
+
+    GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1
+    Accept: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+The `Authorization` header value should be `Bearer <your api key>`.
+
+## Dashboards
+
+### Create / Update dashboard
+
+`POST /api/dashboards/db`
+
+Creates a new dashboard or updates an existing dashboard.
+
+**Example Request for new dashboard**:
+
+    POST /api/dashboards/db HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "dashboard": {
+        "id": null,
+        "title": "Production Overview",
+        "tags": [ "templated" ],
+        "timezone": "browser",
+        "rows": [
+          {
+          }
+        ],
+        "schemaVersion": 6,
+        "version": 0
+      },
+      "overwrite": false
+    }
+
+JSON Body schema:
+
+- **dashboard** – The complete dashboard model, id = null to create a new dashboard.
+- **overwrite** – Set to true if you want to overwrite existing dashboard with newer version or with same dashboard title.
+
+**Example Response**:
+
+    HTTP/1.1 200 OK
+    Content-Type: application/json; charset=UTF-8
+    Content-Length: 78
+
+    {
+      "slug": "production-overview",
+      "status": "success",
+      "version": 1
+    }
+
+Status Codes:
+
+- **200** – Created
+- **400** – Errors (invalid json, missing or invalid fields, etc)
+- **401** – Unauthorized
+- **412** – Precondition failed
+
+The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The
+same status code is also used if another dashboard exists with the same title. The response body will look like this:
+
+    HTTP/1.1 412 Precondition Failed
+    Content-Type: application/json; charset=UTF-8
+    Content-Length: 97
+
+    {
+      "message": "The dashboard has been changed by someone else",
+      "status": "version-mismatch"
+    }
+
+In in case of title already exists the `status` property will be `name-exists`.
+
+### Get dashboard
+
+`GET /api/dashboards/db/:slug`
+
+Will return the dashboard given the dashboard slug. Slug is the url friendly version of the dashboard title.
+
+**Example Request**:
+
+    GET /api/dashboards/db/production-overview HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "meta": {
+        "isStarred": false,
+        "slug": "production-overview"
+      },
+      "model": {
+        "id": null,
+        "title": "Production Overview",
+        "tags": [ "templated" ],
+        "timezone": "browser",
+        "rows": [
+          {
+          }
+        ],
+        "schemaVersion": 6,
+        "version": 0
+      }
+    }
+
+### Delete dashboard
+
+`DELETE /api/dashboards/db/:slug`
+
+The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title.
+
+**Example Request**:
+
+    DELETE /api/dashboards/db/test HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"title": "Test"}
+
+### Gets the home dashboard
+
+`GET /api/dashboards/home`
+
+Will return the home dashboard.
+
+**Example Request**:
+
+    GET /api/dashboards/home HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "meta":	{
+        "isHome":true,
+        "canSave":false,
+        "canEdit":false,
+        "canStar":false,
+        "slug":"",
+        "expires":"0001-01-01T00:00:00Z",
+        "created":"0001-01-01T00:00:00Z"
+      },
+      "dashboard": {
+        "editable":false,
+        "hideControls":true,
+        "nav":[
+        {
+          "enable":false,
+        "type":"timepicker"
+        }
+        ],
+        "rows": [
+          {
+
+          }
+        ],
+        "style":"dark",
+        "tags":[],
+        "templating":{
+          "list":[
+          ]
+        },
+        "time":{
+        },
+        "timezone":"browser",
+        "title":"Home",
+        "version":5
+      }
+    }
+
+### Tags for Dashboard
+
+
+`GET /api/dashboards/tags`
+
+Get all tabs of dashboards
+
+**Example Request**:
+
+    GET /api/dashboards/home HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "term":"tag1",
+        "count":1
+      },
+      {
+        "term":"tag2",
+        "count":4
+      }
+    ]
+
+### Dashboard from JSON file
+
+`GET /file/:file`
+
+### Search Dashboards
+
+`GET /api/search/`
+
+Status Codes:
+
+- **query** – Search Query
+- **tags** – Tags to use
+- **starred** – Flag indicating if only starred Dashboards should be returned
+- **tagcloud** - Flag indicating if a tagcloud should be returned
+
+**Example Request**:
+
+    GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "id":1,
+        "title":"Production Overview",
+        "uri":"db/production-overview",
+        "type":"dash-db",
+        "tags":[],
+        "isStarred":false
+      }
+    ]
+
+## Data sources
+
+### Get all datasources
+
+`GET /api/datasources`
+
+**Example Request**:
+
+    GET /api/datasources HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "id":1,
+        "orgId":1,
+        "name":"datasource_elastic",
+        "type":"elasticsearch",
+        "access":"proxy",
+        "url":"http://mydatasource.com",
+        "password":"",
+        "user":"",
+        "database":"grafana-dash",
+        "basicAuth":false,
+        "basicAuthUser":"",
+        "basicAuthPassword":"",
+        "isDefault":false,
+        "jsonData":null
+      }
+    ]
+
+### Get a single data sources by Id
+
+`GET /api/datasources/:datasourceId`
+
+**Example Request**:
+
+    GET /api/datasources/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "id":1,
+      "orgId":1,
+      "name":"test_datasource",
+      "type":"graphite",
+      "access":"proxy",
+      "url":"http://mydatasource.com",
+      "password":"",
+      "user":"",
+      "database":"",
+      "basicAuth":false,
+      "basicAuthUser":"",
+      "basicAuthPassword":"",
+      "isDefault":false,
+      "jsonData":null
+    }
+
+### Create data source
+
+`POST /api/datasources`
+
+**Example Request**:
+
+    POST /api/datasources HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "name":"test_datasource",
+      "type":"graphite",
+      "url":"http://mydatasource.com",
+      "access":"proxy",
+      "basicAuth":false
+    }
+
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"id":1,"message":"Datasource added"}
+
+### Update an existing data source
+
+`PUT /api/datasources/:datasourceId`
+
+**Example Request**:
+
+    PUT /api/datasources/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "id":1,
+      "orgId":1,
+      "name":"test_datasource",
+      "type":"graphite",
+      "access":"proxy",
+      "url":"http://mydatasource.com",
+      "password":"",
+      "user":"",
+      "database":"",
+      "basicAuth":true,
+      "basicAuthUser":"basicuser",
+      "basicAuthPassword":"basicuser",
+      "isDefault":false,
+      "jsonData":null
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Datasource updated"}
+
+### Delete an existing data source
+
+`DELETE /api/datasources/:datasourceId`
+
+**Example Request**:
+
+    DELETE /api/datasources/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Data source deleted"}
+
+### Available data source types
+
+`GET /api/datasources/plugins`
+
+**Example Request**:
+
+    GET /api/datasources/plugins HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "grafana":{
+        "metrics":true,"module":"plugins/datasource/grafana/datasource",
+        "name":"Grafana (for testing)",
+        "partials":{
+          "query":"app/plugins/datasource/grafana/partials/query.editor.html"
+        },
+        "pluginType":"datasource",
+        "serviceName":"GrafanaDatasource",
+        "type":"grafana"
+      }
+    }
+
+## Data source proxy calls
+
+`GET /api/datasources/proxy/:datasourceId/*`
+
+Proxies all calls to the actual datasource.
+
+## Organisation
+
+### Get current Organisation
+
+`GET /api/org`
+
+**Example Request**:
+
+    GET /api/org HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "id":1,
+      "name":"Main Org."
+    }
+
+### Get Organisation by Id
+
+`GET /api/orgs/:orgId`
+
+**Example Request**:
+
+    GET /api/orgs/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "id":1,
+      "name":"Main Org.",
+      "address":{
+        "address1":"",
+        "address2":"",
+        "city":"",
+        "zipCode":"",
+        "state":"",
+        "country":""
+      }
+    }
+
+### Get Organisation by Name
+
+`GET /api/orgs/name/:orgName`
+
+**Example Request**:
+
+    GET /api/orgs/name/Main%20Org%2E HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "id":1,
+      "name":"Main Org.",
+      "address":{
+        "address1":"",
+        "address2":"",
+        "city":"",
+        "zipCode":"",
+        "state":"",
+        "country":""
+      }
+    }
+
+### Update current Organisation
+
+`PUT /api/org`
+
+**Example Request**:
+
+    PUT /api/org HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "name":"Main Org."
+    }
+
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Organization updated"}
+
+
+### Get all users within the actual organisation
+
+`GET /api/org/users`
+
+**Example Request**:
+
+    GET /api/org/users HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "orgId":1,
+        "userId":1,
+        "email":"admin@mygraf.com",
+        "login":"admin",
+        "role":"Admin"
+      }
+    ]
+
+### Add a new user to the actual organisation
+
+`POST /api/org/users`
+
+Adds a global user to the actual organisation.
+
+**Example Request**:
+
+    POST /api/org/users HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "role": "Admin",
+      "loginOrEmail": "admin"
+    }
+
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"User added to organization"}
+
+### Updates the given user
+
+`PATCH /api/org/users/:userId`
+
+**Example Request**:
+
+    PATCH /api/org/users/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "role": "Viewer",
+    }
+
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Organization user updated"}
+
+
+### Delete user in actual organisation
+
+`DELETE /api/org/users/:userId`
+
+**Example Request**:
+
+    DELETE /api/org/users/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"User removed from organization"}
+
+
+## Organisations
+
+### Search all Organisations
+
+`GET /api/orgs`
+
+**Example Request**:
+
+    GET /api/orgs HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "id":1,
+        "name":"Main Org."
+      }
+    ]
+
+### Update Organisation
+
+`PUT /api/orgs/:orgId`
+
+Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented yet.
+
+**Example Request**:
+
+    PUT /api/orgs/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "name":"Main Org 2."
+    }
+
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Organization updated"}
+
+### Get Users in Organisation
+
+`GET /api/orgs/:orgId/users`
+
+**Example Request**:
+
+    GET /api/orgs/1/users HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    [
+      {
+        "orgId":1,
+        "userId":1,
+        "email":"admin@mygraf.com",
+        "login":"admin",
+        "role":"Admin"
+      }
+    ]
+
+### Add User in Organisation
+
+`POST /api/orgs/:orgId/users`
+
+**Example Request**:
+
+    POST /api/orgs/1/users HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "loginOrEmail":"user",
+      "role":"Viewer"
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"User added to organization"}
+
+### Update Users in Organisation
+
+`PATCH /api/orgs/:orgId/users/:userId`
+
+**Example Request**:
+
+    PATCH /api/orgs/1/users/2 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "role":"Admin"
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Organization user updated"}
+
+### Delete User in Organisation
+
+`DELETE /api/orgs/:orgId/users/:userId`
+
+**Example Request**:
+
+    DELETE /api/orgs/1/users/2 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"User removed from organization"}
+
+## Users
+
+### Search Users
+
+`GET /api/users`
+
+**Example Request**:
+
+    GET /api/users HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "id": 1,
+        "name": "Admin",
+        "login": "admin",
+        "email": "admin@mygraf.com",
+        "isAdmin": true
+      },
+      {
+        "id": 2,
+        "name": "User",
+        "login": "user",
+        "email": "user@mygraf.com",
+        "isAdmin": false
+      }
+    ]
+
+### Get single user by Id
+
+`GET /api/users/:id`
+
+**Example Request**:
+
+    GET /api/users/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "email": "user@mygraf.com"
+      "name": "admin",
+      "login": "admin",
+      "theme": "light",
+      "orgId": 1,
+      "isGrafanaAdmin": true
+    }
+
+### User Update
+
+`PUT /api/users/:id`
+
+**Example Request**:
+
+    PUT /api/users/2 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "email":"user@mygraf.com",
+      "name":"User2",
+      "login":"user",
+      "theme":"light"
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"User updated"}
+
+
+### Get Organisations for user
+
+`GET /api/users/:id/orgs`
+
+**Example Request**:
+
+    GET /api/users/1/orgs HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "orgId":1,
+        "name":"Main Org.",
+        "role":"Admin"
+      }
+    ]
+
+## User
+
+### Actual User
+
+`GET /api/user`
+
+**Example Request**:
+
+    GET /api/user HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "email":"admin@mygraf.com",
+      "name":"Admin",
+      "login":"admin",
+      "theme":"light",
+      "orgId":1,
+      "isGrafanaAdmin":true
+    }
+
+### Change Password
+
+`PUT /api/user/password`
+
+Changes the password for the user
+
+**Example Request**:
+
+    PUT /api/user/password HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "oldPassword": "old_password",
+      "newPassword": "new_password",
+      "confirmNew": "confirm_new_password"
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"User password changed"}
+
+### Switch user context
+
+`POST /api/user/using/:organisationId`
+
+Switch user context to the given organisation.
+
+**Example Request**:
+
+    POST /api/user/using/2 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Active organization changed"}
+
+### Organisations of the actual User
+
+`GET /api/user/orgs`
+
+Return a list of all organisations of the current user.
+
+**Example Request**:
+
+    GET /api/user/orgs HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    [
+      {
+        "orgId":1,
+        "name":"Main Org.",
+        "role":"Admin"
+      }
+    ]
+
+### Star a dashboard
+
+`POST /api/user/stars/dashboard/:dashboardId`
+
+Stars the given Dashboard for the actual user.
+
+**Example Request**:
+
+    POST /api/user/stars/dashboard/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Dashboard starred!"}
+
+### Unstar a dashboard
+
+`DELETE /api/user/stars/dashboard/:dashboardId`
+
+Deletes the starring of the given Dashboard for the actual user.
+
+**Example Request**:
+
+    DELETE /api/user/stars/dashboard/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Dashboard unstarred"}
+
+
+## Snapshots
+
+### Create new snapshot
+
+`POST /api/snapshots`
+
+**Example Request**:
+
+    POST /api/snapshots HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "dashboard": {
+        "editable":false,
+        "hideControls":true,
+        "nav":[
+        {
+          "enable":false,
+        "type":"timepicker"
+        }
+        ],
+        "rows": [
+          {
+
+          }
+        ],
+        "style":"dark",
+        "tags":[],
+        "templating":{
+          "list":[
+          ]
+        },
+        "time":{
+        },
+        "timezone":"browser",
+        "title":"Home",
+        "version":5
+        },
+      "expires": 3600
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    {
+      "deleteKey":"XXXXXXX",
+      "deleteUrl":"myurl/dashboard/snapshot/XXXXXXX",
+      "key":"YYYYYYY",
+      "url":"myurl/dashboard/snapshot/YYYYYYY"
+    }
+
+Keys:
+
+- **deleteKey** – Key generated to delete the snapshot
+- **key** – Key generated to share the dashboard
+
+### Get Snapshot by Id
+
+`GET /api/snapshots/:key`
+
+**Example Request**:
+
+    GET /api/snapshots/YYYYYYY HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "meta":{
+        "isSnapshot":true,
+        "type":"snapshot",
+        "canSave":false,
+        "canEdit":false,
+        "canStar":false,
+        "slug":"",
+        "expires":"2200-13-32T25:23:23+02:00",
+        "created":"2200-13-32T28:24:23+02:00"
+        },
+        "dashboard": {
+          "editable":false,
+          "hideControls":true,
+          "nav":[
+          {
+            "enable":false,
+          "type":"timepicker"
+          }
+          ],
+          "rows": [
+            {
+
+            }
+          ],
+          "style":"dark",
+          "tags":[],
+          "templating":{
+            "list":[
+            ]
+          },
+          "time":{
+          },
+          "timezone":"browser",
+          "title":"Home",
+          "version":5
+        }
+    }
+
+### Delete Snapshot by Id
+
+`GET /api/snapshots-delete/:key`
+
+**Example Request**:
+
+    GET /api/snapshots/YYYYYYY HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message":"Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}
+
+
+## Frontend Settings
+
+### Get Settings
+
+`GET /api/frontend/settings`
+
+**Example Request**:
+
+    GET /api/frontend/settings HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "allowOrgCreate":true,
+      "appSubUrl":"",
+      "buildInfo":{
+        "buildstamp":xxxxxx,
+        "commit":"vyyyy",
+        "version":"zzzzz"
+      },
+      "datasources":{
+        "datasourcename":{
+        "index":"grafana-dash",
+        "meta":{
+          "annotations":true,
+          "module":"plugins/datasource/grafana/datasource",
+          "name":"Grafana",
+          "partials":{
+            "annotations":"app/plugins/datasource/grafana/partials/annotations.editor.html",
+            "config":"app/plugins/datasource/grafana/partials/config.html"
+            },
+          "pluginType":"datasource",
+          "serviceName":"Grafana",
+          "type":"grafanasearch"
+          }
+        }
+      },
+      "defaultDatasource": "Grafana"
+    }
+
+## Login
+
+### Renew session based on remember cookie
+
+`GET /api/login/ping`
+
+**Example Request**:
+
+    GET /api/login/ping HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"message": "Logged in"}
+
+## Admin
+
+### Settings
+
+`GET /api/admin/settings`
+
+**Example Request**:
+
+    GET /api/admin/settings
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+    "DEFAULT":
+    {
+      "app_mode":"production"},
+      "analytics":
+      {
+        "google_analytics_ua_id":"",
+        "reporting_enabled":"false"
+      },
+      "auth.anonymous":{
+        "enabled":"true",
+        "org_name":"Main Org.",
+        "org_role":"Viewer"
+      },
+      "auth.basic":{
+        "enabled":"false"
+      },
+      "auth.github":{
+        "allow_sign_up":"false",
+        "allowed_domains":"",
+        "allowed_organizations":"",
+        "api_url":"https://api.github.com/user",
+        "auth_url":"https://github.com/login/oauth/authorize",
+        "client_id":"some_id",
+        "client_secret":"************",
+        "enabled":"false",
+        "scopes":"user:email",
+        "team_ids":"",
+        "token_url":"https://github.com/login/oauth/access_token"
+      },
+      "auth.google":{
+        "allow_sign_up":"false","allowed_domains":"",
+        "api_url":"https://www.googleapis.com/oauth2/v1/userinfo",
+        "auth_url":"https://accounts.google.com/o/oauth2/auth",
+        "client_id":"some_client_id",
+        "client_secret":"************",
+        "enabled":"false",
+        "scopes":"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
+        "token_url":"https://accounts.google.com/o/oauth2/token"
+      },
+      "auth.ldap":{
+        "config_file":"/etc/grafana/ldap.toml",
+        "enabled":"false"
+      },
+      "auth.proxy":{
+        "auto_sign_up":"true",
+        "enabled":"false",
+        "header_name":"X-WEBAUTH-USER",
+        "header_property":"username"
+      },
+      "dashboards.json":{
+        "enabled":"false",
+        "path":"/var/lib/grafana/dashboards"
+      },
+      "database":{
+        "host":"127.0.0.1:0000",
+        "name":"grafana",
+        "password":"************",
+        "path":"grafana.db",
+        "ssl_mode":"disable",
+        "type":"sqlite3",
+        "user":"root"
+      },
+      "emails":{
+        "templates_pattern":"emails/*.html",
+        "welcome_email_on_sign_up":"false"
+      },
+      "event_publisher":{
+        "enabled":"false",
+        "exchange":"grafana_events",
+        "rabbitmq_url":"amqp://localhost/"
+      },
+      "log":{
+        "buffer_len":"10000",
+        "level":"Info",
+        "mode":"file"
+      },
+      "log.console":{
+        "level":""
+      },
+      "log.file":{
+        "daily_rotate":"true",
+        "file_name":"",
+        "level":"",
+        "log_rotate":"true",
+        "max_days":"7",
+        "max_lines":"1000000",
+        "max_lines_shift":"28",
+        "max_size_shift":""
+      },
+      "paths":{
+        "data":"/tsdb/grafana",
+        "logs":"/logs/apps/grafana"},
+        "security":{
+        "admin_password":"************",
+        "admin_user":"admin",
+        "cookie_remember_name":"grafana_remember",
+        "cookie_username":"grafana_user",
+        "disable_gravatar":"false",
+        "login_remember_days":"7",
+        "secret_key":"************"
+      },
+      "server":{
+        "cert_file":"",
+        "cert_key":"",
+        "domain":"mygraf.com",
+        "enable_gzip":"false",
+        "enforce_domain":"false",
+        "http_addr":"127.0.0.1",
+        "http_port":"0000",
+        "protocol":"http",
+        "root_url":"%(protocol)s://%(domain)s:%(http_port)s/",
+        "router_logging":"true",
+        "static_root_path":"public"
+      },
+      "session":{
+        "cookie_name":"grafana_sess",
+        "cookie_secure":"false",
+        "gc_interval_time":"",
+        "provider":"file",
+        "provider_config":"sessions",
+        "session_life_time":"86400"
+      },
+      "smtp":{
+        "cert_file":"",
+        "enabled":"false",
+        "from_address":"admin@grafana.localhost",
+        "host":"localhost:25",
+        "key_file":"",
+        "password":"************",
+        "skip_verify":"false",
+        "user":""},
+      "users":{
+        "allow_org_create":"true",
+        "allow_sign_up":"false",
+        "auto_assign_org":"true",
+        "auto_assign_org_role":"Viewer"
+      }
+    }
+
+### Grafana Stats
+
+`GET /api/admin/stats`
+
+**Example Request**:
+
+    GET /api/admin/stats
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "user_count":2,
+      "org_count":1,
+      "dashboard_count":4,
+      "db_snapshot_count":2,
+      "db_tag_count":6,
+      "data_source_count":1,
+      "playlist_count":1,
+      "starred_db_count":2,
+      "grafana_admin_count":2
+    }
+
+### Global Users
+
+`POST /api/admin/users`
+
+Create new user
+
+**Example Request**:
+
+    POST /api/admin/users HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "name":"User",
+      "email":"user@graf.com",
+      "login":"user",
+      "password":"userpassword"
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"id":5,"message":"User created"}
+
+### Password for User
+
+`PUT /api/admin/users/:id/password`
+
+Change password for specific user
+
+**Example Request**:
+
+    PUT /api/admin/users/2/password HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {"password":"userpassword"}
+
+### Permissions
+
+`PUT /api/admin/users/:id/permissions`
+
+**Example Request**:
+
+    PUT /api/admin/users/2/permissions HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {message: "User permissions updated"}
+
+### Delete global User
+
+`DELETE /api/admin/users/:id`
+
+**Example Request**:
+
+    DELETE /api/admin/users/2 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {message: "User deleted"}

+ 4 - 4
docs/sources/reference/table_panel.md

@@ -17,7 +17,7 @@ To view table panels in action and test different configurations with sample dat
 
 The table panel has many ways to manipulate your data for optimal presentation.
 
-<img class="no-shadow" src="/img/v2/table-config.png">
+<img class="no-shadow" src="/img/v2/table-config2.png">
 
 1. `Data`: Control how your query is transformed into a table.
 2. `Table Display`: Table display options.
@@ -33,19 +33,19 @@ you want in the table. Only applicable for some transforms.
 
 ### Time series to rows
 
-<img src="/img/v2/table_ts_to_rows.png">
+<img src="/img/v2/table_ts_to_rows2.png">
 
 In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
 
 ### Time series to columns
 
-![](/img/v2/table_ts_to_columns.png)
+![](/img/v2/table_ts_to_columns2.png)
 
 This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
 
 ### Time series aggregations
 
-![](/img/v2/table_ts_to_aggregations.png)
+![](/img/v2/table_ts_to_aggregations2.png)
 This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
 
 ### Annotations

+ 1 - 1
package.json

@@ -60,7 +60,7 @@
   "scripts": {
     "test": "grunt test",
     "coveralls": "grunt karma:coveralls && rm -rf ./coverage",
-    "postinstall": "grunt copy:node_modules"
+    "postinstall": "./node_modules/.bin/grunt copy:node_modules"
   },
   "license": "Apache-2.0",
   "dependencies": {

+ 8 - 1
pkg/api/api.go

@@ -69,9 +69,11 @@ func Register(r *macaron.Macaron) {
 	r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
 
 	// dashboard snapshots
-	r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
 	r.Get("/dashboard/snapshot/*", Index)
+	r.Get("/dashboard/snapshots/", reqSignedIn, Index)
 
+	// api for dashboard snapshots
+	r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
 	r.Get("/api/snapshot/shared-options/", GetSharingOptions)
 	r.Get("/api/snapshots/:key", GetDashboardSnapshot)
 	r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
@@ -183,6 +185,11 @@ func Register(r *macaron.Macaron) {
 			r.Get("/tags", GetDashboardTags)
 		})
 
+		// Dashboard snapshots
+		r.Group("/dashboard/snapshots", func() {
+			r.Get("/", wrap(SearchDashboardSnapshots))
+		})
+
 		// Playlist
 		r.Group("/playlists", func() {
 			r.Get("/", wrap(SearchPlaylists))

+ 163 - 8
pkg/api/cloudwatch/metrics.go

@@ -3,7 +3,14 @@ package cloudwatch
 import (
 	"encoding/json"
 	"sort"
+	"strings"
+	"sync"
+	"time"
 
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/awsutil"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/util"
 )
@@ -11,6 +18,14 @@ import (
 var metricsMap map[string][]string
 var dimensionsMap map[string][]string
 
+type CustomMetricsCache struct {
+	Expire time.Time
+	Cache  []string
+}
+
+var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache
+var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache
+
 func init() {
 	metricsMap = map[string][]string{
 		"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
@@ -85,6 +100,9 @@ func init() {
 		"AWS/WAF":              {"Rule", "WebACL"},
 		"AWS/WorkSpaces":       {"DirectoryId", "WorkspaceId"},
 	}
+
+	customMetricsMetricsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
+	customMetricsDimensionsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
 }
 
 // Whenever this list is updated, frontend list should also be updated.
@@ -127,10 +145,19 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
 
 	json.Unmarshal(req.Body, reqParam)
 
-	namespaceMetrics, exists := metricsMap[reqParam.Parameters.Namespace]
-	if !exists {
-		c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
-		return
+	var namespaceMetrics []string
+	if !isCustomMetrics(reqParam.Parameters.Namespace) {
+		var exists bool
+		if namespaceMetrics, exists = metricsMap[reqParam.Parameters.Namespace]; !exists {
+			c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
+			return
+		}
+	} else {
+		var err error
+		if namespaceMetrics, err = getMetricsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, getAllMetrics); err != nil {
+			c.JsonApiErr(500, "Unable to call AWS API", err)
+			return
+		}
 	}
 	sort.Sort(sort.StringSlice(namespaceMetrics))
 
@@ -151,10 +178,19 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
 
 	json.Unmarshal(req.Body, reqParam)
 
-	dimensionValues, exists := dimensionsMap[reqParam.Parameters.Namespace]
-	if !exists {
-		c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
-		return
+	var dimensionValues []string
+	if !isCustomMetrics(reqParam.Parameters.Namespace) {
+		var exists bool
+		if dimensionValues, exists = dimensionsMap[reqParam.Parameters.Namespace]; !exists {
+			c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
+			return
+		}
+	} else {
+		var err error
+		if dimensionValues, err = getDimensionsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, getAllMetrics); err != nil {
+			c.JsonApiErr(500, "Unable to call AWS API", err)
+			return
+		}
 	}
 	sort.Sort(sort.StringSlice(dimensionValues))
 
@@ -165,3 +201,122 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
 
 	c.JSON(200, result)
 }
+
+func getAllMetrics(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
+	cfg := &aws.Config{
+		Region:      aws.String(region),
+		Credentials: getCredentials(database),
+	}
+
+	svc := cloudwatch.New(session.New(cfg), cfg)
+
+	params := &cloudwatch.ListMetricsInput{
+		Namespace: aws.String(namespace),
+	}
+
+	var resp cloudwatch.ListMetricsOutput
+	err := svc.ListMetricsPages(params,
+		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
+			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
+			for _, metric := range metrics {
+				resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
+			}
+			return !lastPage
+		})
+	if err != nil {
+		return resp, err
+	}
+
+	return resp, nil
+}
+
+var metricsCacheLock sync.Mutex
+
+func getMetricsForCustomMetrics(region string, namespace string, database string, getAllMetrics func(string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
+	result, err := getAllMetrics(region, namespace, database)
+	if err != nil {
+		return []string{}, err
+	}
+
+	metricsCacheLock.Lock()
+	defer metricsCacheLock.Unlock()
+
+	if _, ok := customMetricsMetricsMap[database]; !ok {
+		customMetricsMetricsMap[database] = make(map[string]map[string]*CustomMetricsCache)
+	}
+	if _, ok := customMetricsMetricsMap[database][region]; !ok {
+		customMetricsMetricsMap[database][region] = make(map[string]*CustomMetricsCache)
+	}
+	if _, ok := customMetricsMetricsMap[database][region][namespace]; !ok {
+		customMetricsMetricsMap[database][region][namespace] = &CustomMetricsCache{}
+		customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
+	}
+
+	if customMetricsMetricsMap[database][region][namespace].Expire.After(time.Now()) {
+		return customMetricsMetricsMap[database][region][namespace].Cache, nil
+	}
+	customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
+	customMetricsMetricsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
+
+	for _, metric := range result.Metrics {
+		if isDuplicate(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName) {
+			continue
+		}
+		customMetricsMetricsMap[database][region][namespace].Cache = append(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName)
+	}
+
+	return customMetricsMetricsMap[database][region][namespace].Cache, nil
+}
+
+var dimensionsCacheLock sync.Mutex
+
+func getDimensionsForCustomMetrics(region string, namespace string, database string, getAllMetrics func(string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
+	result, err := getAllMetrics(region, namespace, database)
+	if err != nil {
+		return []string{}, err
+	}
+
+	dimensionsCacheLock.Lock()
+	defer dimensionsCacheLock.Unlock()
+
+	if _, ok := customMetricsDimensionsMap[database]; !ok {
+		customMetricsDimensionsMap[database] = make(map[string]map[string]*CustomMetricsCache)
+	}
+	if _, ok := customMetricsDimensionsMap[database][region]; !ok {
+		customMetricsDimensionsMap[database][region] = make(map[string]*CustomMetricsCache)
+	}
+	if _, ok := customMetricsDimensionsMap[database][region][namespace]; !ok {
+		customMetricsDimensionsMap[database][region][namespace] = &CustomMetricsCache{}
+		customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
+	}
+
+	if customMetricsDimensionsMap[database][region][namespace].Expire.After(time.Now()) {
+		return customMetricsDimensionsMap[database][region][namespace].Cache, nil
+	}
+	customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
+	customMetricsDimensionsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
+
+	for _, metric := range result.Metrics {
+		for _, dimension := range metric.Dimensions {
+			if isDuplicate(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name) {
+				continue
+			}
+			customMetricsDimensionsMap[database][region][namespace].Cache = append(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name)
+		}
+	}
+
+	return customMetricsDimensionsMap[database][region][namespace].Cache, nil
+}
+
+func isDuplicate(nameList []string, target string) bool {
+	for _, name := range nameList {
+		if name == target {
+			return true
+		}
+	}
+	return false
+}
+
+func isCustomMetrics(namespace string) bool {
+	return strings.Index(namespace, "AWS/") != 0
+}

+ 63 - 0
pkg/api/cloudwatch/metrics_test.go

@@ -0,0 +1,63 @@
+package cloudwatch
+
+import (
+	"testing"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestCloudWatchMetrics(t *testing.T) {
+
+	Convey("When calling getMetricsForCustomMetrics", t, func() {
+		region := "us-east-1"
+		namespace := "Foo"
+		database := "default"
+		f := func(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
+			return cloudwatch.ListMetricsOutput{
+				Metrics: []*cloudwatch.Metric{
+					{
+						MetricName: aws.String("Test_MetricName"),
+						Dimensions: []*cloudwatch.Dimension{
+							{
+								Name: aws.String("Test_DimensionName"),
+							},
+						},
+					},
+				},
+			}, nil
+		}
+		metrics, _ := getMetricsForCustomMetrics(region, namespace, database, f)
+
+		Convey("Should contain Test_MetricName", func() {
+			So(metrics, ShouldContain, "Test_MetricName")
+		})
+	})
+
+	Convey("When calling getDimensionsForCustomMetrics", t, func() {
+		region := "us-east-1"
+		namespace := "Foo"
+		database := "default"
+		f := func(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
+			return cloudwatch.ListMetricsOutput{
+				Metrics: []*cloudwatch.Metric{
+					{
+						MetricName: aws.String("Test_MetricName"),
+						Dimensions: []*cloudwatch.Dimension{
+							{
+								Name: aws.String("Test_DimensionName"),
+							},
+						},
+					},
+				},
+			}, nil
+		}
+		dimensionKeys, _ := getDimensionsForCustomMetrics(region, namespace, database, f)
+
+		Convey("Should contain Test_DimensionName", func() {
+			So(dimensionKeys, ShouldContain, "Test_DimensionName")
+		})
+	})
+
+}

+ 22 - 13
pkg/api/dashboard.go

@@ -49,17 +49,13 @@ func GetDashboard(c *middleware.Context) {
 
 	dash := query.Result
 
-	// Finding the last updater of the dashboard
-	updater := "Anonymous"
-	if dash.UpdatedBy != 0 {
-		userQuery := m.GetUserByIdQuery{Id: dash.UpdatedBy}
-		userErr := bus.Dispatch(&userQuery)
-		if userErr != nil {
-			updater = "Unknown"
-		} else {
-			user := userQuery.Result
-			updater = user.Login
-		}
+	// Finding creator and last updater of the dashboard
+	updater, creator := "Anonymous", "Anonymous"
+	if dash.UpdatedBy > 0 {
+		updater = getUserLogin(dash.UpdatedBy)
+	}
+	if dash.CreatedBy > 0 {
+		creator = getUserLogin(dash.CreatedBy)
 	}
 
 	dto := dtos.DashboardFullWithMeta{
@@ -74,12 +70,25 @@ func GetDashboard(c *middleware.Context) {
 			Created:   dash.Created,
 			Updated:   dash.Updated,
 			UpdatedBy: updater,
+			CreatedBy: creator,
+			Version:   dash.Version,
 		},
 	}
 
 	c.JSON(200, dto)
 }
 
+func getUserLogin(userId int64) string {
+	query := m.GetUserByIdQuery{Id: userId}
+	err := bus.Dispatch(&query)
+	if err != nil {
+		return "Anonymous"
+	} else {
+		user := query.Result
+		return user.Login
+	}
+}
+
 func DeleteDashboard(c *middleware.Context) {
 	slug := c.Params(":slug")
 
@@ -104,9 +113,9 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
 	cmd.OrgId = c.OrgId
 
 	if !c.IsSignedIn {
-		cmd.UpdatedBy = 0
+		cmd.UserId = -1
 	} else {
-		cmd.UpdatedBy = c.UserId
+		cmd.UserId = c.UserId
 	}
 
 	dash := cmd.GetDashboardModel()

+ 40 - 1
pkg/api/dashboard_snapshot.go

@@ -36,7 +36,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
 		cmd.DeleteKey = util.GetRandomString(32)
 		cmd.OrgId = c.OrgId
 		cmd.UserId = c.UserId
-		cmd.Name = c.Name
 		metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
 	}
 
@@ -99,3 +98,43 @@ func DeleteDashboardSnapshot(c *middleware.Context) {
 
 	c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
 }
+
+func SearchDashboardSnapshots(c *middleware.Context) Response {
+	query := c.Query("query")
+	limit := c.QueryInt("limit")
+
+	if limit == 0 {
+		limit = 1000
+	}
+
+	searchQuery := m.GetDashboardSnapshotsQuery{
+		Name:  query,
+		Limit: limit,
+		OrgId: c.OrgId,
+	}
+
+	err := bus.Dispatch(&searchQuery)
+	if err != nil {
+		return ApiError(500, "Search failed", err)
+	}
+
+	dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
+	for i, snapshot := range searchQuery.Result {
+		dtos[i] = &m.DashboardSnapshotDTO{
+			Id:          snapshot.Id,
+			Name:        snapshot.Name,
+			Key:         snapshot.Key,
+			DeleteKey:   snapshot.DeleteKey,
+			OrgId:       snapshot.OrgId,
+			UserId:      snapshot.UserId,
+			External:    snapshot.External,
+			ExternalUrl: snapshot.ExternalUrl,
+			Expires:     snapshot.Expires,
+			Created:     snapshot.Created,
+			Updated:     snapshot.Updated,
+		}
+	}
+
+	return Json(200, dtos)
+	//return Json(200, searchQuery.Result)
+}

+ 2 - 0
pkg/api/dtos/models.go

@@ -42,6 +42,8 @@ type DashboardMeta struct {
 	Created    time.Time `json:"created"`
 	Updated    time.Time `json:"updated"`
 	UpdatedBy  string    `json:"updatedBy"`
+	CreatedBy  string    `json:"createdBy"`
+	Version    int       `json:"version"`
 }
 
 type DashboardFullWithMeta struct {

+ 6 - 0
pkg/api/index.go

@@ -60,6 +60,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		Url:  "/playlists",
 	})
 
+	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+		Text: "Snapshots",
+		Icon: "fa-fw icon-gf icon-gf-snapshot",
+		Url:  "/dashboard/snapshots",
+	})
+
 	if c.OrgRole == m.ROLE_ADMIN {
 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
 			Text: "Data Sources",

+ 26 - 0
pkg/models/dashboard_snapshot.go

@@ -20,6 +20,22 @@ type DashboardSnapshot struct {
 	Dashboard map[string]interface{}
 }
 
+// DashboardSnapshotDTO without dashboard map
+type DashboardSnapshotDTO struct {
+	Id          int64  `json:"id"`
+	Name        string `json:"name"`
+	Key         string `json:"key"`
+	DeleteKey   string `json:"deleteKey"`
+	OrgId       int64  `json:"orgId"`
+	UserId      int64  `json:"userId"`
+	External    bool   `json:"external"`
+	ExternalUrl string `json:"externalUrl"`
+
+	Expires time.Time `json:"expires"`
+	Created time.Time `json:"created"`
+	Updated time.Time `json:"updated"`
+}
+
 // -----------------
 // COMMANDS
 
@@ -48,3 +64,13 @@ type GetDashboardSnapshotQuery struct {
 
 	Result *DashboardSnapshot
 }
+
+type DashboardSnapshots []*DashboardSnapshot
+
+type GetDashboardSnapshotsQuery struct {
+	Name  string
+	Limit int
+	OrgId int64
+
+	Result DashboardSnapshots
+}

+ 7 - 3
pkg/models/dashboards.go

@@ -34,6 +34,7 @@ type Dashboard struct {
 	Updated time.Time
 
 	UpdatedBy int64
+	CreatedBy int64
 
 	Title string
 	Data  map[string]interface{}
@@ -91,8 +92,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard {
 // GetDashboardModel turns the command into the savable model
 func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 	dash := NewDashboardFromJson(cmd.Dashboard)
+	if dash.Data["version"] == 0 {
+		dash.CreatedBy = cmd.UserId
+	}
+	dash.UpdatedBy = cmd.UserId
 	dash.OrgId = cmd.OrgId
-	dash.UpdatedBy = cmd.UpdatedBy
 	dash.UpdateSlug()
 	return dash
 }
@@ -114,9 +118,9 @@ func (dash *Dashboard) UpdateSlug() {
 
 type SaveDashboardCommand struct {
 	Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
-	Overwrite bool                   `json:"overwrite"`
+	UserId    int64                  `json:"userId"`
 	OrgId     int64                  `json:"-"`
-	UpdatedBy int64                  `json:"-"`
+	Overwrite bool                   `json:"overwrite"`
 
 	Result *Dashboard
 }

+ 16 - 0
pkg/services/sqlstore/dashboard_snapshot.go

@@ -12,6 +12,7 @@ func init() {
 	bus.AddHandler("sql", CreateDashboardSnapshot)
 	bus.AddHandler("sql", GetDashboardSnapshot)
 	bus.AddHandler("sql", DeleteDashboardSnapshot)
+	bus.AddHandler("sql", SearchDashboardSnapshots)
 }
 
 func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
@@ -64,3 +65,18 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
 	query.Result = &snapshot
 	return nil
 }
+
+func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error {
+	var snapshots = make(m.DashboardSnapshots, 0)
+
+	sess := x.Limit(query.Limit)
+
+	if query.Name != "" {
+		sess.Where("name LIKE ?", query.Name)
+	}
+
+	sess.Where("org_id = ?", query.OrgId)
+	err := sess.Find(&snapshots)
+	query.Result = snapshots
+	return err
+}

+ 5 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -97,4 +97,9 @@ func addDashboardMigration(mg *Migrator) {
 	mg.AddMigration("Add column updated_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
 		Name: "updated_by", Type: DB_Int, Nullable: true,
 	}))
+
+	// add column to store creator of a dashboard
+	mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
+		Name: "created_by", Type: DB_Int, Nullable: true,
+	}))
 }

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

@@ -3,7 +3,6 @@
 
 import "./directives/annotation_tooltip";
 import "./directives/body_class";
-import "./directives/config_modal";
 import "./directives/confirm_click";
 import "./directives/dash_edit_link";
 import "./directives/dash_upload";
@@ -16,6 +15,8 @@ import "./directives/password_strenght";
 import "./directives/spectrum_picker";
 import "./directives/tags";
 import "./directives/value_select_dropdown";
+import "./directives/plugin_component";
+import "./directives/rebuild_on_change";
 import "./directives/give_focus";
 import './jquery_extended';
 import './partials';

+ 0 - 46
public/app/core/directives/config_modal.js

@@ -1,46 +0,0 @@
-define([
-  'lodash',
-  'jquery',
-  '../core_module',
-],
-function (_, $, coreModule) {
-  'use strict';
-
-  coreModule.default.directive('configModal', function($modal, $q, $timeout) {
-    return {
-      restrict: 'A',
-      link: function(scope, elem, attrs) {
-        var partial = attrs.configModal;
-        var id = '#' + partial.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
-
-        elem.bind('click',function() {
-          if ($(id).length) {
-            elem.attr('data-target', id).attr('data-toggle', 'modal');
-            scope.$apply(function() { scope.$broadcast('modal-opened'); });
-            return;
-          }
-
-          var panelModal = $modal({
-            template: partial,
-            persist: false,
-            show: false,
-            scope: scope.$new(),
-            keyboard: false
-          });
-
-          $q.when(panelModal).then(function(modalEl) {
-            elem.attr('data-target', id).attr('data-toggle', 'modal');
-
-            $timeout(function () {
-              if (!modalEl.data('modal').isShown) {
-                modalEl.modal('show');
-              }
-            }, 50);
-          });
-
-          scope.$apply();
-        });
-      }
-    };
-  });
-});

+ 0 - 1
public/app/core/directives/misc.js

@@ -90,7 +90,6 @@ function (angular, coreModule, kbn) {
         var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
           '<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
           (item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
-          (item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
           '>' + (item.text || '') + '</a>';
 
         if (item.submenu && item.submenu.length) {

+ 199 - 0
public/app/core/directives/plugin_component.ts

@@ -0,0 +1,199 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+import config from 'app/core/config';
+import coreModule from 'app/core/core_module';
+import {UnknownPanelCtrl} from 'app/plugins/panel/unknown/module';
+
+/** @ngInject */
+function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
+
+  function getTemplate(component) {
+    if (component.template) {
+      return $q.when(component.template);
+    }
+    var cached = $templateCache.get(component.templateUrl);
+    if (cached) {
+      return $q.when(cached);
+    }
+    return $http.get(component.templateUrl).then(res => {
+      return res.data;
+    });
+  }
+
+  function getPluginComponentDirective(options) {
+    return function() {
+      return {
+        templateUrl: options.Component.templateUrl,
+        template: options.Component.template,
+        restrict: 'E',
+        controller: options.Component,
+        controllerAs: 'ctrl',
+        bindToController: true,
+        scope: options.bindings,
+        link: (scope, elem, attrs, ctrl) => {
+          if (ctrl.link) {
+            ctrl.link(scope, elem, attrs, ctrl);
+          }
+          if (ctrl.init) {
+            ctrl.init();
+          }
+        }
+      };
+    };
+  }
+
+  function loadPanelComponentInfo(scope, attrs) {
+    var componentInfo: any = {
+      name: 'panel-plugin-' + scope.panel.type,
+      bindings: {dashboard: "=", panel: "=", row: "="},
+      attrs: {dashboard: "dashboard", panel: "panel", row: "row"},
+    };
+
+    var panelElemName = 'panel-' + scope.panel.type;
+    let panelInfo = config.panels[scope.panel.type];
+    var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
+    if (panelInfo) {
+      panelCtrlPromise = System.import(panelInfo.module).then(function(panelModule) {
+        return panelModule.PanelCtrl;
+      });
+    }
+
+    return panelCtrlPromise.then(function(PanelCtrl: any) {
+      componentInfo.Component = PanelCtrl;
+
+      if (!PanelCtrl || PanelCtrl.registered) {
+        return componentInfo;
+      };
+
+      if (PanelCtrl.templatePromise) {
+        return PanelCtrl.templatePromise.then(res => {
+          return componentInfo;
+        });
+      }
+
+      PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
+        PanelCtrl.templateUrl = null;
+        PanelCtrl.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
+        return componentInfo;
+      });
+
+      return PanelCtrl.templatePromise;
+    });
+  }
+
+  function getModule(scope, attrs) {
+    switch (attrs.type) {
+      // QueryCtrl
+      case "query-ctrl": {
+        let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
+        return datasourceSrv.get(datasource).then(ds => {
+          scope.datasource = ds;
+
+          return System.import(ds.meta.module).then(dsModule => {
+            return {
+              name: 'query-ctrl-' + ds.meta.id,
+              bindings: {target: "=", panelCtrl: "=", datasource: "="},
+              attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"},
+              Component: dsModule.QueryCtrl
+            };
+          });
+        });
+      }
+      // QueryOptionsCtrl
+      case "query-options-ctrl": {
+        return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
+          return System.import(ds.meta.module).then((dsModule): any => {
+            if (!dsModule.QueryOptionsCtrl) {
+              return {notFound: true};
+            }
+
+            return {
+              name: 'query-options-ctrl-' + ds.meta.id,
+              bindings: {panelCtrl: "="},
+              attrs: {"panel-ctrl": "ctrl"},
+              Component: dsModule.QueryOptionsCtrl
+            };
+          });
+        });
+      }
+      // Annotations
+      case "annotations-query-ctrl": {
+        return System.import(scope.currentDatasource.meta.module).then(function(dsModule) {
+          return {
+            name: 'annotations-query-ctrl-' + scope.currentDatasource.meta.id,
+            bindings: {annotation: "=", datasource: "="},
+            attrs: {"annotation": "currentAnnotation", datasource: "currentDatasource"},
+            Component: dsModule.AnnotationsQueryCtrl,
+          };
+        });
+      }
+      // ConfigCtrl
+      case 'datasource-config-ctrl': {
+        return System.import(scope.datasourceMeta.module).then(function(dsModule) {
+          return {
+            name: 'ds-config-' + scope.datasourceMeta.id,
+            bindings: {meta: "=", current: "="},
+            attrs: {meta: "datasourceMeta", current: "current"},
+            Component: dsModule.ConfigCtrl,
+          };
+        });
+      }
+      // Panel
+      case 'panel': {
+        return loadPanelComponentInfo(scope, attrs);
+      }
+      default: {
+        return $q.reject({message: "Could not find component type: " + attrs.type });
+      }
+    }
+  }
+
+  function appendAndCompile(scope, elem, componentInfo) {
+    var child = angular.element(document.createElement(componentInfo.name));
+    _.each(componentInfo.attrs, (value, key) => {
+      child.attr(key, value);
+    });
+
+    $compile(child)(scope);
+
+    elem.empty();
+    elem.append(child);
+  }
+
+  function registerPluginComponent(scope, elem, attrs, componentInfo) {
+    if (componentInfo.notFound) {
+      elem.empty();
+      return;
+    }
+
+    if (!componentInfo.Component) {
+      throw {message: 'Failed to find exported plugin component for ' + componentInfo.name};
+    }
+
+    if (!componentInfo.Component.registered) {
+      var directiveName = attrs.$normalize(componentInfo.name);
+      var directiveFn = getPluginComponentDirective(componentInfo);
+      coreModule.directive(directiveName, directiveFn);
+      componentInfo.Component.registered = true;
+    }
+
+    appendAndCompile(scope, elem, componentInfo);
+  }
+
+  return {
+    restrict: 'E',
+    link: function(scope, elem, attrs) {
+      getModule(scope, attrs).then(function (componentInfo) {
+        registerPluginComponent(scope, elem, attrs, componentInfo);
+      }).catch(err => {
+        $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
+        console.log('Plugin componnet error', err);
+      });
+    }
+  };
+}
+
+coreModule.directive('pluginComponent', pluginDirectiveLoader);

+ 75 - 0
public/app/core/directives/rebuild_on_change.ts

@@ -0,0 +1,75 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+import $ from 'jquery';
+
+import coreModule from '../core_module';
+
+function getBlockNodes(nodes) {
+  var node = nodes[0];
+  var endNode = nodes[nodes.length - 1];
+  var blockNodes;
+
+  for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
+    if (blockNodes || nodes[i] !== node) {
+      if (!blockNodes) {
+        blockNodes = $([].slice.call(nodes, 0, i));
+      }
+      blockNodes.push(node);
+    }
+  }
+
+  return blockNodes || nodes;
+}
+
+function rebuildOnChange($animate) {
+
+  return {
+    multiElement: true,
+    terminal: true,
+    transclude: true,
+    priority: 600,
+    restrict: 'E',
+    link: function(scope, elem, attrs, ctrl, transclude) {
+      var block, childScope, previousElements;
+
+      function cleanUp() {
+        if (previousElements) {
+          previousElements.remove();
+          previousElements = null;
+        }
+        if (childScope) {
+          childScope.$destroy();
+          childScope = null;
+        }
+        if (block) {
+          previousElements = getBlockNodes(block.clone);
+          $animate.leave(previousElements).then(function() {
+            previousElements = null;
+          });
+          block = null;
+        }
+      }
+
+      scope.$watch(attrs.property, function rebuildOnChangeAction(value, oldValue) {
+        if (childScope && value !== oldValue) {
+          cleanUp();
+        }
+
+        if (!childScope && (value || attrs.showNull)) {
+          transclude(function(clone, newScope) {
+            childScope = newScope;
+            clone[clone.length++] = document.createComment(' end rebuild on change ');
+            block = {clone: clone};
+            $animate.enter(clone, elem.parent(), elem);
+          });
+        } else {
+          cleanUp();
+        }
+      });
+    }
+  };
+}
+
+coreModule.directive('rebuildOnChange', rebuildOnChange);

+ 0 - 0
public/app/core/plugins/directive.ts


+ 5 - 0
public/app/core/routes/all.js

@@ -137,6 +137,11 @@ define([
         templateUrl: 'public/app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',
       })
+      .when('/dashboard/snapshots', {
+        templateUrl: 'public/app/features/snapshot/partials/snapshots.html',
+        controller : 'SnapshotsCtrl',
+        controllerAs: 'ctrl',
+      })
       .when('/apps', {
         templateUrl: 'public/app/features/apps/partials/list.html',
         controller: 'AppListCtrl',

+ 1 - 0
public/app/features/all.js

@@ -5,6 +5,7 @@ define([
   './templating/templateSrv',
   './dashboard/all',
   './playlist/all',
+  './snapshot/all',
   './panel/all',
   './profile/profileCtrl',
   './profile/changePasswordCtrl',

+ 0 - 1
public/app/features/annotations/annotations_srv.js

@@ -2,7 +2,6 @@ define([
   'angular',
   'lodash',
   './editor_ctrl',
-  './query_editor'
 ], function (angular, _) {
   'use strict';
 

+ 4 - 2
public/app/features/annotations/partials/editor.html

@@ -91,8 +91,10 @@
 				</div>
 			</div>
 
-			<annotations-query-editor datasource="currentDatasource" annotation="currentAnnotation">
-			</annotations-query-editor>
+			<rebuild-on-change property="currentAnnotation.datasource">
+				<plugin-component type="annotations-query-ctrl">
+				</plugin-component>
+			</rebuild-on-change>
 
 			<br>
 			<button ng-show="mode === 'new'" type="button" class="btn btn-success" ng-click="add()">Add</button>

+ 0 - 25
public/app/features/annotations/query_editor.ts

@@ -1,25 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-
-/** @ngInject */
-function annotationsQueryEditor(dynamicDirectiveSrv) {
-  return dynamicDirectiveSrv.create({
-    scope: {
-      annotation: "=",
-      datasource: "="
-    },
-    watchPath: "annotation.datasource",
-    directive: scope => {
-      return System.import(scope.datasource.meta.module).then(function(dsModule) {
-        return {
-          name: 'annotation-query-editor-' + scope.datasource.meta.id,
-          fn: dsModule.annotationsQueryEditor,
-        };
-      });
-    },
-  });
-}
-
-
-angular.module('grafana.directives').directive('annotationsQueryEditor', annotationsQueryEditor);

+ 27 - 37
public/app/features/dashboard/dashboardSrv.js

@@ -177,42 +177,6 @@ function (angular, $, _, moment) {
       return newPanel;
     };
 
-    p.getNextQueryLetter = function(panel) {
-      var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-      return _.find(letters, function(refId) {
-        return _.every(panel.targets, function(other) {
-          return other.refId !== refId;
-        });
-      });
-    };
-
-    p.addDataQueryTo = function(panel, datasource) {
-      var target = {
-        refId: this.getNextQueryLetter(panel)
-      };
-
-      if (datasource) {
-        target.datasource = datasource.name;
-      }
-
-      panel.targets.push(target);
-    };
-
-    p.removeDataQuery = function (panel, query) {
-      panel.targets = _.without(panel.targets, query);
-    };
-
-    p.duplicateDataQuery = function(panel, query) {
-      var clone = angular.copy(query);
-      clone.refId = this.getNextQueryLetter(panel);
-      panel.targets.push(clone);
-    };
-
-    p.moveDataQuery = function(panel, fromIndex, toIndex) {
-      _.move(panel.targets, fromIndex, toIndex);
-    };
-
     p.formatDate = function(date, format) {
       date = moment.isMoment(date) ? date : moment(date);
       format = format || 'YYYY-MM-DD HH:mm:ss';
@@ -230,11 +194,21 @@ function (angular, $, _, moment) {
         moment.utc(date).fromNow();
     };
 
+    p.getNextQueryLetter = function(panel) {
+      var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+      return _.find(letters, function(refId) {
+        return _.every(panel.targets, function(other) {
+          return other.refId !== refId;
+        });
+      });
+    };
+
     p._updateSchema = function(old) {
       var i, j, k;
       var oldVersion = this.schemaVersion;
       var panelUpgrades = [];
-      this.schemaVersion = 9;
+      this.schemaVersion = 10;
 
       if (oldVersion === this.schemaVersion) {
         return;
@@ -407,6 +381,22 @@ function (angular, $, _, moment) {
         });
       }
 
+      // schema version 10 changes
+      if (oldVersion < 10) {
+        // move aliasYAxis changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'table') { return; }
+
+          _.each(panel.styles, function(style) {
+            if (style.thresholds && style.thresholds.length >= 3) {
+              var k = style.thresholds;
+              k.shift();
+              style.thresholds = k;
+            }
+          });
+        });
+      }
+
       if (panelUpgrades.length === 0) {
         return;
       }

+ 8 - 8
public/app/features/dashboard/dynamicDashboardSrv.js

@@ -34,7 +34,7 @@ function (angular, _) {
 
         // handle row repeats
         if (row.repeat) {
-          this.repeatRow(row);
+          this.repeatRow(row, i);
         }
         // clean up old left overs
         else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
@@ -58,13 +58,13 @@ function (angular, _) {
     };
 
     // returns a new row clone or reuses a clone from previous iteration
-    this.getRowClone = function(sourceRow, index) {
-      if (index === 0) {
+    this.getRowClone = function(sourceRow, repeatIndex, sourceRowIndex) {
+      if (repeatIndex === 0) {
         return sourceRow;
       }
 
       var i, panel, row, copy;
-      var sourceRowId = _.indexOf(this.dashboard.rows, sourceRow) + 1;
+      var sourceRowId = sourceRowIndex + 1;
 
       // look for row to reuse
       for (i = 0; i < this.dashboard.rows.length; i++) {
@@ -77,7 +77,7 @@ function (angular, _) {
 
       if (!copy) {
         copy = angular.copy(sourceRow);
-        this.dashboard.rows.push(copy);
+        this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
 
         // set new panel ids
         for (i = 0; i < copy.panels.length; i++) {
@@ -92,8 +92,8 @@ function (angular, _) {
       return copy;
     };
 
-    // returns a new panel clone or reuses a clone from previous iteration
-    this.repeatRow = function(row) {
+    // returns a new row clone or reuses a clone from previous iteration
+    this.repeatRow = function(row, rowIndex) {
       var variables = this.dashboard.templating.list;
       var variable = _.findWhere(variables, {name: row.repeat});
       if (!variable) {
@@ -108,7 +108,7 @@ function (angular, _) {
       }
 
       _.each(selected, function(option, index) {
-        copy = self.getRowClone(row, index);
+        copy = self.getRowClone(row, index, rowIndex);
         copy.scopedVars = {};
         copy.scopedVars[variable.name] = option;
 

+ 29 - 7
public/app/features/dashboard/partials/settings.html

@@ -115,9 +115,9 @@
 	</div>
 
   <div ng-if="editor.index == 4">
-    <div class="editor-row">
-      <div class="tight-form-section">
-        <h5>Dashboard info</h5>
+    <div class="row">
+      <h5>Dashboard info</h5>
+      <div class="pull-left tight-form">
         <div class="tight-form">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 120px">
@@ -129,6 +129,17 @@
           </ul>
           <div class="clearfix"></div>
         </div>
+        <div class="tight-form">
+          <ul class="tight-form-list">
+            <li class="tight-form-item" style="width: 120px">
+              Last updated by:
+            </li>
+            <li class="tight-form-item" style="width: 180px">
+              {{dashboardMeta.updatedBy}}
+            </li>
+          </ul>
+          <div class="clearfix"></div>
+        </div> 
         <div class="tight-form">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 120px">
@@ -136,17 +147,28 @@
             </li>
             <li class="tight-form-item" style="width: 180px">
               {{formatDate(dashboardMeta.created)}}
-           </li>
+            </li>
           </ul>
           <div class="clearfix"></div>
         </div>
-        <div class="tight-form last">
+        <div class="tight-form">
           <ul class="tight-form-list">
             <li class="tight-form-item" style="width: 120px">
-              Last updated by:
+              Created by:
             </li>
             <li class="tight-form-item" style="width: 180px">
-              {{dashboardMeta.updatedBy}}
+              {{dashboardMeta.createdBy}}
+            </li>
+          </ul>
+          <div class="clearfix"></div>
+        </div>
+        <div class="tight-form">
+          <ul class="tight-form-list">
+            <li class="tight-form-item" style="width: 120px">
+              Current version:
+            </li>
+            <li class="tight-form-item" style="width: 180px">
+              {{dashboardMeta.version}}
             </li>
           </ul>
           <div class="clearfix"></div>

+ 7 - 0
public/app/features/dashboard/rowCtrl.js

@@ -61,6 +61,13 @@ function (angular, _, config) {
       });
     };
 
+    $scope.editRow = function() {
+      $scope.appEvent('show-dash-editor', {
+        src: 'public/app/partials/roweditor.html',
+        scope: $scope.$new()
+      });
+    };
+
     $scope.moveRow = function(direction) {
       var rowsList = $scope.dashboard.rows;
       var rowIndex = _.indexOf(rowsList, $scope.row);

+ 5 - 0
public/app/features/dashboard/viewStateSrv.js

@@ -103,6 +103,11 @@ function (angular, _, $) {
         if (!panelScope) {
           return;
         }
+
+        if (!panelScope.ctrl.editModeInitiated) {
+          panelScope.ctrl.initEditMode();
+        }
+
         this.enterFullscreen(panelScope);
         return;
       }

+ 0 - 1
public/app/features/datasources/all.js

@@ -1,5 +1,4 @@
 define([
   './list_ctrl',
   './edit_ctrl',
-  './config_view',
 ], function () {});

+ 0 - 25
public/app/features/datasources/config_view.ts

@@ -1,25 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-
-/** @ngInject */
-function dsConfigView(dynamicDirectiveSrv) {
-  return dynamicDirectiveSrv.create({
-    scope: {
-      dsMeta: "=",
-      current: "="
-    },
-    watchPath: "dsMeta.module",
-    directive: scope => {
-      return System.import(scope.dsMeta.module).then(function(dsModule) {
-        return {
-          name: 'ds-config-' + scope.dsMeta.id,
-          fn: dsModule.configView,
-        };
-      });
-    },
-  });
-}
-
-
-angular.module('grafana.directives').directive('dsConfigView', dsConfigView);

+ 4 - 1
public/app/features/datasources/edit_ctrl.js

@@ -10,7 +10,10 @@ function (angular, _, config) {
   var datasourceTypes = [];
 
   module.directive('datasourceHttpSettings', function() {
-    return {templateUrl: 'public/app/features/datasources/partials/http_settings.html'};
+    return {
+      scope: {current: "="},
+      templateUrl: 'public/app/features/datasources/partials/http_settings.html'
+    };
   });
 
   module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {

+ 4 - 1
public/app/features/datasources/partials/edit.html

@@ -41,7 +41,10 @@
 				<div class="clearfix"></div>
 			</div>
 
-			<ds-config-view ng-if="datasourceMeta.id" ds-meta="datasourceMeta" current="current"></ds-config-view>
+			<rebuild-on-change property="datasourceMeta.id">
+				<plugin-component type="datasource-config-ctrl">
+				</plugin-component>
+			</rebuild-on-change>
 
 			<div ng-if="testing" style="margin-top: 25px">
 				<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>

+ 2 - 2
public/app/features/panel/all.js

@@ -2,7 +2,7 @@ define([
   './panel_menu',
   './panel_directive',
   './solo_panel_ctrl',
-  './panel_loader',
-  './query_editor',
+  './query_ctrl',
   './panel_editor_tab',
+  './query_editor_row',
 ], function () {});

+ 24 - 38
public/app/features/panel/metrics_panel_ctrl.ts

@@ -38,25 +38,15 @@ class MetricsPanelCtrl extends PanelCtrl {
     if (!this.panel.targets) {
       this.panel.targets = [{}];
     }
-
-    // hookup initial data fetch
-    this.$timeout(() => {
-      if (!this.skipDataOnInit) {
-        this.refresh();
-      }
-    }, 30);;
   }
 
   initEditMode() {
+    super.initEditMode();
     this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
     this.datasources = this.datasourceSrv.getMetricSources();
   }
 
-  refresh() {
-    this.getData();
-  }
-
   refreshData(data) {
     // null op
     return this.$q.when(data);
@@ -67,13 +57,14 @@ class MetricsPanelCtrl extends PanelCtrl {
     return data;
   }
 
-  getData() {
+  refresh() {
     // ignore fetching data if another panel is in fullscreen
     if (this.otherPanelInFullscreenMode()) { return; }
 
     // if we have snapshot data use that
     if (this.panel.snapshotData) {
       if (this.loadSnapshot) {
+        this.updateTimeRange();
         this.loadSnapshot(this.panel.snapshotData);
       }
       return;
@@ -140,6 +131,7 @@ class MetricsPanelCtrl extends PanelCtrl {
           this.rangeRaw.from = timeFromInfo.from;
           this.rangeRaw.to = timeFromInfo.to;
           this.range.from = timeFromDate;
+          this.range.to = dateMath.parse(timeFromInfo.to);
         }
       }
 
@@ -164,12 +156,12 @@ class MetricsPanelCtrl extends PanelCtrl {
     };
 
   issueQueries(datasource) {
+    this.updateTimeRange();
+
     if (!this.panel.targets || this.panel.targets.length === 0) {
       return this.$q.when([]);
     }
 
-    this.updateTimeRange();
-
     var metricsQuery = {
       range: this.range,
       rangeRaw: this.rangeRaw,
@@ -182,32 +174,19 @@ class MetricsPanelCtrl extends PanelCtrl {
     };
 
     this.setTimeQueryStart();
-    return datasource.query(metricsQuery).then(results => {
-      this.setTimeQueryEnd();
-
-      if (this.dashboard.snapshot) {
-        this.panel.snapshotData = results;
-      }
-
-      return results;
-    });
-  }
-
-  addDataQuery(datasource) {
-    this.dashboard.addDataQueryTo(this.panel, datasource);
-  }
+    try {
+      return datasource.query(metricsQuery).then(results => {
+        this.setTimeQueryEnd();
 
-  removeDataQuery(query) {
-    this.dashboard.removeDataQuery(this.panel, query);
-    this.refresh();
-  };
-
-  duplicateDataQuery(query) {
-    this.dashboard.duplicateDataQuery(this.panel, query);
-  }
+        if (this.dashboard.snapshot) {
+          this.panel.snapshotData = results;
+        }
 
-  moveDataQuery(fromIndex, toIndex) {
-    this.dashboard.moveDataQuery(this.panel, fromIndex, toIndex);
+        return results;
+      });
+    } catch (err) {
+      return this.$q.reject(err);
+    }
   }
 
   setDatasource(datasource) {
@@ -229,6 +208,13 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.datasource = null;
     this.refresh();
   }
+
+  addDataQuery(datasource) {
+    var target = {
+      datasource: datasource ? datasource.name : undefined
+    };
+    this.panel.targets.push(target);
+  }
 }
 
 export {MetricsPanelCtrl};

+ 2 - 40
public/app/features/panel/panel.ts

@@ -4,48 +4,10 @@ import config from 'app/core/config';
 
 import {PanelCtrl} from './panel_ctrl';
 import {MetricsPanelCtrl} from './metrics_panel_ctrl';
-
-export class DefaultPanelCtrl extends PanelCtrl {
-  /** @ngInject */
-  constructor($scope, $injector) {
-    super($scope, $injector);
-  }
-}
-
-class PanelDirective {
-  template: string;
-  templateUrl: string;
-  bindToController: boolean;
-  scope: any;
-  controller: any;
-  controllerAs: string;
-
-  getDirective() {
-    if (!this.controller) {
-      this.controller = DefaultPanelCtrl;
-    }
-
-    return {
-      template: this.template,
-      templateUrl: this.templateUrl,
-      controller: this.controller,
-      controllerAs: 'ctrl',
-      bindToController: true,
-      scope: {dashboard: "=", panel: "=", row: "="},
-      link: (scope, elem, attrs, ctrl) => {
-        ctrl.init();
-        this.link(scope, elem, attrs, ctrl);
-      }
-    };
-  }
-
-  link(scope, elem, attrs, ctrl) {
-    return null;
-  }
-}
+import {QueryCtrl} from './query_ctrl';
 
 export {
   PanelCtrl,
   MetricsPanelCtrl,
-  PanelDirective,
+  QueryCtrl,
 }

+ 22 - 13
public/app/features/panel/panel_ctrl.ts

@@ -2,6 +2,7 @@
 
 import config from 'app/core/config';
 import _ from 'lodash';
+import angular from 'angular';
 
 export class PanelCtrl {
   panel: any;
@@ -63,12 +64,6 @@ export class PanelCtrl {
   }
 
   editPanel() {
-    if (!this.editModeInitiated) {
-      this.editorTabs = [];
-      this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
-      this.initEditMode();
-    }
-
     this.changeView(true, true);
   }
 
@@ -77,7 +72,9 @@ export class PanelCtrl {
   }
 
   initEditMode() {
-    return;
+    this.editorTabs = [];
+    this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
+    this.editModeInitiated = true;
   }
 
   addEditorTab(title, directiveFn, index?) {
@@ -166,14 +163,26 @@ export class PanelCtrl {
     });
   }
 
- sharePanel() {
-   var shareScope = this.$scope.$new();
-   shareScope.panel = this.panel;
-   shareScope.dashboard = this.dashboard;
+  sharePanel() {
+    var shareScope = this.$scope.$new();
+    shareScope.panel = this.panel;
+    shareScope.dashboard = this.dashboard;
 
-   this.publishAppEvent('show-modal', {
+    this.publishAppEvent('show-modal', {
      src: 'public/app/features/dashboard/partials/shareModal.html',
      scope: shareScope
    });
- }
+  }
+
+  openInspector() {
+    var modalScope = this.$scope.$new();
+    modalScope.panel = this.panel;
+    modalScope.dashboard = this.dashboard;
+    modalScope.inspector = angular.copy(this.inspector);
+
+    this.publishAppEvent('show-modal', {
+      src: 'public/app/partials/inspector.html',
+      scope: modalScope
+    });
+  }
 }

+ 0 - 104
public/app/features/panel/panel_directive.js

@@ -1,104 +0,0 @@
-define([
-  'angular',
-  'jquery',
-],
-function (angular, $) {
-  'use strict';
-
-  var module = angular.module('grafana.directives');
-
-  module.directive('grafanaPanel', function() {
-    return {
-      restrict: 'E',
-      templateUrl: 'public/app/features/panel/partials/panel.html',
-      transclude: true,
-      scope: { ctrl: "=" },
-      link: function(scope, elem) {
-        var panelContainer = elem.find('.panel-container');
-        var ctrl = scope.ctrl;
-        scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
-          panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
-          elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
-        });
-      }
-    };
-  });
-
-  module.directive('panelResizer', function($rootScope) {
-    return {
-      restrict: 'E',
-      template: '<span class="resize-panel-handle"></span>',
-      link: function(scope, elem) {
-        var resizing = false;
-        var lastPanel = false;
-        var ctrl = scope.ctrl;
-        var handleOffset;
-        var originalHeight;
-        var originalWidth;
-        var maxWidth;
-
-        function dragStartHandler(e) {
-          e.preventDefault();
-          resizing = true;
-
-          handleOffset = $(e.target).offset();
-          originalHeight = parseInt(ctrl.row.height);
-          originalWidth = ctrl.panel.span;
-          maxWidth = $(document).width();
-
-          lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
-
-          $('body').on('mousemove', moveHandler);
-          $('body').on('mouseup', dragEndHandler);
-        }
-
-        function moveHandler(e) {
-          ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
-          ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
-          ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
-
-          var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
-
-          // auto adjust other panels
-          if (Math.floor(rowSpan) < 14) {
-            // last panel should not push row down
-            if (lastPanel === ctrl.panel && rowSpan > 12) {
-              lastPanel.span -= rowSpan - 12;
-            }
-            // reduce width of last panel so total in row is 12
-            else if (lastPanel !== ctrl.panel) {
-              lastPanel.span = lastPanel.span - (rowSpan - 12);
-              lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
-            }
-          }
-
-          scope.$apply(function() {
-            scope.$broadcast('render');
-          });
-        }
-
-        function dragEndHandler() {
-          // if close to 12
-          var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
-          if (rowSpan < 12 && rowSpan > 11) {
-            lastPanel.span +=  12 - rowSpan;
-          }
-
-          scope.$apply(function() {
-            $rootScope.$broadcast('render');
-          });
-
-          $('body').off('mousemove', moveHandler);
-          $('body').off('mouseup', dragEndHandler);
-        }
-
-        elem.on('mousedown', dragStartHandler);
-
-        scope.$on("$destroy", function() {
-          elem.off('mousedown', dragStartHandler);
-        });
-      }
-    };
-  });
-
-});

+ 101 - 0
public/app/features/panel/panel_directive.ts

@@ -0,0 +1,101 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import $ from 'jquery';
+
+var module = angular.module('grafana.directives');
+
+module.directive('grafanaPanel', function() {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/features/panel/partials/panel.html',
+    transclude: true,
+    scope: { ctrl: "=" },
+    link: function(scope, elem) {
+      var panelContainer = elem.find('.panel-container');
+      var ctrl = scope.ctrl;
+      scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
+        panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
+        elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
+      });
+    }
+  };
+});
+
+module.directive('panelResizer', function($rootScope) {
+  return {
+    restrict: 'E',
+    template: '<span class="resize-panel-handle"></span>',
+    link: function(scope, elem) {
+      var resizing = false;
+      var lastPanel;
+      var ctrl = scope.ctrl;
+      var handleOffset;
+      var originalHeight;
+      var originalWidth;
+      var maxWidth;
+
+      function dragStartHandler(e) {
+        e.preventDefault();
+        resizing = true;
+
+        handleOffset = $(e.target).offset();
+        originalHeight = parseInt(ctrl.row.height);
+        originalWidth = ctrl.panel.span;
+        maxWidth = $(document).width();
+
+        lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
+
+        $('body').on('mousemove', moveHandler);
+        $('body').on('mouseup', dragEndHandler);
+      }
+
+      function moveHandler(e) {
+        ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
+        ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
+        ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
+
+        var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
+
+        // auto adjust other panels
+        if (Math.floor(rowSpan) < 14) {
+          // last panel should not push row down
+          if (lastPanel === ctrl.panel && rowSpan > 12) {
+            lastPanel.span -= rowSpan - 12;
+          } else if (lastPanel !== ctrl.panel) {
+            // reduce width of last panel so total in row is 12
+            lastPanel.span = lastPanel.span - (rowSpan - 12);
+            lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
+          }
+        }
+
+        scope.$apply(function() {
+          scope.$broadcast('render');
+        });
+      }
+
+      function dragEndHandler() {
+        // if close to 12
+        var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
+        if (rowSpan < 12 && rowSpan > 11) {
+          lastPanel.span +=  12 - rowSpan;
+        }
+
+        scope.$apply(function() {
+          $rootScope.$broadcast('render');
+        });
+
+        $('body').off('mousemove', moveHandler);
+        $('body').off('mouseup', dragEndHandler);
+      }
+
+      elem.on('mousedown', dragStartHandler);
+
+      scope.$on("$destroy", function() {
+        elem.off('mousedown', dragStartHandler);
+      });
+    }
+  };
+});
+
+

+ 0 - 88
public/app/features/panel/panel_loader.ts

@@ -1,88 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-import config from 'app/core/config';
-
-import {UnknownPanel} from '../../plugins/panel/unknown/module';
-
-var directiveModule = angular.module('grafana.directives');
-
-/** @ngInject */
-function panelLoader($compile, dynamicDirectiveSrv, $http, $q, $injector, $templateCache) {
-  return {
-    restrict: 'E',
-    scope: {
-      dashboard: "=",
-      row: "=",
-      panel: "="
-    },
-    link: function(scope, elem, attrs) {
-
-      function getTemplate(directive) {
-        if (directive.template) {
-          return $q.when(directive.template);
-        }
-        var cached = $templateCache.get(directive.templateUrl);
-        if (cached) {
-          return $q.when(cached);
-        }
-        return $http.get(directive.templateUrl).then(res => {
-          return res.data;
-        });
-      }
-
-      function addPanelAndCompile(name) {
-        var child = angular.element(document.createElement(name));
-        child.attr('dashboard', 'dashboard');
-        child.attr('panel', 'panel');
-        child.attr('row', 'row');
-        $compile(child)(scope);
-
-        elem.empty();
-        elem.append(child);
-      }
-
-      function addPanel(name, Panel) {
-        if (Panel.registered) {
-          addPanelAndCompile(name);
-          return;
-        }
-
-        if (Panel.promise) {
-          Panel.promise.then(() => {
-            addPanelAndCompile(name);
-          });
-          return;
-        }
-
-        var panelInstance = $injector.instantiate(Panel);
-        var directive = panelInstance.getDirective();
-
-        Panel.promise = getTemplate(directive).then(template => {
-          directive.templateUrl = null;
-          directive.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
-          directiveModule.directive(attrs.$normalize(name), function() {
-            return directive;
-          });
-          Panel.registered = true;
-          addPanelAndCompile(name);
-        });
-      }
-
-      var panelElemName = 'panel-directive-' + scope.panel.type;
-      let panelInfo = config.panels[scope.panel.type];
-      if (!panelInfo) {
-        addPanel(panelElemName, UnknownPanel);
-        return;
-      }
-
-      System.import(panelInfo.module).then(function(panelModule) {
-        addPanel(panelElemName, panelModule.Panel);
-      }).catch(err => {
-        console.log('Panel err: ', err);
-      });
-    }
-  };
-}
-
-directiveModule.directive('panelLoader', panelLoader);

+ 1 - 2
public/app/features/panel/panel_menu.js

@@ -37,7 +37,7 @@ function (angular, $, _) {
           template += '<div class="panel-menu-row">';
           template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
           template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
-          template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-remove"></i></a>';
+          template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-trash"></i></a>';
           template += '<div class="clearfix"></div>';
           template += '</div>';
         }
@@ -53,7 +53,6 @@ function (angular, $, _) {
 
           template += '<a class="panel-menu-link" ';
           if (item.click) { template += ' ng-click="' + item.click + '"'; }
-          if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
           template += '>';
           template += item.text + '</a>';
         });

+ 1 - 1
public/app/features/panel/partials/panel.html

@@ -1,6 +1,6 @@
 <div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
 	<div class="panel-header">
-		<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="ctrl.error">
+		<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
 			<span data-placement="top" bs-tooltip="ctrl.error">
 				<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
 			</span>

+ 56 - 0
public/app/features/panel/partials/query_editor_row.html

@@ -0,0 +1,56 @@
+<div class="tight-form">
+	<ul class="tight-form-list pull-right">
+		<li ng-show="ctrl.error" class="tight-form-item">
+			<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
+				<i class="fa fa-warning"></i>
+			</a>
+		</li>
+		<li class="tight-form-item small" ng-show="ctrl.target.datasource">
+			<em>{{ctrl.target.datasource}}</em>
+		</li>
+		<li class="tight-form-item" ng-if="ctrl.toggleEditorMode">
+			<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
+				<i class="fa fa-pencil"></i>
+			</a>
+		</li>
+		<li class="tight-form-item">
+			<div class="dropdown">
+				<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
+					<i class="fa fa-bars"></i>
+				</a>
+				<ul class="dropdown-menu pull-right" role="menu">
+					<li role="menuitem">
+						<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
+					</li>
+					<li role="menuitem">
+						<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
+					</li>
+					<li role="menuitem">
+						<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
+					</li>
+				</ul>
+			</div>
+		</li>
+		<li class="tight-form-item last">
+			<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
+				<i class="fa fa-trash"></i>
+			</a>
+		</li>
+	</ul>
+
+	<ul class="tight-form-list">
+		<li class="tight-form-item" style="min-width: 15px; text-align: center">
+			{{ctrl.target.refId}}
+		</li>
+		<li>
+			<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
+				<i class="fa fa-eye"></i>
+			</a>
+		</li>
+	</ul>
+
+	<ul class="tight-form-list" ng-transclude>
+	</ul>
+
+	<div class="clearfix"></div>
+</div>

+ 57 - 0
public/app/features/panel/query_ctrl.ts

@@ -0,0 +1,57 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+export class QueryCtrl {
+  target: any;
+  datasource: any;
+  panelCtrl: any;
+  panel: any;
+  hasRawMode: boolean;
+  error: string;
+
+  constructor(public $scope, private $injector) {
+    this.panel = this.panelCtrl.panel;
+
+    if (!this.target.refId) {
+      this.target.refId = this.getNextQueryLetter();
+    }
+  }
+
+  getNextQueryLetter() {
+    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+    return _.find(letters, refId => {
+      return _.every(this.panel.targets, function(other) {
+        return other.refId !== refId;
+      });
+    });
+  }
+
+  removeQuery() {
+    this.panel.targets = _.without(this.panel.targets, this.target);
+    this.panelCtrl.refresh();
+  };
+
+  duplicateQuery() {
+    var clone = angular.copy(this.target);
+    clone.refId = this.getNextQueryLetter();
+    this.panel.targets.push(clone);
+  }
+
+  moveQuery(direction) {
+    var index = _.indexOf(this.panel.targets, this.target);
+    _.move(this.panel.targets, index, index + direction);
+  }
+
+  refresh() {
+    this.panelCtrl.refresh();
+  }
+
+  toggleHideQuery() {
+    this.target.hide = !this.target.hide;
+    this.panelCtrl.refresh();
+  }
+}
+

+ 0 - 48
public/app/features/panel/query_editor.ts

@@ -1,48 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-
-/** @ngInject */
-function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
-  return dynamicDirectiveSrv.create({
-    watchPath: "ctrl.panel.datasource",
-    directive: scope => {
-      let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
-      return datasourceSrv.get(datasource).then(ds => {
-        scope.datasource = ds;
-
-        if (!scope.target.refId) {
-          scope.target.refId = 'A';
-        }
-
-        return System.import(ds.meta.module).then(dsModule => {
-          return {
-            name: 'metrics-query-editor-' + ds.meta.id,
-            fn: dsModule.metricsQueryEditor,
-          };
-        });
-      });
-    }
-  });
-}
-
-/** @ngInject */
-function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
-  return dynamicDirectiveSrv.create({
-    watchPath: "ctrl.panel.datasource",
-    directive: scope => {
-      return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
-        return System.import(ds.meta.module).then(dsModule => {
-          return {
-            name: 'metrics-query-options-' + ds.meta.id,
-            fn: dsModule.metricsQueryOptions
-          };
-        });
-      });
-    }
-  });
-}
-
-angular.module('grafana.directives')
-  .directive('metricsQueryEditor', metricsQueryEditor)
-  .directive('metricsQueryOptions', metricsQueryOptions);

+ 18 - 0
public/app/features/panel/query_editor_row.ts

@@ -0,0 +1,18 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import $ from 'jquery';
+
+var module = angular.module('grafana.directives');
+
+/** @ngInject **/
+function queryEditorRowDirective() {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/features/panel/partials/query_editor_row.html',
+    transclude: true,
+    scope: {ctrl: "="},
+  };
+}
+
+module.directive('queryEditorRow', queryEditorRowDirective);

+ 1 - 0
public/app/features/snapshot/all.ts

@@ -0,0 +1 @@
+import './snapshot_ctrl';

+ 39 - 0
public/app/features/snapshot/partials/snapshots.html

@@ -0,0 +1,39 @@
+<navbar icon="icon-gf icon-gf-snapshot" title="Dashboard snapshots"></navbar>
+
+<div class="page-container">
+  <div class="page-wide">
+
+    <h2>Available snapshots</h2>
+
+     <table class="filter-table" style="margin-top: 20px">
+      <thead>
+        <th><strong>Name</strong></th>
+        <th><strong>Snapshot url</strong></th>
+        <th style="width: 70px"></th>
+        <th style="width: 25px"></th>
+
+     </thead>
+
+      <tr ng-repeat="snapshot in ctrl.snapshots">
+        <td>
+					<a href="dashboard/snapshot/{{snapshot.key}}">{{snapshot.name}}</a>
+        </td>
+        <td >
+          <a href="dashboard/snapshot/{{snapshot.key}}">dashboard/snapshot/{{snapshot.key}}</a>
+        </td>
+        <td class="text-center">
+          <a href="dashboard/snapshot/{{snapshot.key}}" class="btn btn-inverse btn-mini">
+            <i class="fa fa-eye"></i>
+            View
+          </a>
+        </td>
+        <td  class="text-right">
+          <a ng-click="ctrl.removeSnapshot(snapshot)" class="btn btn-danger btn-mini">
+            <i class="fa fa-remove"></i>
+          </a>
+        </td>
+      </tr>
+    </table>
+
+  </div>
+</div>

+ 42 - 0
public/app/features/snapshot/snapshot_ctrl.ts

@@ -0,0 +1,42 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+export class SnapshotsCtrl {
+
+  snapshots: any;
+
+  /** @ngInject */
+  constructor(private $rootScope, private backendSrv) {
+    this.backendSrv.get('/api/dashboard/snapshots').then(result => {
+      this.snapshots = result;
+    });
+  }
+
+  removeSnapshotConfirmed(snapshot) {
+    _.remove(this.snapshots, {key: snapshot.key});
+    this.backendSrv.get('/api/snapshots-delete/' + snapshot.deleteKey)
+    .then(() => {
+      this.$rootScope.appEvent('alert-success', ['Snapshot deleted', '']);
+    }, () => {
+      this.$rootScope.appEvent('alert-error', ['Unable to delete snapshot', '']);
+      this.snapshots.push(snapshot);
+    });
+  }
+
+  removeSnapshot(snapshot) {
+    this.$rootScope.appEvent('confirm-modal', {
+      title: 'Confirm delete snapshot',
+      text: 'Are you sure you want to delete snapshot ' + snapshot.name + '?',
+      yesText: "Delete",
+      icon: "fa-warning",
+      onConfirm: () => {
+        this.removeSnapshotConfirmed(snapshot);
+      }
+    });
+  }
+
+}
+
+angular.module('grafana.controllers').controller('SnapshotsCtrl', SnapshotsCtrl);

+ 3 - 3
public/app/partials/dashboard.html

@@ -65,7 +65,7 @@
 									</ul>
 								</li>
 								<li>
-									<a dash-editor-link="app/partials/roweditor.html">Row editor</a>
+									<a ng-click="editRow()">Row editor</a>
 								</li>
 								<li>
 									<a ng-click="deleteRow()">Delete row</a>
@@ -81,8 +81,8 @@
 
 					<div ng-repeat="panel in row.panels track by panel.id" class="panel" ui-draggable="!dashboard.meta.fullscreen" drag="panel.id"
 						ui-on-drop="onDrop($data, row, panel)" drag-handle-class="drag-handle" panel-width>
-						<panel-loader class="panel-margin" dashboard="dashboard" row="row" panel="panel">
-						</panel-loader>
+						<plugin-component type="panel" class="panel-margin">
+						</plugin-component>
 					</div>
 
 					<div panel-drop-zone class="panel panel-drop-zone" ui-on-drop="onDrop($data, row)" data-drop="true">

+ 3 - 3
public/app/partials/inspector.html

@@ -61,9 +61,9 @@
 		<div ng-if="editor.index == 2">
 
 			<label>Message:</label>
-			<pre>
-			{{message}}
-		</pre>
+<pre>
+{{message}}
+</pre>
 
 			<label>Stack trace:</label>
 			<pre>

+ 11 - 3
public/app/partials/metrics.html

@@ -1,8 +1,12 @@
 <div class="editor-row">
 
 	<div class="tight-form-container">
-		<metrics-query-editor ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
-		</metrics-query-editor>
+		<div ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-form-disabled': target.hide}">
+			<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
+				<plugin-component type="query-ctrl">
+				</plugin-component>
+			</rebuild-on-change>
+		</div>
 	</div>
 
 	<div style="margin: 20px 0 0 0">
@@ -26,7 +30,11 @@
 
 	</div>
 
-	<metrics-query-options></metrics-query-options>
+	<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
+		<plugin-component type="query-options-ctrl">
+		</plugin-component>
+	</rebuild-on-change>
+
 </div>
 
 <div class="editor-row" style="margin-top: 30px">

+ 2 - 2
public/app/plugins/datasource/cloudwatch/datasource.d.ts

@@ -1,3 +1,3 @@
-declare var Datasource: any;
-export default Datasource;
+declare var CloudWatchDatasource: any;
+export {CloudWatchDatasource};
 

+ 11 - 7
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -90,18 +90,20 @@ function (angular, _, moment, dateMath) {
       return this.awsRequest({action: '__GetNamespaces'});
     };
 
-    this.getMetrics = function(namespace) {
+    this.getMetrics = function(namespace, region) {
       return this.awsRequest({
         action: '__GetMetrics',
+        region: region,
         parameters: {
           namespace: templateSrv.replace(namespace)
         }
       });
     };
 
-    this.getDimensionKeys = function(namespace) {
+    this.getDimensionKeys = function(namespace, region) {
       return this.awsRequest({
         action: '__GetDimensions',
+        region: region,
         parameters: {
           namespace: templateSrv.replace(namespace)
         }
@@ -164,14 +166,14 @@ function (angular, _, moment, dateMath) {
         return this.getNamespaces();
       }
 
-      var metricNameQuery = query.match(/^metrics\(([^\)]+?)\)/);
+      var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
       if (metricNameQuery) {
-        return this.getMetrics(metricNameQuery[1]);
+        return this.getMetrics(metricNameQuery[1], metricNameQuery[3]);
       }
 
-      var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)\)/);
+      var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
       if (dimensionKeysQuery) {
-        return this.getDimensionKeys(dimensionKeysQuery[1]);
+        return this.getDimensionKeys(dimensionKeysQuery[1], dimensionKeysQuery[3]);
       }
 
       var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
@@ -357,5 +359,7 @@ function (angular, _, moment, dateMath) {
 
   }
 
-  return CloudWatchDatasource;
+  return {
+    CloudWatchDatasource: CloudWatchDatasource
+  };
 });

+ 0 - 27
public/app/plugins/datasource/cloudwatch/module.js

@@ -1,27 +0,0 @@
-define([
-  './datasource',
-  './query_parameter_ctrl',
-  './query_ctrl',
-],
-function (CloudWatchDatasource) {
-  'use strict';
-
-  function metricsQueryEditor() {
-    return {controller: 'CloudWatchQueryCtrl', templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.editor.html'};
-  }
-
-  function annotationsQueryEditor() {
-    return {templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
-  }
-
-  function configView() {
-    return {templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/edit_view.html'};
-  }
-
-  return  {
-    Datasource: CloudWatchDatasource,
-    configView: configView,
-    annotationsQueryEditor: annotationsQueryEditor,
-    metricsQueryEditor: metricsQueryEditor,
-  };
-});

+ 20 - 0
public/app/plugins/datasource/cloudwatch/module.ts

@@ -0,0 +1,20 @@
+import './query_parameter_ctrl';
+
+import {CloudWatchDatasource} from './datasource';
+import {CloudWatchQueryCtrl} from './query_ctrl';
+
+class CloudWatchConfigCtrl {
+  static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/config.html';
+}
+
+class CloudWatchAnnotationsQueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html';
+}
+
+export {
+  CloudWatchDatasource as Datasource,
+  CloudWatchQueryCtrl as QueryCtrl,
+  CloudWatchConfigCtrl as ConfigCtrl,
+  CloudWatchAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+};
+

+ 1 - 1
public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html

@@ -1 +1 @@
-<cloudwatch-query-parameter target="annotation" datasource="datasource"></cloudwatch-query-parameter>
+<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>

+ 3 - 3
public/app/plugins/datasource/cloudwatch/partials/edit_view.html → public/app/plugins/datasource/cloudwatch/partials/config.html

@@ -9,7 +9,7 @@
 					Credentials profile name<tip>Credentials profile name, as specified in ~/.aws/credentials, leave blank for default</tip>
 				</li>
 				<li>
-					<input type="text" class="tight-form-input input-large last" ng-model='current.database' placeholder="default"></input>
+					<input type="text" class="tight-form-input input-large last" ng-model='ctrl.current.database' placeholder="default"></input>
 				</li>
 			</ul>
 			<div class="clearfix"></div>
@@ -19,12 +19,12 @@
 				<li class="tight-form-item" style="width: 200px">
 					Default Region<tip>Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.</tip>
 				</li>
-        <!-- 
+        <!--
           Whenever this list is updated, backend list should also be updated.
           Please update the region list in pkg/api/cloudwatch/metric.go
 				-->
         <li>
-          <select class="tight-form-input input-large last" ng-model="current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
+          <select class="tight-form-input input-large last" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
 				</li>
 			</ul>
 			<div class="clearfix"></div>

+ 3 - 37
public/app/plugins/datasource/cloudwatch/partials/query.editor.html

@@ -1,38 +1,4 @@
-<div class="tight-form">
-	<ul class="tight-form-list pull-right">
-		<li class="tight-form-item">
-			<div class="dropdown">
-				<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
-					<i class="fa fa-bars"></i>
-				</a>
-				<ul class="dropdown-menu pull-right" role="menu">
-					<li role="menuitem"><a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a></li>
-					<li role="menuitem"><a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a></li>
-					<li role="menuitem"><a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a></li>
-				</ul>
-			</div>
-		</li>
-		<li class="tight-form-item last">
-			<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
-				<i class="fa fa-remove"></i>
-			</a>
-		</li>
-	</ul>
+<query-editor-row ctrl="ctrl">
+</query-editor-row>
 
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="min-width: 15px; text-align: center">
-			{{target.refId}}
-		</li>
-		<li>
-			<a  class="tight-form-item"
-				ng-click="target.hide = !target.hide; ctrl.refresh();"
-				role="menuitem">
-				<i class="fa fa-eye"></i>
-			</a>
-		</li>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>
-
-<cloudwatch-query-parameter target="target" datasource="ctrl.datasource" on-change="refreshMetricData()"></cloudwatch-query-parameter>
+<cloudwatch-query-parameter target="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.refresh()"></cloudwatch-query-parameter>

+ 0 - 27
public/app/plugins/datasource/cloudwatch/query_ctrl.js

@@ -1,27 +0,0 @@
-define([
-  'angular',
-  'lodash',
-],
-function (angular, _) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('CloudWatchQueryCtrl', function($scope) {
-
-    $scope.init = function() {
-      $scope.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
-    };
-
-    $scope.refreshMetricData = function() {
-      if (!_.isEqual($scope.oldTarget, $scope.target)) {
-        $scope.oldTarget = angular.copy($scope.target);
-        $scope.ctrl.refresh();
-      }
-    };
-
-    $scope.init();
-
-  });
-
-});

+ 17 - 0
public/app/plugins/datasource/cloudwatch/query_ctrl.ts

@@ -0,0 +1,17 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import './query_parameter_ctrl';
+import _ from 'lodash';
+import {QueryCtrl} from 'app/features/panel/panel';
+
+export class CloudWatchQueryCtrl extends QueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/query.editor.html';
+
+  aliasSyntax: string;
+
+  /** @ngInject **/
+  constructor($scope, $injector) {
+    super($scope, $injector);
+    this.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
+  }
+}

+ 3 - 3
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js

@@ -9,7 +9,7 @@ function (angular, _) {
 
   module.directive('cloudwatchQueryParameter', function() {
     return {
-      templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
+      templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.parameter.html',
       controller: 'CloudWatchQueryParameterCtrl',
       restrict: 'E',
       scope: {
@@ -102,7 +102,7 @@ function (angular, _) {
       var query = $q.when([]);
 
       if (segment.type === 'key' || segment.type === 'plus-button') {
-        query = $scope.datasource.getDimensionKeys($scope.target.namespace);
+        query = $scope.datasource.getDimensionKeys($scope.target.namespace, $scope.target.region);
       } else if (segment.type === 'value')  {
         var dimensionKey = $scope.dimSegments[$index-2].value;
         query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
@@ -160,7 +160,7 @@ function (angular, _) {
     };
 
     $scope.getMetrics = function() {
-      return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ')')
+      return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ',' + $scope.target.region + ')')
       .then($scope.transformToSegments(true));
     };
 

+ 2 - 2
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -3,7 +3,7 @@ import "../datasource";
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import moment from 'moment';
 import helpers from 'test/specs/helpers';
-import Datasource from "../datasource";
+import {CloudWatchDatasource} from "../datasource";
 
 describe('CloudWatchDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
@@ -20,7 +20,7 @@ describe('CloudWatchDatasource', function() {
     ctx.$q = $q;
     ctx.$httpBackend =  $httpBackend;
     ctx.$rootScope = $rootScope;
-    ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+    ctx.ds = $injector.instantiate(CloudWatchDatasource, {instanceSettings: instanceSettings});
   }));
 
   describe('When performing CloudWatch query', function() {

+ 34 - 0
public/app/plugins/datasource/elasticsearch/config_ctrl.ts

@@ -0,0 +1,34 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+export class ElasticConfigCtrl {
+  static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/config.html';
+  current: any;
+
+  /** @ngInject */
+  constructor($scope) {
+    this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
+  }
+
+  indexPatternTypes = [
+    {name: 'No pattern',  value: undefined},
+    {name: 'Hourly',      value: 'Hourly',  example: '[logstash-]YYYY.MM.DD.HH'},
+    {name: 'Daily',       value: 'Daily',   example: '[logstash-]YYYY.MM.DD'},
+    {name: 'Weekly',      value: 'Weekly',  example: '[logstash-]GGGG.WW'},
+    {name: 'Monthly',     value: 'Monthly', example: '[logstash-]YYYY.MM'},
+    {name: 'Yearly',      value: 'Yearly',  example: '[logstash-]YYYY'},
+  ];
+
+  esVersions = [
+    {name: '1.x', value: 1},
+    {name: '2.x', value: 2},
+  ];
+
+  indexPatternTypeChanged() {
+    var def = _.findWhere(this.indexPatternTypes, {value: this.current.jsonData.interval});
+    this.current.database = def.example || 'es-index-name';
+  }
+}
+

+ 2 - 2
public/app/plugins/datasource/elasticsearch/datasource.d.ts

@@ -1,3 +1,3 @@
-declare var Datasource: any;
-export default Datasource;
+declare var ElasticDatasource: any;
+export {ElasticDatasource};
 

+ 3 - 1
public/app/plugins/datasource/elasticsearch/datasource.js

@@ -304,5 +304,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
     };
   }
 
-  return ElasticDatasource;
+  return {
+    ElasticDatasource: ElasticDatasource
+  };
 });

+ 0 - 39
public/app/plugins/datasource/elasticsearch/edit_view.ts

@@ -1,39 +0,0 @@
-///<reference path="../../../headers/common.d.ts" />
-
-import angular from 'angular';
-import _ from 'lodash';
-
-export class EditViewCtrl {
-
-  /** @ngInject */
-  constructor($scope) {
-    $scope.indexPatternTypes = [
-      {name: 'No pattern',  value: undefined},
-      {name: 'Hourly',      value: 'Hourly',  example: '[logstash-]YYYY.MM.DD.HH'},
-      {name: 'Daily',       value: 'Daily',   example: '[logstash-]YYYY.MM.DD'},
-      {name: 'Weekly',      value: 'Weekly',  example: '[logstash-]GGGG.WW'},
-      {name: 'Monthly',     value: 'Monthly', example: '[logstash-]YYYY.MM'},
-      {name: 'Yearly',      value: 'Yearly',  example: '[logstash-]YYYY'},
-    ];
-
-    $scope.esVersions = [
-      {name: '1.x', value: 1},
-      {name: '2.x', value: 2},
-    ];
-
-    $scope.indexPatternTypeChanged = function() {
-      var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval});
-      $scope.current.database = def.example || 'es-index-name';
-    };
-  }
-}
-
-function editViewDirective() {
-  return {
-    templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/edit_view.html',
-    controller: EditViewCtrl,
-  };
-};
-
-
-export default editViewDirective;

+ 0 - 30
public/app/plugins/datasource/elasticsearch/module.js

@@ -1,30 +0,0 @@
-define([
-  './datasource',
-  './edit_view',
-  './bucket_agg',
-  './metric_agg',
-],
-function (ElasticDatasource, editView) {
-  'use strict';
-
-  function metricsQueryEditor() {
-    return {controller: 'ElasticQueryCtrl', templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html'};
-  }
-
-  function metricsQueryOptions() {
-    return {templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.options.html'};
-  }
-
-  function annotationsQueryEditor() {
-    return {templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
-  }
-
-  return {
-    Datasource: ElasticDatasource,
-    configView: editView.default,
-    annotationsQueryEditor: annotationsQueryEditor,
-    metricsQueryEditor: metricsQueryEditor,
-    metricsQueryOptions: metricsQueryOptions,
-  };
-
-});

+ 19 - 0
public/app/plugins/datasource/elasticsearch/module.ts

@@ -0,0 +1,19 @@
+import {ElasticDatasource} from './datasource';
+import {ElasticQueryCtrl} from './query_ctrl';
+import {ElasticConfigCtrl} from './config_ctrl';
+
+class ElasticQueryOptionsCtrl {
+  static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/query.options.html';
+}
+
+class ElasticAnnotationsQueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html';
+}
+
+export {
+  ElasticDatasource as Datasource,
+  ElasticQueryCtrl as QueryCtrl,
+  ElasticConfigCtrl as ConfigCtrl,
+  ElasticQueryOptionsCtrl as QueryOptionsCtrl,
+  ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+};

+ 7 - 7
public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html

@@ -1,14 +1,14 @@
 <div class="editor-row">
-	<div class="section" ng-if="annotation.index">
+	<div class="section" ng-if="ctrl.annotation.index">
 		<h5>Index name</h5>
 		<div class="editor-option">
-			<input type="text" class="span4" ng-model='annotation.index' placeholder="events-*"></input>
+			<input type="text" class="span4" ng-model='ctrl.annotation.index' placeholder="events-*"></input>
 		</div>
 	</div>
 	<div class="section">
 		<h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></h5>
 		<div class="editor-option">
-			<input type="text" class="span6" ng-model='annotation.query' placeholder="tags:deploy"></input>
+			<input type="text" class="span6" ng-model='ctrl.annotation.query' placeholder="tags:deploy"></input>
 		</div>
 	</div>
 </div>
@@ -18,22 +18,22 @@
 		<h5>Field mappings</h5>
 		<div class="editor-option">
 			<label class="small">Time</label>
-			<input type="text" class="input-small" ng-model='annotation.timeField' placeholder="@timestamp"></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
 		</div>
 
 		<div class="editor-option">
 			<label class="small">Title</label>
-			<input type="text" class="input-small" ng-model='annotation.titleField' placeholder="desc"></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
 		</div>
 
 		<div class="editor-option">
 			<label class="small">Tags</label>
-			<input type="text" class="input-small" ng-model='annotation.tagsField' placeholder="tags"></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
 		</div>
 
 		<div class="editor-option">
 			<label class="small">Text</label>
-			<input type="text" class="input-small" ng-model='annotation.textField' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.textField' placeholder=""></input>
 		</div>
 	</div>
 </div>

+ 7 - 6
public/app/plugins/datasource/elasticsearch/partials/edit_view.html → public/app/plugins/datasource/elasticsearch/partials/config.html

@@ -1,4 +1,5 @@
-<datasource-http-settings></datasource-http-settings>
+<datasource-http-settings current="ctrl.current">
+</datasource-http-settings>
 
 <h4>Elasticsearch details</h4>
 
@@ -8,13 +9,13 @@
 			Index name
 		</li>
 		<li>
-			<input type="text" class="tight-form-input input-xlarge" ng-model='current.database' placeholder="" required></input>
+			<input type="text" class="tight-form-input input-xlarge" ng-model='ctrl.current.database' placeholder="" required></input>
 		</li>
 		<li class="tight-form-item">
 			Pattern
 		</li>
 		<li>
-			<select class="input-medium tight-form-input" ng-model="current.jsonData.interval" ng-options="f.value as f.name for f in indexPatternTypes" ng-change="indexPatternTypeChanged()" ></select>
+			<select class="input-medium tight-form-input" ng-model="ctrl.current.jsonData.interval" ng-options="f.value as f.name for f in ctrl.indexPatternTypes" ng-change="ctrl.indexPatternTypeChanged()" ></select>
 		</li>
 	</ul>
 	<div class="clearfix"></div>
@@ -25,7 +26,7 @@
 			Time field name
 		</li>
 		<li>
-			<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.timeField' placeholder="" required ng-init="current.jsonData.timeField = current.jsonData.timeField || '@timestamp'"></input>
+			<input type="text" class="tight-form-input input-xlarge" ng-model='ctrl.current.jsonData.timeField' placeholder="" required ng-init=""></input>
 		</li>
 	</ul>
 	<div class="clearfix"></div>
@@ -36,7 +37,7 @@
 			Version
 		</li>
 		<li>
-			<select class="input-medium tight-form-input" ng-model="current.jsonData.esVersion" ng-options="f.value as f.name for f in esVersions"></select>
+			<select class="input-medium tight-form-input" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions"></select>
 		</li>
 	</ul>
 	<div class="clearfix"></div>
@@ -52,7 +53,7 @@
 			Group by time interval
 		</li>
 		<li>
-			<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="current.jsonData.timeInterval"
+			<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="ctrl.current.jsonData.timeInterval"
 			spellcheck='false' placeholder="example: >10s">
 		</li>
 		<li class="tight-form-item">

+ 28 - 73
public/app/plugins/datasource/elasticsearch/partials/query.editor.html

@@ -1,77 +1,32 @@
-<div  class="tight-form">
-	<ul class="tight-form-list pull-right">
-		<li ng-show="parserError" class="tight-form-item">
-			<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
-				<i class="fa fa-warning"></i>
-			</a>
-		</li>
-		<li class="tight-form-item small" ng-show="target.datasource">
-			<em>{{target.datasource}}</em>
-		</li>
-		<li class="tight-form-item">
-			<div class="dropdown">
-				<a  class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
-					<i class="fa fa-bars"></i>
-				</a>
-				<ul class="dropdown-menu pull-right" role="menu">
-					<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.duplicateDataQuery(target)">Duplicate</a></li>
-					<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index-1)">Move up</a></li>
-					<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index+1)">Move down</a></li>
-				</ul>
-			</div>
-		</li>
+<query-editor-row ctrl="ctrl">
+	<li class="tight-form-item query-keyword" style="width: 75px">
+		Query
+	</li>
+	<li>
+		<input type="text" class="tight-form-input" style="width: 345px;" ng-model="ctrl.target.query" spellcheck='false' placeholder="Lucene query" ng-blur="ctrl.refresh()">
+	</li>
+	<li class="tight-form-item query-keyword">
+		Alias
+	</li>
+	<li>
+		<input type="text" class="tight-form-input" style="width: 200px;" ng-model="ctrl.target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="ctrl.refresh()">
+	</li>
+</query-editor-row>
 
-		<li class="tight-form-item last">
-			<a class="pointer" tabindex="1" ng-click="panelCtrl.removeDataQuery(target)">
-				<i class="fa fa-remove"></i>
-			</a>
-		</li>
-	</ul>
-
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="min-width: 15px; text-align: center">
-			{{target.refId}}
-		</li>
-		<li>
-			<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
-				<i class="fa fa-eye"></i>
-			</a>
-		</li>
-	</ul>
-
-	<ul class="tight-form-list">
-		<li class="tight-form-item query-keyword" style="width: 75px">
-			Query
-		</li>
-		<li>
-			<input type="text" class="tight-form-input" style="width: 345px;" ng-model="target.query" spellcheck='false' placeholder="Lucene query" ng-blur="panelCtrl.refresh()">
-		</li>
-		<li class="tight-form-item query-keyword">
-			Alias
-		</li>
-		<li>
-			<input type="text" class="tight-form-input" style="width: 200px;" ng-model="target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="panelCtrl.refresh()">
-		</li>
-	</ul>
-	<div class="clearfix"></div>
+<div ng-repeat="agg in ctrl.target.metrics">
+	<elastic-metric-agg
+		target="ctrl.target" index="$index"
+		get-fields="ctrl.getFields($fieldType)"
+		on-change="ctrl.queryUpdated()"
+		es-version="ctrl.esVersion">
+	</elastic-metric-agg>
 </div>
 
-<div ng-hide="target.rawQuery">
-	<div ng-repeat="agg in target.metrics">
-		<elastic-metric-agg
-			target="target" index="$index"
-			get-fields="getFields($fieldType)"
-			on-change="queryUpdated()"
-			es-version="esVersion">
-		</elastic-metric-agg>
-	</div>
-
-	<div ng-repeat="agg in target.bucketAggs">
-		<elastic-bucket-agg
-			target="target" index="$index"
-			get-fields="getFields($fieldType)"
-			on-change="queryUpdated()">
-		</elastic-bucket-agg>
-	</div>
-
+<div ng-repeat="agg in ctrl.target.bucketAggs">
+	<elastic-bucket-agg
+		target="ctrl.target" index="$index"
+		get-fields="ctrl.getFields($fieldType)"
+		on-change="ctrl.queryUpdated()">
+	</elastic-bucket-agg>
 </div>
+

+ 3 - 3
public/app/plugins/datasource/elasticsearch/partials/query.options.html

@@ -8,7 +8,7 @@
 				Group by time interval
 			</li>
 			<li>
-				<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panel.interval" ng-blur="ctrl.refresh();"
+				<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
 							 spellcheck='false' placeholder="example: >10s">
 			</li>
 			<li class="tight-form-item">
@@ -23,7 +23,7 @@
 				<i class="fa fa-info-circle"></i>
 			</li>
 			<li class="tight-form-item">
-				<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
 					alias patterns
 				</a>
 			</li>
@@ -34,7 +34,7 @@
 
 <div class="editor-row">
 	<div class="pull-left" style="margin-top: 30px;">
-		<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 1">
+		<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
 			<h5>Alias patterns</h5>
 			<ul ng-non-bindable>
 				<li>{{term fieldname}} = replaced with value of term group by</li>

+ 0 - 46
public/app/plugins/datasource/elasticsearch/query_ctrl.js

@@ -1,46 +0,0 @@
-define([
-  'angular',
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('ElasticQueryCtrl', function($scope, $rootScope, $timeout, uiSegmentSrv) {
-    $scope.esVersion = $scope.datasource.esVersion;
-    $scope.panelCtrl = $scope.ctrl;
-
-    $scope.init = function() {
-      var target = $scope.target;
-      if (!target) { return; }
-
-      $scope.queryUpdated();
-    };
-
-    $scope.getFields = function(type) {
-      var jsonStr = angular.toJson({find: 'fields', type: type});
-      return $scope.datasource.metricFindQuery(jsonStr)
-      .then(uiSegmentSrv.transformToSegments(false))
-      .then(null, $scope.handleQueryError);
-    };
-
-    $scope.queryUpdated = function() {
-      var newJson = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true);
-      if (newJson !== $scope.oldQueryRaw) {
-        $scope.rawQueryOld = newJson;
-        $scope.panelCtrl.refresh();
-      }
-
-      $rootScope.appEvent('elastic-query-updated');
-    };
-
-    $scope.handleQueryError = function(err) {
-      $scope.parserError = err.message || 'Failed to issue metric query';
-      return [];
-    };
-
-    $scope.init();
-
-  });
-
-});

+ 45 - 0
public/app/plugins/datasource/elasticsearch/query_ctrl.ts

@@ -0,0 +1,45 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import './bucket_agg';
+import './metric_agg';
+
+import angular from 'angular';
+import _ from 'lodash';
+import {QueryCtrl} from 'app/features/panel/panel';
+
+export class ElasticQueryCtrl extends QueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html';
+
+  esVersion: any;
+  rawQueryOld: string;
+
+  /** @ngInject **/
+  constructor($scope, $injector, private $rootScope, private $timeout, private uiSegmentSrv) {
+    super($scope, $injector);
+
+    this.esVersion = this.datasource.esVersion;
+    this.queryUpdated();
+  }
+
+  getFields(type) {
+    var jsonStr = angular.toJson({find: 'fields', type: type});
+    return this.datasource.metricFindQuery(jsonStr)
+    .then(this.uiSegmentSrv.transformToSegments(false))
+    .catch(this.handleQueryError.bind(this));
+  }
+
+  queryUpdated() {
+    var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true);
+    if (newJson !== this.rawQueryOld) {
+      this.rawQueryOld = newJson;
+      this.refresh();
+    }
+
+    this.$rootScope.appEvent('elastic-query-updated');
+  }
+
+  handleQueryError(err) {
+    this.error = err.message || 'Failed to issue metric query';
+    return [];
+  }
+}

+ 2 - 2
public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts

@@ -3,7 +3,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co
 import moment from 'moment';
 import angular from 'angular';
 import helpers from 'test/specs/helpers';
-import Datasource from "../datasource";
+import {ElasticDatasource} from "../datasource";
 
 describe('ElasticDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
@@ -21,7 +21,7 @@ describe('ElasticDatasource', function() {
 
   function createDatasource(instanceSettings) {
     instanceSettings.jsonData = instanceSettings.jsonData || {};
-    ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+    ctx.ds = ctx.$injector.instantiate(ElasticDatasource, {instanceSettings: instanceSettings});
   }
 
   describe('When testing datasource with index pattern', function() {

+ 0 - 29
public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts

@@ -1,29 +0,0 @@
-///<amd-dependency path="../query_ctrl" />
-///<amd-dependency path="app/core/services/segment_srv" />
-///<amd-dependency path="test/specs/helpers" name="helpers" />
-
-import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
-import helpers from 'test/specs/helpers';
-
-describe('ElasticQueryCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.providePhase());
-  beforeEach(ctx.createControllerPhase('ElasticQueryCtrl'));
-
-  beforeEach(function() {
-    ctx.scope.target = {};
-    ctx.scope.$parent = { get_data: sinon.spy() };
-
-    ctx.scope.datasource = ctx.datasource;
-    ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-  });
-
-  describe('init', function() {
-    beforeEach(function() {
-      ctx.scope.init();
-    });
-  });
-});

+ 4 - 6
public/app/plugins/datasource/grafana/module.ts

@@ -2,17 +2,15 @@
 
 import angular from 'angular';
 import {GrafanaDatasource} from './datasource';
+import {QueryCtrl} from 'app/features/panel/panel';
 
-var module = angular.module('grafana.directives');
-
-function grafanaMetricsQueryEditor() {
-  return {templateUrl: 'public/app/plugins/datasource/grafana/partials/query.editor.html'};
+class GrafanaQueryCtrl extends QueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/grafana/partials/query.editor.html';
 }
 
-
 export {
   GrafanaDatasource,
   GrafanaDatasource as Datasource,
-  grafanaMetricsQueryEditor as metricsQueryEditor
+  GrafanaQueryCtrl as QueryCtrl,
 };
 

+ 5 - 56
public/app/plugins/datasource/grafana/partials/query.editor.html

@@ -1,56 +1,5 @@
-<div class="tight-form">
-	<ul class="tight-form-list pull-right">
-		<li ng-show="parserError" class="tight-form-item">
-			<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
-				<i class="fa fa-warning"></i>
-			</a>
-		</li>
-		<li class="tight-form-item">
-			<div class="dropdown">
-				<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
-					<i class="fa fa-bars"></i>
-				</a>
-				<ul class="dropdown-menu pull-right" role="menu">
-					<li role="menuitem">
-						<a  tabindex="1"
-							ng-click="duplicate()">
-							Duplicate
-						</a>
-					</li>
-					<li role="menuitem">
-						<a  tabindex="1"
-							ng-click="moveMetricQuery($index, $index-1)">
-							Move up
-						</a>
-					</li>
-					<li role="menuitem">
-						<a  tabindex="1"
-							ng-click="moveMetricQuery($index, $index+1)">
-							Move down
-						</a>
-					</li>
-				</ul>
-			</div>
-		</li>
-		<li class="tight-form-item last">
-			<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
-				<i class="fa fa-remove"></i>
-			</a>
-		</li>
-	</ul>
-
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="min-width: 15px; text-align: center">
-			{{target.refId}}
-		</li>
-		<li>
-			<a class="tight-form-item" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
-				<i class="fa fa-eye"></i>
-			</a>
-		</li>
-		<li class="tight-form-item">
-			Test metric (fake data source)
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
+<query-editor-row ctrl="ctrl">
+	<li class="tight-form-item">
+		Test metric (fake data source)
+	</li>
+</query-editor-row>

+ 2 - 1
public/app/plugins/datasource/graphite/add_graphite_func.js

@@ -22,6 +22,7 @@ function (angular, _, $, gfunc) {
         link: function($scope, elem) {
           var categories = gfunc.getCategories();
           var allFunctions = getAllFunctionNames(categories);
+          var ctrl = $scope.ctrl;
 
           $scope.functionMenu = createFunctionDropDownMenu(categories);
 
@@ -48,7 +49,7 @@ function (angular, _, $, gfunc) {
               }
 
               $scope.$apply(function() {
-                $scope.addFunction(funcDef);
+                ctrl.addFunction(funcDef);
               });
 
               $input.trigger('blur');

+ 0 - 3
public/app/plugins/datasource/graphite/datasource.d.ts

@@ -1,3 +0,0 @@
-declare var Datasource: any;
-export default Datasource;
-

+ 0 - 296
public/app/plugins/datasource/graphite/datasource.js

@@ -1,296 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'jquery',
-  'app/core/config',
-  'app/core/utils/datemath',
-  './query_ctrl',
-  './func_editor',
-  './add_graphite_func',
-],
-function (angular, _, $, config, dateMath) {
-  'use strict';
-
-  /** @ngInject */
-  function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
-    this.basicAuth = instanceSettings.basicAuth;
-    this.url = instanceSettings.url;
-    this.name = instanceSettings.name;
-    this.cacheTimeout = instanceSettings.cacheTimeout;
-    this.withCredentials = instanceSettings.withCredentials;
-    this.render_method = instanceSettings.render_method || 'POST';
-
-    this.query = function(options) {
-      try {
-        var graphOptions = {
-          from: this.translateTime(options.rangeRaw.from, false),
-          until: this.translateTime(options.rangeRaw.to, true),
-          targets: options.targets,
-          format: options.format,
-          cacheTimeout: options.cacheTimeout || this.cacheTimeout,
-          maxDataPoints: options.maxDataPoints,
-        };
-
-        var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
-        if (params.length === 0) {
-          return $q.when([]);
-        }
-
-        if (options.format === 'png') {
-          return $q.when(this.url + '/render' + '?' + params.join('&'));
-        }
-
-        var httpOptions = { method: this.render_method, url: '/render' };
-
-        if (httpOptions.method === 'GET') {
-          httpOptions.url = httpOptions.url + '?' + params.join('&');
-        }
-        else {
-          httpOptions.data = params.join('&');
-          httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
-        }
-
-        return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
-      }
-      catch(err) {
-        return $q.reject(err);
-      }
-    };
-
-    this.convertDataPointsToMs = function(result) {
-      if (!result || !result.data) { return []; }
-      for (var i = 0; i < result.data.length; i++) {
-        var series = result.data[i];
-        for (var y = 0; y < series.datapoints.length; y++) {
-          series.datapoints[y][1] *= 1000;
-        }
-      }
-      return result;
-    };
-
-    this.annotationQuery = function(options) {
-      // Graphite metric as annotation
-      if (options.annotation.target) {
-        var target = templateSrv.replace(options.annotation.target);
-        var graphiteQuery = {
-          rangeRaw: options.rangeRaw,
-          targets: [{ target: target }],
-          format: 'json',
-          maxDataPoints: 100
-        };
-
-        return this.query(graphiteQuery)
-        .then(function(result) {
-          var list = [];
-
-          for (var i = 0; i < result.data.length; i++) {
-            var target = result.data[i];
-
-            for (var y = 0; y < target.datapoints.length; y++) {
-              var datapoint = target.datapoints[y];
-              if (!datapoint[0]) { continue; }
-
-              list.push({
-                annotation: options.annotation,
-                time: datapoint[1],
-                title: target.target
-              });
-            }
-          }
-
-          return list;
-        });
-      }
-      // Graphite event as annotation
-      else {
-        var tags = templateSrv.replace(options.annotation.tags);
-        return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
-          var list = [];
-          for (var i = 0; i < results.data.length; i++) {
-            var e = results.data[i];
-
-            list.push({
-              annotation: options.annotation,
-              time: e.when * 1000,
-              title: e.what,
-              tags: e.tags,
-              text: e.data
-            });
-          }
-          return list;
-        });
-      }
-    };
-
-    this.events = function(options) {
-      try {
-        var tags = '';
-        if (options.tags) {
-          tags = '&tags=' + options.tags;
-        }
-
-        return this.doGraphiteRequest({
-          method: 'GET',
-          url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
-            '&until=' + this.translateTime(options.range.to, true) + tags,
-        });
-      }
-      catch(err) {
-        return $q.reject(err);
-      }
-    };
-
-    this.translateTime = function(date, roundUp) {
-      if (_.isString(date)) {
-        if (date === 'now') {
-          return 'now';
-        }
-        else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
-          date = date.substring(3);
-          date = date.replace('m', 'min');
-          date = date.replace('M', 'mon');
-          return date;
-        }
-        date = dateMath.parse(date, roundUp);
-      }
-
-      // graphite' s from filter is exclusive
-      // here we step back one minute in order
-      // to guarantee that we get all the data that
-      // exists for the specified range
-      if (roundUp) {
-        if (date.get('s')) {
-          date.add(1, 'm');
-        }
-      }
-      else if (roundUp === false) {
-        if (date.get('s')) {
-          date.subtract(1, 'm');
-        }
-      }
-
-      return date.unix();
-    };
-
-    this.metricFindQuery = function(query) {
-      var interpolated;
-      try {
-        interpolated = encodeURIComponent(templateSrv.replace(query));
-      }
-      catch(err) {
-        return $q.reject(err);
-      }
-
-      return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
-      .then(function(results) {
-        return _.map(results.data, function(metric) {
-          return {
-            text: metric.text,
-            expandable: metric.expandable ? true : false
-          };
-        });
-      });
-    };
-
-    this.testDatasource = function() {
-      return this.metricFindQuery('*').then(function () {
-        return { status: "success", message: "Data source is working", title: "Success" };
-      });
-    };
-
-    this.listDashboards = function(query) {
-      return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} })
-      .then(function(results) {
-        return results.data.dashboards;
-      });
-    };
-
-    this.loadDashboard = function(dashName) {
-      return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
-    };
-
-    this.doGraphiteRequest = function(options) {
-      if (this.basicAuth || this.withCredentials) {
-        options.withCredentials = true;
-      }
-      if (this.basicAuth) {
-        options.headers = options.headers || {};
-        options.headers.Authorization = this.basicAuth;
-      }
-
-      options.url = this.url + options.url;
-      options.inspect = { type: 'graphite' };
-
-      return backendSrv.datasourceRequest(options);
-    };
-
-    this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    this.buildGraphiteParams = function(options, scopedVars) {
-      var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
-      var clean_options = [], targets = {};
-      var target, targetValue, i;
-      var regex = /\#([A-Z])/g;
-      var intervalFormatFixRegex = /'(\d+)m'/gi;
-      var hasTargets = false;
-
-      if (options.format !== 'png') {
-        options['format'] = 'json';
-      }
-
-      function fixIntervalFormat(match) {
-        return match.replace('m', 'min').replace('M', 'mon');
-      }
-
-      for (i = 0; i < options.targets.length; i++) {
-        target = options.targets[i];
-        if (!target.target) {
-          continue;
-        }
-
-        if (!target.refId) {
-          target.refId = this._seriesRefLetters[i];
-        }
-
-        targetValue = templateSrv.replace(target.target, scopedVars);
-        targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
-        targets[target.refId] = targetValue;
-      }
-
-      function nestedSeriesRegexReplacer(match, g1) {
-        return targets[g1];
-      }
-
-      for (i = 0; i < options.targets.length; i++) {
-        target = options.targets[i];
-        if (!target.target) {
-          continue;
-        }
-
-        targetValue = targets[target.refId];
-        targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
-        targets[target.refId] = targetValue;
-
-        if (!target.hide) {
-          hasTargets = true;
-          clean_options.push("target=" + encodeURIComponent(targetValue));
-        }
-      }
-
-      _.each(options, function (value, key) {
-        if ($.inArray(key, graphite_options) === -1) { return; }
-        if (value) {
-          clean_options.push(key + "=" + encodeURIComponent(value));
-        }
-      });
-
-      if (!hasTargets) {
-        return [];
-      }
-
-      return clean_options;
-    };
-  }
-
-  return GraphiteDatasource;
-});

+ 281 - 0
public/app/plugins/datasource/graphite/datasource.ts

@@ -0,0 +1,281 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+import moment from 'moment';
+
+import * as dateMath from 'app/core/utils/datemath';
+
+/** @ngInject */
+export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+  this.basicAuth = instanceSettings.basicAuth;
+  this.url = instanceSettings.url;
+  this.name = instanceSettings.name;
+  this.cacheTimeout = instanceSettings.cacheTimeout;
+  this.withCredentials = instanceSettings.withCredentials;
+  this.render_method = instanceSettings.render_method || 'POST';
+
+  this.query = function(options) {
+    try {
+      var graphOptions = {
+        from: this.translateTime(options.rangeRaw.from, false),
+        until: this.translateTime(options.rangeRaw.to, true),
+        targets: options.targets,
+        format: options.format,
+        cacheTimeout: options.cacheTimeout || this.cacheTimeout,
+        maxDataPoints: options.maxDataPoints,
+      };
+
+      var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
+      if (params.length === 0) {
+        return $q.when([]);
+      }
+
+      if (options.format === 'png') {
+        return $q.when(this.url + '/render' + '?' + params.join('&'));
+      }
+
+      var httpOptions: any = {method: this.render_method, url: '/render'};
+
+      if (httpOptions.method === 'GET') {
+        httpOptions.url = httpOptions.url + '?' + params.join('&');
+      } else {
+        httpOptions.data = params.join('&');
+        httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
+      }
+
+      return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
+    } catch (err) {
+      return $q.reject(err);
+    }
+  };
+
+  this.convertDataPointsToMs = function(result) {
+    if (!result || !result.data) { return []; }
+    for (var i = 0; i < result.data.length; i++) {
+      var series = result.data[i];
+      for (var y = 0; y < series.datapoints.length; y++) {
+        series.datapoints[y][1] *= 1000;
+      }
+    }
+    return result;
+  };
+
+  this.annotationQuery = function(options) {
+    // Graphite metric as annotation
+    if (options.annotation.target) {
+      var target = templateSrv.replace(options.annotation.target);
+      var graphiteQuery = {
+        rangeRaw: options.rangeRaw,
+        targets: [{ target: target }],
+        format: 'json',
+        maxDataPoints: 100
+      };
+
+      return this.query(graphiteQuery)
+      .then(function(result) {
+        var list = [];
+
+        for (var i = 0; i < result.data.length; i++) {
+          var target = result.data[i];
+
+          for (var y = 0; y < target.datapoints.length; y++) {
+            var datapoint = target.datapoints[y];
+            if (!datapoint[0]) { continue; }
+
+            list.push({
+              annotation: options.annotation,
+              time: datapoint[1],
+              title: target.target
+            });
+          }
+        }
+
+        return list;
+      });
+    } else {
+      // Graphite event as annotation
+      var tags = templateSrv.replace(options.annotation.tags);
+      return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
+        var list = [];
+        for (var i = 0; i < results.data.length; i++) {
+          var e = results.data[i];
+
+          list.push({
+            annotation: options.annotation,
+            time: e.when * 1000,
+            title: e.what,
+            tags: e.tags,
+            text: e.data
+          });
+        }
+        return list;
+      });
+    }
+  };
+
+  this.events = function(options) {
+    try {
+      var tags = '';
+      if (options.tags) {
+        tags = '&tags=' + options.tags;
+      }
+
+      return this.doGraphiteRequest({
+        method: 'GET',
+        url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
+          '&until=' + this.translateTime(options.range.to, true) + tags,
+      });
+    } catch (err) {
+      return $q.reject(err);
+    }
+  };
+
+  this.translateTime = function(date, roundUp) {
+    if (_.isString(date)) {
+      if (date === 'now') {
+        return 'now';
+      } else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
+        date = date.substring(3);
+        date = date.replace('m', 'min');
+        date = date.replace('M', 'mon');
+        return date;
+      }
+      date = dateMath.parse(date, roundUp);
+    }
+
+    // graphite' s from filter is exclusive
+    // here we step back one minute in order
+    // to guarantee that we get all the data that
+    // exists for the specified range
+    if (roundUp) {
+      if (date.get('s')) {
+        date.add(1, 'm');
+      }
+    } else if (roundUp === false) {
+      if (date.get('s')) {
+        date.subtract(1, 'm');
+      }
+    }
+
+    return date.unix();
+  };
+
+  this.metricFindQuery = function(query) {
+    var interpolated;
+    try {
+      interpolated = encodeURIComponent(templateSrv.replace(query));
+    } catch (err) {
+      return $q.reject(err);
+    }
+
+    return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
+    .then(function(results) {
+      return _.map(results.data, function(metric) {
+        return {
+          text: metric.text,
+          expandable: metric.expandable ? true : false
+        };
+      });
+    });
+  };
+
+  this.testDatasource = function() {
+    return this.metricFindQuery('*').then(function () {
+      return { status: "success", message: "Data source is working", title: "Success" };
+    });
+  };
+
+  this.listDashboards = function(query) {
+    return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} })
+    .then(function(results) {
+      return results.data.dashboards;
+    });
+  };
+
+  this.loadDashboard = function(dashName) {
+    return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
+  };
+
+  this.doGraphiteRequest = function(options) {
+    if (this.basicAuth || this.withCredentials) {
+      options.withCredentials = true;
+    }
+    if (this.basicAuth) {
+      options.headers = options.headers || {};
+      options.headers.Authorization = this.basicAuth;
+    }
+
+    options.url = this.url + options.url;
+    options.inspect = { type: 'graphite' };
+
+    return backendSrv.datasourceRequest(options);
+  };
+
+  this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+  this.buildGraphiteParams = function(options, scopedVars) {
+    var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
+    var clean_options = [], targets = {};
+    var target, targetValue, i;
+    var regex = /\#([A-Z])/g;
+    var intervalFormatFixRegex = /'(\d+)m'/gi;
+    var hasTargets = false;
+
+    if (options.format !== 'png') {
+      options['format'] = 'json';
+    }
+
+    function fixIntervalFormat(match) {
+      return match.replace('m', 'min').replace('M', 'mon');
+    }
+
+    for (i = 0; i < options.targets.length; i++) {
+      target = options.targets[i];
+      if (!target.target) {
+        continue;
+      }
+
+      if (!target.refId) {
+        target.refId = this._seriesRefLetters[i];
+      }
+
+      targetValue = templateSrv.replace(target.target, scopedVars);
+      targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
+      targets[target.refId] = targetValue;
+    }
+
+    function nestedSeriesRegexReplacer(match, g1) {
+      return targets[g1];
+    }
+
+    for (i = 0; i < options.targets.length; i++) {
+      target = options.targets[i];
+      if (!target.target) {
+        continue;
+      }
+
+      targetValue = targets[target.refId];
+      targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
+      targets[target.refId] = targetValue;
+
+      if (!target.hide) {
+        hasTargets = true;
+        clean_options.push("target=" + encodeURIComponent(targetValue));
+      }
+    }
+
+    _.each(options, function (value, key) {
+      if (_.indexOf(graphite_options, key) === -1) { return; }
+      if (value) {
+        clean_options.push(key + "=" + encodeURIComponent(value));
+      }
+    });
+
+    if (!hasTargets) {
+      return [];
+    }
+
+    return clean_options;
+  };
+}

+ 10 - 7
public/app/plugins/datasource/graphite/func_editor.js

@@ -27,6 +27,7 @@ function (angular, _, $) {
         link: function postLink($scope, elem) {
           var $funcLink = $(funcSpanTemplate);
           var $funcControls = $(funcControlsTemplate);
+          var ctrl = $scope.ctrl;
           var func = $scope.func;
           var funcDef = func.def;
           var scheduledRelink = false;
@@ -79,11 +80,13 @@ function (angular, _, $) {
               func.updateParam($input.val(), paramIndex);
               scheduledRelinkIfNeeded();
 
-              $scope.$apply($scope.targetChanged);
-            }
+              $scope.$apply(function() {
+                ctrl.targetChanged();
+              });
 
-            $input.hide();
-            $link.show();
+              $input.hide();
+              $link.show();
+            }
           }
 
           function inputKeyPress(paramIndex, e) {
@@ -198,7 +201,7 @@ function (angular, _, $) {
               if ($target.hasClass('fa-remove')) {
                 toggleFuncControls();
                 $scope.$apply(function() {
-                  $scope.removeFunction($scope.func);
+                  ctrl.removeFunction($scope.func);
                 });
                 return;
               }
@@ -206,7 +209,7 @@ function (angular, _, $) {
               if ($target.hasClass('fa-arrow-left')) {
                 $scope.$apply(function() {
                   _.move($scope.functions, $scope.$index, $scope.$index - 1);
-                  $scope.targetChanged();
+                  ctrl.targetChanged();
                 });
                 return;
               }
@@ -214,7 +217,7 @@ function (angular, _, $) {
               if ($target.hasClass('fa-arrow-right')) {
                 $scope.$apply(function() {
                   _.move($scope.functions, $scope.$index, $scope.$index + 1);
-                  $scope.targetChanged();
+                  ctrl.targetChanged();
                 });
                 return;
               }

+ 0 - 682
public/app/plugins/datasource/graphite/lexer.js

@@ -1,682 +0,0 @@
-define([
-  'lodash'
-], function(_) {
-  'use strict';
-
-  // This is auto generated from the unicode tables.
-  // The tables are at:
-  // http://www.fileformat.info/info/unicode/category/Lu/list.htm
-  // http://www.fileformat.info/info/unicode/category/Ll/list.htm
-  // http://www.fileformat.info/info/unicode/category/Lt/list.htm
-  // http://www.fileformat.info/info/unicode/category/Lm/list.htm
-  // http://www.fileformat.info/info/unicode/category/Lo/list.htm
-  // http://www.fileformat.info/info/unicode/category/Nl/list.htm
-
-  var unicodeLetterTable = [
-    170, 170, 181, 181, 186, 186, 192, 214,
-    216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
-    880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
-    910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
-    1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
-    1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
-    1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
-    1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
-    2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
-    2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
-    2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
-    2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
-    2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
-    2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
-    2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
-    2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
-    2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
-    2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
-    2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
-    2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
-    3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
-    3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
-    3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
-    3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
-    3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
-    3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
-    3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
-    3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
-    3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
-    3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
-    3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
-    4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
-    4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
-    4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
-    4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
-    4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
-    4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
-    5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
-    5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
-    6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
-    6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
-    6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
-    6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
-    7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
-    7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
-    8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
-    8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
-    8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
-    8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
-    8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
-    8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
-    8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
-    11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
-    11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
-    11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
-    11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
-    12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
-    12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
-    12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
-    19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
-    42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
-    42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
-    42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
-    43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
-    43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
-    43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
-    43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
-    43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
-    43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
-    43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
-    43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
-    55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
-    64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
-    64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
-    64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
-    64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
-    65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
-    65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
-    65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
-    65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
-    65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
-    66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
-    66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
-    67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
-    67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
-    68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
-    68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
-    69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
-    92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
-    119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
-    119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
-    120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
-    120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
-    120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
-    120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
-    120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
-    131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
-    177984, 177984, 178205, 178205, 194560, 195101
-  ];
-
-  var identifierStartTable = [];
-
-  for (var i = 0; i < 128; i++) {
-    identifierStartTable[i] =
-      i >= 48 && i <= 57 || // 0-9
-      i === 36 ||           // $
-      i === 126 ||          // ~
-      i === 124 ||          // |
-      i >= 65 && i <= 90 || // A-Z
-      i === 95 ||           // _
-      i === 45 ||           // -
-      i === 42 ||           // *
-      i === 58 ||           // :
-      i === 91 ||           // templateStart [
-      i === 93 ||           // templateEnd ]
-      i === 63 ||           // ?
-      i === 37 ||           // %
-      i === 35 ||           // #
-      i === 61 ||           // =
-      i >= 97 && i <= 122;  // a-z
-  }
-
-  var identifierPartTable = [];
-
-  for (var i2 = 0; i2 < 128; i2++) {
-    identifierPartTable[i2] =
-      identifierStartTable[i2] || // $, _, A-Z, a-z
-      i2 >= 48 && i2 <= 57;        // 0-9
-  }
-
-  function Lexer(expression) {
-    this.input = expression;
-    this.char = 1;
-    this.from = 1;
-  }
-
-  Lexer.prototype = {
-
-    peek: function (i) {
-      return this.input.charAt(i || 0);
-    },
-
-    skip: function (i) {
-      i = i || 1;
-      this.char += i;
-      this.input = this.input.slice(i);
-    },
-
-    tokenize: function() {
-      var list = [];
-      var token;
-      while (token = this.next()) {
-        list.push(token);
-      }
-      return list;
-    },
-
-    next: function() {
-      this.from = this.char;
-
-      // Move to the next non-space character.
-      var start;
-      if (/\s/.test(this.peek())) {
-        start = this.char;
-
-        while (/\s/.test(this.peek())) {
-          this.from += 1;
-          this.skip();
-        }
-
-        if (this.peek() === "") { // EOL
-          return null;
-        }
-      }
-
-      var match = this.scanStringLiteral();
-      if (match) {
-        return match;
-      }
-
-      match =
-        this.scanPunctuator() ||
-        this.scanNumericLiteral() ||
-        this.scanIdentifier() ||
-        this.scanTemplateSequence();
-
-      if (match) {
-        this.skip(match.value.length);
-        return match;
-      }
-
-      // No token could be matched, give up.
-      return null;
-    },
-
-    scanTemplateSequence: function() {
-      if (this.peek() === '[' && this.peek(1) === '[') {
-        return {
-          type: 'templateStart',
-          value: '[[',
-          pos: this.char
-        };
-      }
-
-      if (this.peek() === ']' && this.peek(1) === ']') {
-        return {
-          type: 'templateEnd',
-          value: '[[',
-          pos: this.char
-        };
-      }
-
-      return null;
-    },
-
-      /*
-     * Extract a JavaScript identifier out of the next sequence of
-     * characters or return 'null' if its not possible. In addition,
-     * to Identifier this method can also produce BooleanLiteral
-     * (true/false) and NullLiteral (null).
-     */
-    scanIdentifier: function() {
-      var id = "";
-      var index = 0;
-      var type, char;
-
-      // Detects any character in the Unicode categories "Uppercase
-      // letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
-      // (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
-      // "Letter number (Nl)".
-      //
-      // Both approach and unicodeLetterTable were borrowed from
-      // Google's Traceur.
-
-      function isUnicodeLetter(code) {
-        for (var i = 0; i < unicodeLetterTable.length;) {
-          if (code < unicodeLetterTable[i++]) {
-            return false;
-          }
-
-          if (code <= unicodeLetterTable[i++]) {
-            return true;
-          }
-        }
-
-        return false;
-      }
-
-      function isHexDigit(str) {
-        return (/^[0-9a-fA-F]$/).test(str);
-      }
-
-      var readUnicodeEscapeSequence = _.bind(function () {
-        /*jshint validthis:true */
-        index += 1;
-
-        if (this.peek(index) !== "u") {
-          return null;
-        }
-
-        var ch1 = this.peek(index + 1);
-        var ch2 = this.peek(index + 2);
-        var ch3 = this.peek(index + 3);
-        var ch4 = this.peek(index + 4);
-        var code;
-
-        if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
-          code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
-
-          if (isUnicodeLetter(code)) {
-            index += 5;
-            return "\\u" + ch1 + ch2 + ch3 + ch4;
-          }
-
-          return null;
-        }
-
-        return null;
-      }, this);
-
-      var getIdentifierStart = _.bind(function () {
-        /*jshint validthis:true */
-        var chr = this.peek(index);
-        var code = chr.charCodeAt(0);
-
-        if (chr === '*') {
-          index += 1;
-          return chr;
-        }
-
-        if (code === 92) {
-          return readUnicodeEscapeSequence();
-        }
-
-        if (code < 128) {
-          if (identifierStartTable[code]) {
-            index += 1;
-            return chr;
-          }
-
-          return null;
-        }
-
-        if (isUnicodeLetter(code)) {
-          index += 1;
-          return chr;
-        }
-
-        return null;
-      }, this);
-
-      var getIdentifierPart = _.bind(function () {
-        /*jshint validthis:true */
-        var chr = this.peek(index);
-        var code = chr.charCodeAt(0);
-
-        if (code === 92) {
-          return readUnicodeEscapeSequence();
-        }
-
-        if (code < 128) {
-          if (identifierPartTable[code]) {
-            index += 1;
-            return chr;
-          }
-
-          return null;
-        }
-
-        if (isUnicodeLetter(code)) {
-          index += 1;
-          return chr;
-        }
-
-        return null;
-      }, this);
-
-      char = getIdentifierStart();
-      if (char === null) {
-        return null;
-      }
-
-      id = char;
-      for (;;) {
-        char = getIdentifierPart();
-
-        if (char === null) {
-          break;
-        }
-
-        id += char;
-      }
-
-      switch (id) {
-      case 'true': {
-        type = 'bool';
-        break;
-      }
-      case 'false': {
-        type = 'bool';
-        break;
-      }
-      default:
-        type = "identifier";
-      }
-
-      return {
-        type: type,
-        value: id,
-        pos: this.char
-      };
-
-    },
-
-    /*
-     * Extract a numeric literal out of the next sequence of
-     * characters or return 'null' if its not possible. This method
-     * supports all numeric literals described in section 7.8.3
-     * of the EcmaScript 5 specification.
-     *
-     * This method's implementation was heavily influenced by the
-     * scanNumericLiteral function in the Esprima parser's source code.
-     */
-    scanNumericLiteral: function () {
-      var index = 0;
-      var value = "";
-      var length = this.input.length;
-      var char = this.peek(index);
-      var bad;
-
-      function isDecimalDigit(str) {
-        return (/^[0-9]$/).test(str);
-      }
-
-      function isOctalDigit(str) {
-        return (/^[0-7]$/).test(str);
-      }
-
-      function isHexDigit(str) {
-        return (/^[0-9a-fA-F]$/).test(str);
-      }
-
-      function isIdentifierStart(ch) {
-        return (ch === "$") || (ch === "_") || (ch === "\\") ||
-          (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
-      }
-
-      // handle negative num literals
-      if (char === '-') {
-        value += char;
-        index += 1;
-        char = this.peek(index);
-      }
-
-      // Numbers must start either with a decimal digit or a point.
-      if (char !== "." && !isDecimalDigit(char)) {
-        return null;
-      }
-
-      if (char !== ".") {
-        value += this.peek(index);
-        index += 1;
-        char = this.peek(index);
-
-        if (value === "0") {
-          // Base-16 numbers.
-          if (char === "x" || char === "X") {
-            index += 1;
-            value += char;
-
-            while (index < length) {
-              char = this.peek(index);
-              if (!isHexDigit(char)) {
-                break;
-              }
-              value += char;
-              index += 1;
-            }
-
-            if (value.length <= 2) { // 0x
-              return {
-                type: 'number',
-                value: value,
-                isMalformed: true,
-                pos: this.char
-              };
-            }
-
-            if (index < length) {
-              char = this.peek(index);
-              if (isIdentifierStart(char)) {
-                return null;
-              }
-            }
-
-            return {
-              type: 'number',
-              value: value,
-              base: 16,
-              isMalformed: false,
-              pos: this.char
-            };
-          }
-
-          // Base-8 numbers.
-          if (isOctalDigit(char)) {
-            index += 1;
-            value += char;
-            bad = false;
-
-            while (index < length) {
-              char = this.peek(index);
-
-              // Numbers like '019' (note the 9) are not valid octals
-              // but we still parse them and mark as malformed.
-
-              if (isDecimalDigit(char)) {
-                bad = true;
-              } else if (!isOctalDigit(char)) {
-                break;
-              }
-              value += char;
-              index += 1;
-            }
-
-            if (index < length) {
-              char = this.peek(index);
-              if (isIdentifierStart(char)) {
-                return null;
-              }
-            }
-
-            return {
-              type: 'number',
-              value: value,
-              base: 8,
-              isMalformed: false
-            };
-          }
-
-          // Decimal numbers that start with '0' such as '09' are illegal
-          // but we still parse them and return as malformed.
-
-          if (isDecimalDigit(char)) {
-            index += 1;
-            value += char;
-          }
-        }
-
-        while (index < length) {
-          char = this.peek(index);
-          if (!isDecimalDigit(char)) {
-            break;
-          }
-          value += char;
-          index += 1;
-        }
-      }
-
-      // Decimal digits.
-
-      if (char === ".") {
-        value += char;
-        index += 1;
-
-        while (index < length) {
-          char = this.peek(index);
-          if (!isDecimalDigit(char)) {
-            break;
-          }
-          value += char;
-          index += 1;
-        }
-      }
-
-      // Exponent part.
-
-      if (char === "e" || char === "E") {
-        value += char;
-        index += 1;
-        char = this.peek(index);
-
-        if (char === "+" || char === "-") {
-          value += this.peek(index);
-          index += 1;
-        }
-
-        char = this.peek(index);
-        if (isDecimalDigit(char)) {
-          value += char;
-          index += 1;
-
-          while (index < length) {
-            char = this.peek(index);
-            if (!isDecimalDigit(char)) {
-              break;
-            }
-            value += char;
-            index += 1;
-          }
-        } else {
-          return null;
-        }
-      }
-
-      if (index < length) {
-        char = this.peek(index);
-        if (!this.isPunctuator(char)) {
-          return null;
-        }
-      }
-
-      return {
-        type: 'number',
-        value: value,
-        base: 10,
-        pos: this.char,
-        isMalformed: !isFinite(value)
-      };
-    },
-
-    isPunctuator: function (ch1) {
-      switch (ch1) {
-      case ".":
-      case "(":
-      case ")":
-      case ",":
-      case "{":
-      case "}":
-        return true;
-      }
-
-      return false;
-    },
-
-    scanPunctuator: function () {
-      var ch1 = this.peek();
-
-      if (this.isPunctuator(ch1)) {
-        return {
-          type: ch1,
-          value: ch1,
-          pos: this.char
-        };
-      }
-
-      return null;
-    },
-
-      /*
-     * Extract a string out of the next sequence of characters and/or
-     * lines or return 'null' if its not possible. Since strings can
-     * span across multiple lines this method has to move the char
-     * pointer.
-     *
-     * This method recognizes pseudo-multiline JavaScript strings:
-     *
-     *   var str = "hello\
-     *   world";
-     */
-    scanStringLiteral: function () {
-      /*jshint loopfunc:true */
-      var quote = this.peek();
-
-      // String must start with a quote.
-      if (quote !== "\"" && quote !== "'") {
-        return null;
-      }
-
-      var value = "";
-
-      this.skip();
-
-      while (this.peek() !== quote) {
-        if (this.peek() === "") { // End Of Line
-          return {
-            type: 'string',
-            value: value,
-            isUnclosed: true,
-            quote: quote,
-            pos: this.char
-          };
-        }
-
-        var char = this.peek();
-        var jump = 1; // A length of a jump, after we're done
-                      // parsing this character.
-
-        value += char;
-        this.skip(jump);
-      }
-
-      this.skip();
-      return {
-        type: 'string',
-        value: value,
-        isUnclosed: false,
-        quote: quote,
-        pos: this.char
-      };
-    },
-
-  };
-
-  return Lexer;
-
-});

+ 678 - 0
public/app/plugins/datasource/graphite/lexer.ts

@@ -0,0 +1,678 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import _ from 'lodash';
+
+// This is auto generated from the unicode tables.
+// The tables are at:
+// http://www.fileformat.info/info/unicode/category/Lu/list.htm
+// http://www.fileformat.info/info/unicode/category/Ll/list.htm
+// http://www.fileformat.info/info/unicode/category/Lt/list.htm
+// http://www.fileformat.info/info/unicode/category/Lm/list.htm
+// http://www.fileformat.info/info/unicode/category/Lo/list.htm
+// http://www.fileformat.info/info/unicode/category/Nl/list.htm
+
+var unicodeLetterTable = [
+  170, 170, 181, 181, 186, 186, 192, 214,
+  216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
+  880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
+  910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
+  1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
+  1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
+  1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
+  1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
+  2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
+  2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
+  2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
+  2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
+  2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
+  2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
+  2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
+  2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
+  2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
+  2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
+  2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
+  2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
+  3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
+  3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
+  3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
+  3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
+  3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
+  3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
+  3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
+  3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
+  3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
+  3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
+  3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
+  4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
+  4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
+  4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
+  4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
+  4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
+  4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
+  5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
+  5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
+  6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
+  6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
+  6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
+  6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
+  7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
+  7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
+  8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
+  8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
+  8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
+  8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
+  8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
+  8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
+  8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
+  11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
+  11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
+  11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
+  11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
+  12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
+  12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
+  12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
+  19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
+  42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
+  42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
+  42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
+  43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
+  43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
+  43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
+  43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
+  43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
+  43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
+  43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
+  43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
+  55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
+  64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
+  64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
+  64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
+  64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
+  65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
+  65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
+  65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
+  65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
+  65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
+  66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
+  66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
+  67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
+  67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
+  68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
+  68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
+  69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
+  92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
+  119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
+  119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
+  120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
+  120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
+  120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
+  120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
+  120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
+  131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
+  177984, 177984, 178205, 178205, 194560, 195101
+];
+
+var identifierStartTable = [];
+
+for (var i = 0; i < 128; i++) {
+  identifierStartTable[i] =
+    i >= 48 && i <= 57 || // 0-9
+    i === 36 ||           // $
+    i === 126 ||          // ~
+    i === 124 ||          // |
+    i >= 65 && i <= 90 || // A-Z
+    i === 95 ||           // _
+    i === 45 ||           // -
+    i === 42 ||           // *
+    i === 58 ||           // :
+    i === 91 ||           // templateStart [
+    i === 93 ||           // templateEnd ]
+    i === 63 ||           // ?
+    i === 37 ||           // %
+    i === 35 ||           // #
+    i === 61 ||           // =
+    i >= 97 && i <= 122;  // a-z
+}
+
+var identifierPartTable = [];
+
+for (var i2 = 0; i2 < 128; i2++) {
+  identifierPartTable[i2] =
+    identifierStartTable[i2] || // $, _, A-Z, a-z
+    i2 >= 48 && i2 <= 57;        // 0-9
+}
+
+export function Lexer(expression) {
+  this.input = expression;
+  this.char = 1;
+  this.from = 1;
+}
+
+Lexer.prototype = {
+
+  peek: function (i) {
+    return this.input.charAt(i || 0);
+  },
+
+  skip: function (i) {
+    i = i || 1;
+    this.char += i;
+    this.input = this.input.slice(i);
+  },
+
+  tokenize: function() {
+    var list = [];
+    var token;
+    while (token = this.next()) {
+      list.push(token);
+    }
+    return list;
+  },
+
+  next: function() {
+    this.from = this.char;
+
+    // Move to the next non-space character.
+    var start;
+    if (/\s/.test(this.peek())) {
+      start = this.char;
+
+      while (/\s/.test(this.peek())) {
+        this.from += 1;
+        this.skip();
+      }
+
+      if (this.peek() === "") { // EOL
+        return null;
+      }
+    }
+
+    var match = this.scanStringLiteral();
+    if (match) {
+      return match;
+    }
+
+    match =
+      this.scanPunctuator() ||
+      this.scanNumericLiteral() ||
+      this.scanIdentifier() ||
+      this.scanTemplateSequence();
+
+    if (match) {
+      this.skip(match.value.length);
+      return match;
+    }
+
+    // No token could be matched, give up.
+    return null;
+  },
+
+  scanTemplateSequence: function() {
+    if (this.peek() === '[' && this.peek(1) === '[') {
+      return {
+        type: 'templateStart',
+        value: '[[',
+        pos: this.char
+      };
+    }
+
+    if (this.peek() === ']' && this.peek(1) === ']') {
+      return {
+        type: 'templateEnd',
+        value: '[[',
+        pos: this.char
+      };
+    }
+
+    return null;
+  },
+
+  /*
+   * Extract a JavaScript identifier out of the next sequence of
+   * characters or return 'null' if its not possible. In addition,
+   * to Identifier this method can also produce BooleanLiteral
+   * (true/false) and NullLiteral (null).
+   */
+  scanIdentifier: function() {
+    var id = "";
+    var index = 0;
+    var type, char;
+
+    // Detects any character in the Unicode categories "Uppercase
+    // letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
+    // (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
+    // "Letter number (Nl)".
+    //
+    // Both approach and unicodeLetterTable were borrowed from
+    // Google's Traceur.
+
+    function isUnicodeLetter(code) {
+      for (var i = 0; i < unicodeLetterTable.length;) {
+        if (code < unicodeLetterTable[i++]) {
+          return false;
+        }
+
+        if (code <= unicodeLetterTable[i++]) {
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+    function isHexDigit(str) {
+      return (/^[0-9a-fA-F]$/).test(str);
+    }
+
+    var readUnicodeEscapeSequence = _.bind(function () {
+      /*jshint validthis:true */
+      index += 1;
+
+      if (this.peek(index) !== "u") {
+        return null;
+      }
+
+      var ch1 = this.peek(index + 1);
+      var ch2 = this.peek(index + 2);
+      var ch3 = this.peek(index + 3);
+      var ch4 = this.peek(index + 4);
+      var code;
+
+      if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
+        code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
+
+        if (isUnicodeLetter(code)) {
+          index += 5;
+          return "\\u" + ch1 + ch2 + ch3 + ch4;
+        }
+
+        return null;
+      }
+
+      return null;
+    }, this);
+
+    var getIdentifierStart = _.bind(function () {
+      /*jshint validthis:true */
+      var chr = this.peek(index);
+      var code = chr.charCodeAt(0);
+
+      if (chr === '*') {
+        index += 1;
+        return chr;
+      }
+
+      if (code === 92) {
+        return readUnicodeEscapeSequence();
+      }
+
+      if (code < 128) {
+        if (identifierStartTable[code]) {
+          index += 1;
+          return chr;
+        }
+
+        return null;
+      }
+
+      if (isUnicodeLetter(code)) {
+        index += 1;
+        return chr;
+      }
+
+      return null;
+    }, this);
+
+    var getIdentifierPart = _.bind(function () {
+      /*jshint validthis:true */
+      var chr = this.peek(index);
+      var code = chr.charCodeAt(0);
+
+      if (code === 92) {
+        return readUnicodeEscapeSequence();
+      }
+
+      if (code < 128) {
+        if (identifierPartTable[code]) {
+          index += 1;
+          return chr;
+        }
+
+        return null;
+      }
+
+      if (isUnicodeLetter(code)) {
+        index += 1;
+        return chr;
+      }
+
+      return null;
+    }, this);
+
+    char = getIdentifierStart();
+    if (char === null) {
+      return null;
+    }
+
+    id = char;
+    for (;;) {
+      char = getIdentifierPart();
+
+      if (char === null) {
+        break;
+      }
+
+      id += char;
+    }
+
+    switch (id) {
+      case 'true': {
+        type = 'bool';
+        break;
+      }
+      case 'false': {
+        type = 'bool';
+        break;
+      }
+      default:
+        type = "identifier";
+    }
+
+    return {
+      type: type,
+      value: id,
+      pos: this.char
+    };
+
+  },
+
+  /*
+   * Extract a numeric literal out of the next sequence of
+   * characters or return 'null' if its not possible. This method
+   * supports all numeric literals described in section 7.8.3
+   * of the EcmaScript 5 specification.
+   *
+   * This method's implementation was heavily influenced by the
+   * scanNumericLiteral function in the Esprima parser's source code.
+   */
+  scanNumericLiteral: function (): any {
+    var index = 0;
+    var value = "";
+    var length = this.input.length;
+    var char = this.peek(index);
+    var bad;
+
+    function isDecimalDigit(str) {
+      return (/^[0-9]$/).test(str);
+    }
+
+    function isOctalDigit(str) {
+      return (/^[0-7]$/).test(str);
+    }
+
+    function isHexDigit(str) {
+      return (/^[0-9a-fA-F]$/).test(str);
+    }
+
+    function isIdentifierStart(ch) {
+      return (ch === "$") || (ch === "_") || (ch === "\\") ||
+        (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
+    }
+
+    // handle negative num literals
+    if (char === '-') {
+      value += char;
+      index += 1;
+    char = this.peek(index);
+      }
+
+      // Numbers must start either with a decimal digit or a point.
+      if (char !== "." && !isDecimalDigit(char)) {
+        return null;
+      }
+
+      if (char !== ".") {
+        value += this.peek(index);
+        index += 1;
+        char = this.peek(index);
+
+        if (value === "0") {
+          // Base-16 numbers.
+          if (char === "x" || char === "X") {
+            index += 1;
+            value += char;
+
+            while (index < length) {
+              char = this.peek(index);
+              if (!isHexDigit(char)) {
+                break;
+              }
+              value += char;
+              index += 1;
+            }
+
+            if (value.length <= 2) { // 0x
+              return {
+                type: 'number',
+                value: value,
+                isMalformed: true,
+                pos: this.char
+              };
+            }
+
+            if (index < length) {
+              char = this.peek(index);
+              if (isIdentifierStart(char)) {
+                return null;
+              }
+            }
+
+            return {
+              type: 'number',
+              value: value,
+              base: 16,
+              isMalformed: false,
+              pos: this.char
+            };
+          }
+
+          // Base-8 numbers.
+          if (isOctalDigit(char)) {
+            index += 1;
+            value += char;
+            bad = false;
+
+            while (index < length) {
+              char = this.peek(index);
+
+              // Numbers like '019' (note the 9) are not valid octals
+              // but we still parse them and mark as malformed.
+
+              if (isDecimalDigit(char)) {
+                bad = true;
+              } else if (!isOctalDigit(char)) {
+                break;
+              }
+              value += char;
+              index += 1;
+            }
+
+            if (index < length) {
+              char = this.peek(index);
+              if (isIdentifierStart(char)) {
+                return null;
+              }
+            }
+
+            return {
+              type: 'number',
+              value: value,
+              base: 8,
+              isMalformed: false
+            };
+          }
+
+          // Decimal numbers that start with '0' such as '09' are illegal
+          // but we still parse them and return as malformed.
+
+          if (isDecimalDigit(char)) {
+            index += 1;
+            value += char;
+          }
+        }
+
+        while (index < length) {
+          char = this.peek(index);
+          if (!isDecimalDigit(char)) {
+            break;
+          }
+          value += char;
+          index += 1;
+        }
+      }
+
+      // Decimal digits.
+
+      if (char === ".") {
+        value += char;
+        index += 1;
+
+        while (index < length) {
+          char = this.peek(index);
+          if (!isDecimalDigit(char)) {
+            break;
+          }
+          value += char;
+          index += 1;
+        }
+      }
+
+      // Exponent part.
+
+      if (char === "e" || char === "E") {
+        value += char;
+        index += 1;
+        char = this.peek(index);
+
+        if (char === "+" || char === "-") {
+          value += this.peek(index);
+          index += 1;
+        }
+
+        char = this.peek(index);
+        if (isDecimalDigit(char)) {
+          value += char;
+          index += 1;
+
+          while (index < length) {
+            char = this.peek(index);
+            if (!isDecimalDigit(char)) {
+              break;
+            }
+            value += char;
+            index += 1;
+          }
+        } else {
+          return null;
+        }
+      }
+
+      if (index < length) {
+        char = this.peek(index);
+        if (!this.isPunctuator(char)) {
+          return null;
+        }
+      }
+
+      return {
+        type: 'number',
+        value: value,
+        base: 10,
+        pos: this.char,
+        isMalformed: !isFinite(+value)
+      };
+    },
+
+    isPunctuator: function (ch1) {
+      switch (ch1) {
+        case ".":
+          case "(":
+          case ")":
+          case ",":
+          case "{":
+          case "}":
+          return true;
+      }
+
+      return false;
+    },
+
+    scanPunctuator: function () {
+      var ch1 = this.peek();
+
+      if (this.isPunctuator(ch1)) {
+        return {
+          type: ch1,
+          value: ch1,
+          pos: this.char
+        };
+      }
+
+      return null;
+    },
+
+    /*
+     * Extract a string out of the next sequence of characters and/or
+     * lines or return 'null' if its not possible. Since strings can
+     * span across multiple lines this method has to move the char
+     * pointer.
+     *
+     * This method recognizes pseudo-multiline JavaScript strings:
+     *
+     *   var str = "hello\
+     *   world";
+     */
+    scanStringLiteral: function () {
+      /*jshint loopfunc:true */
+      var quote = this.peek();
+
+      // String must start with a quote.
+      if (quote !== "\"" && quote !== "'") {
+        return null;
+      }
+
+      var value = "";
+
+      this.skip();
+
+      while (this.peek() !== quote) {
+        if (this.peek() === "") { // End Of Line
+          return {
+            type: 'string',
+            value: value,
+            isUnclosed: true,
+            quote: quote,
+            pos: this.char
+          };
+        }
+
+        var char = this.peek();
+        var jump = 1; // A length of a jump, after we're done
+        // parsing this character.
+
+        value += char;
+        this.skip(jump);
+      }
+
+      this.skip();
+      return {
+        type: 'string',
+        value: value,
+        isUnclosed: false,
+        quote: quote,
+        pos: this.char
+      };
+    },
+
+  };
+

+ 0 - 33
public/app/plugins/datasource/graphite/module.js

@@ -1,33 +0,0 @@
-define([
-  './datasource',
-],
-function (GraphiteDatasource) {
-  'use strict';
-
-  function metricsQueryEditor() {
-    return {
-      controller: 'GraphiteQueryCtrl',
-      templateUrl: 'public/app/plugins/datasource/graphite/partials/query.editor.html'
-    };
-  }
-
-  function metricsQueryOptions() {
-    return {templateUrl: 'public/app/plugins/datasource/graphite/partials/query.options.html'};
-  }
-
-  function annotationsQueryEditor() {
-    return {templateUrl: 'public/app/plugins/datasource/graphite/partials/annotations.editor.html'};
-  }
-
-  function configView() {
-    return {templateUrl: 'public/app/plugins/datasource/graphite/partials/config.html'};
-  }
-
-  return {
-    Datasource: GraphiteDatasource,
-    configView: configView,
-    annotationsQueryEditor: annotationsQueryEditor,
-    metricsQueryEditor: metricsQueryEditor,
-    metricsQueryOptions: metricsQueryOptions,
-  };
-});

+ 23 - 0
public/app/plugins/datasource/graphite/module.ts

@@ -0,0 +1,23 @@
+import {GraphiteDatasource} from './datasource';
+import {GraphiteQueryCtrl} from './query_ctrl';
+
+class GraphiteConfigCtrl {
+  static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
+}
+
+class GraphiteQueryOptionsCtrl {
+  static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.options.html';
+}
+
+class AnnotationsQueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/graphite/partials/annotations.editor.html';
+}
+
+export {
+  GraphiteDatasource as Datasource,
+  GraphiteQueryCtrl as QueryCtrl,
+  GraphiteConfigCtrl as ConfigCtrl,
+  GraphiteQueryOptionsCtrl as QueryOptionsCtrl,
+  AnnotationsQueryCtrl as AnnotationsQueryCtrl,
+};
+

+ 0 - 265
public/app/plugins/datasource/graphite/parser.js

@@ -1,265 +0,0 @@
-define([
-  './lexer'
-], function (Lexer) {
-  'use strict';
-
-  function Parser(expression) {
-    this.expression = expression;
-    this.lexer = new Lexer(expression);
-    this.tokens = this.lexer.tokenize();
-    this.index = 0;
-  }
-
-  Parser.prototype = {
-
-    getAst: function () {
-      return this.start();
-    },
-
-    start: function () {
-      try {
-        return this.functionCall() || this.metricExpression();
-      }
-      catch (e) {
-        return {
-          type: 'error',
-          message: e.message,
-          pos: e.pos
-        };
-      }
-    },
-
-    curlyBraceSegment: function() {
-      if (this.match('identifier', '{') || this.match('{')) {
-
-        var curlySegment = "";
-
-        while (!this.match('') && !this.match('}')) {
-          curlySegment += this.consumeToken().value;
-        }
-
-        if (!this.match('}')) {
-          this.errorMark("Expected closing '}'");
-        }
-
-        curlySegment += this.consumeToken().value;
-
-        // if curly segment is directly followed by identifier
-        // include it in the segment
-        if (this.match('identifier')) {
-          curlySegment += this.consumeToken().value;
-        }
-
-        return {
-          type: 'segment',
-          value: curlySegment
-        };
-      }
-      else {
-        return null;
-      }
-    },
-
-    metricSegment: function() {
-      var curly = this.curlyBraceSegment();
-      if (curly) {
-        return curly;
-      }
-
-      if (this.match('identifier') || this.match('number')) {
-        // hack to handle float numbers in metric segments
-        var parts = this.consumeToken().value.split('.');
-        if (parts.length === 2) {
-          this.tokens.splice(this.index, 0, { type: '.' });
-          this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
-        }
-
-        return {
-          type: 'segment',
-          value: parts[0]
-        };
-      }
-
-      if (!this.match('templateStart')) {
-        this.errorMark('Expected metric identifier');
-      }
-
-      this.consumeToken();
-
-      if (!this.match('identifier')) {
-        this.errorMark('Expected identifier after templateStart');
-      }
-
-      var node = {
-        type: 'template',
-        value: this.consumeToken().value
-      };
-
-      if (!this.match('templateEnd')) {
-        this.errorMark('Expected templateEnd');
-      }
-
-      this.consumeToken();
-      return node;
-    },
-
-    metricExpression: function() {
-      if (!this.match('templateStart') &&
-          !this.match('identifier') &&
-          !this.match('number') &&
-          !this.match('{')) {
-        return null;
-      }
-
-      var node = {
-        type: 'metric',
-        segments: []
-      };
-
-      node.segments.push(this.metricSegment());
-
-      while (this.match('.')) {
-        this.consumeToken();
-
-        var segment = this.metricSegment();
-        if (!segment) {
-          this.errorMark('Expected metric identifier');
-        }
-
-        node.segments.push(segment);
-      }
-
-      return node;
-    },
-
-    functionCall: function() {
-      if (!this.match('identifier', '(')) {
-        return null;
-      }
-
-      var node = {
-        type: 'function',
-        name: this.consumeToken().value,
-      };
-
-      // consume left parenthesis
-      this.consumeToken();
-
-      node.params = this.functionParameters();
-
-      if (!this.match(')')) {
-        this.errorMark('Expected closing parenthesis');
-      }
-
-      this.consumeToken();
-
-      return node;
-    },
-
-    boolExpression: function() {
-      if (!this.match('bool')) {
-        return null;
-      }
-
-      return {
-        type: 'bool',
-        value: this.consumeToken().value === 'true',
-      };
-    },
-
-    functionParameters: function () {
-      if (this.match(')') || this.match('')) {
-        return [];
-      }
-
-      var param =
-        this.functionCall() ||
-        this.numericLiteral() ||
-        this.seriesRefExpression() ||
-        this.boolExpression() ||
-        this.metricExpression() ||
-        this.stringLiteral();
-
-      if (!this.match(',')) {
-        return [param];
-      }
-
-      this.consumeToken();
-      return [param].concat(this.functionParameters());
-    },
-
-    seriesRefExpression: function() {
-      if (!this.match('identifier')) {
-        return null;
-      }
-
-      var value = this.tokens[this.index].value;
-      if (!value.match(/\#[A-Z]/)) {
-        return null;
-      }
-
-      var token = this.consumeToken();
-
-      return {
-        type: 'series-ref',
-        value: token.value
-      };
-    },
-
-    numericLiteral: function () {
-      if (!this.match('number')) {
-        return null;
-      }
-
-      return {
-        type: 'number',
-        value: parseFloat(this.consumeToken().value)
-      };
-    },
-
-    stringLiteral: function () {
-      if (!this.match('string')) {
-        return null;
-      }
-
-      var token = this.consumeToken();
-      if (token.isUnclosed) {
-        throw { message: 'Unclosed string parameter', pos: token.pos };
-      }
-
-      return {
-        type: 'string',
-        value: token.value
-      };
-    },
-
-    errorMark: function(text) {
-      var currentToken = this.tokens[this.index];
-      var type = currentToken ? currentToken.type : 'end of string';
-      throw {
-        message: text + " instead found " + type,
-        pos: currentToken ? currentToken.pos : this.lexer.char
-      };
-    },
-
-    // returns token value and incre
-    consumeToken: function() {
-      this.index++;
-      return this.tokens[this.index - 1];
-    },
-
-    matchToken: function(type, index) {
-      var token = this.tokens[this.index + index];
-      return (token === undefined && type === '') ||
-             token && token.type === type;
-    },
-
-    match: function(token1, token2) {
-      return this.matchToken(token1, 0) &&
-        (!token2 || this.matchToken(token2, 1));
-    },
-
-  };
-
-  return Parser;
-});

+ 258 - 0
public/app/plugins/datasource/graphite/parser.ts

@@ -0,0 +1,258 @@
+
+import {Lexer} from './lexer';
+
+export function Parser(expression) {
+  this.expression = expression;
+  this.lexer = new Lexer(expression);
+  this.tokens = this.lexer.tokenize();
+  this.index = 0;
+}
+
+Parser.prototype = {
+
+  getAst: function () {
+    return this.start();
+  },
+
+  start: function () {
+    try {
+      return this.functionCall() || this.metricExpression();
+    } catch (e) {
+      return {
+        type: 'error',
+        message: e.message,
+        pos: e.pos
+      };
+    }
+  },
+
+  curlyBraceSegment: function() {
+    if (this.match('identifier', '{') || this.match('{')) {
+
+      var curlySegment = "";
+
+      while (!this.match('') && !this.match('}')) {
+        curlySegment += this.consumeToken().value;
+      }
+
+      if (!this.match('}')) {
+        this.errorMark("Expected closing '}'");
+      }
+
+      curlySegment += this.consumeToken().value;
+
+      // if curly segment is directly followed by identifier
+      // include it in the segment
+      if (this.match('identifier')) {
+        curlySegment += this.consumeToken().value;
+      }
+
+      return {
+        type: 'segment',
+        value: curlySegment
+      };
+    } else {
+      return null;
+    }
+  },
+
+  metricSegment: function() {
+    var curly = this.curlyBraceSegment();
+    if (curly) {
+      return curly;
+    }
+
+    if (this.match('identifier') || this.match('number')) {
+      // hack to handle float numbers in metric segments
+      var parts = this.consumeToken().value.split('.');
+      if (parts.length === 2) {
+        this.tokens.splice(this.index, 0, { type: '.' });
+        this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
+      }
+
+      return {
+        type: 'segment',
+        value: parts[0]
+      };
+    }
+
+    if (!this.match('templateStart')) {
+      this.errorMark('Expected metric identifier');
+    }
+
+    this.consumeToken();
+
+    if (!this.match('identifier')) {
+      this.errorMark('Expected identifier after templateStart');
+    }
+
+    var node = {
+      type: 'template',
+      value: this.consumeToken().value
+    };
+
+    if (!this.match('templateEnd')) {
+      this.errorMark('Expected templateEnd');
+    }
+
+    this.consumeToken();
+    return node;
+  },
+
+  metricExpression: function() {
+    if (!this.match('templateStart') &&
+        !this.match('identifier') &&
+          !this.match('number') &&
+            !this.match('{')) {
+      return null;
+    }
+
+    var node = {
+      type: 'metric',
+      segments: []
+    };
+
+    node.segments.push(this.metricSegment());
+
+    while (this.match('.')) {
+      this.consumeToken();
+
+      var segment = this.metricSegment();
+      if (!segment) {
+        this.errorMark('Expected metric identifier');
+      }
+
+      node.segments.push(segment);
+    }
+
+    return node;
+  },
+
+  functionCall: function() {
+    if (!this.match('identifier', '(')) {
+      return null;
+    }
+
+    var node: any = {
+      type: 'function',
+      name: this.consumeToken().value,
+    };
+
+    // consume left parenthesis
+    this.consumeToken();
+
+    node.params = this.functionParameters();
+
+    if (!this.match(')')) {
+      this.errorMark('Expected closing parenthesis');
+    }
+
+    this.consumeToken();
+
+    return node;
+  },
+
+  boolExpression: function() {
+    if (!this.match('bool')) {
+      return null;
+    }
+
+    return {
+      type: 'bool',
+      value: this.consumeToken().value === 'true',
+    };
+  },
+
+  functionParameters: function () {
+    if (this.match(')') || this.match('')) {
+      return [];
+    }
+
+    var param =
+      this.functionCall() ||
+      this.numericLiteral() ||
+      this.seriesRefExpression() ||
+      this.boolExpression() ||
+      this.metricExpression() ||
+      this.stringLiteral();
+
+    if (!this.match(',')) {
+      return [param];
+    }
+
+    this.consumeToken();
+    return [param].concat(this.functionParameters());
+  },
+
+  seriesRefExpression: function() {
+    if (!this.match('identifier')) {
+      return null;
+    }
+
+    var value = this.tokens[this.index].value;
+    if (!value.match(/\#[A-Z]/)) {
+      return null;
+    }
+
+    var token = this.consumeToken();
+
+    return {
+      type: 'series-ref',
+      value: token.value
+    };
+  },
+
+  numericLiteral: function () {
+    if (!this.match('number')) {
+      return null;
+    }
+
+    return {
+      type: 'number',
+      value: parseFloat(this.consumeToken().value)
+    };
+  },
+
+  stringLiteral: function () {
+    if (!this.match('string')) {
+      return null;
+    }
+
+    var token = this.consumeToken();
+    if (token.isUnclosed) {
+      throw { message: 'Unclosed string parameter', pos: token.pos };
+    }
+
+    return {
+      type: 'string',
+      value: token.value
+    };
+  },
+
+  errorMark: function(text) {
+    var currentToken = this.tokens[this.index];
+    var type = currentToken ? currentToken.type : 'end of string';
+    throw {
+      message: text + " instead found " + type,
+      pos: currentToken ? currentToken.pos : this.lexer.char
+    };
+  },
+
+  // returns token value and incre
+  consumeToken: function() {
+    this.index++;
+    return this.tokens[this.index - 1];
+  },
+
+  matchToken: function(type, index) {
+    var token = this.tokens[this.index + index];
+    return (token === undefined && type === '') ||
+      token && token.type === type;
+  },
+
+  match: function(token1, token2) {
+    return this.matchToken(token1, 0) &&
+      (!token2 || this.matchToken(token2, 1));
+  },
+};
+

+ 2 - 2
public/app/plugins/datasource/graphite/partials/annotations.editor.html

@@ -1,14 +1,14 @@
 <div class="editor-row">
 	<div class="editor-option">
 		<label class="small">Graphite target expression</label>
-		<input type="text" class="span10" ng-model='annotation.target' placeholder=""></input>
+		<input type="text" class="span10" ng-model='ctrl.annotation.target' placeholder=""></input>
 	</div>
 </div>
 
 <div class="editor-row">
 	<div class="editor-option">
 		<label class="small">Graphite event tags</label>
-		<input type="text" ng-model='annotation.tags' placeholder=""></input>
+		<input type="text" ng-model='ctrl.annotation.tags' placeholder=""></input>
 	</div>
 </div>
 

+ 2 - 1
public/app/plugins/datasource/graphite/partials/config.html

@@ -1,2 +1,3 @@
-<datasource-http-settings></datasource-http-settings>
+<datasource-http-settings current="ctrl.current">
+</datasource-http-settings>
 

+ 18 - 70
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -1,73 +1,21 @@
-<div class="tight-form">
-	<ul class="tight-form-list pull-right">
-		<li ng-show="parserError" class="tight-form-item">
-			<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
-				<i class="fa fa-warning"></i>
-			</a>
-		</li>
-		<li class="tight-form-item small" ng-show="target.datasource">
-			<em>{{target.datasource}}</em>
-		</li>
-		<li class="tight-form-item">
-			<a class="pointer" tabindex="1" ng-click="toggleEditorMode()">
-				<i class="fa fa-pencil"></i>
-			</a>
-		</li>
-		<li class="tight-form-item">
-			<div class="dropdown">
-				<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
-					<i class="fa fa-bars"></i>
-				</a>
-				<ul class="dropdown-menu pull-right" role="menu">
-					<li role="menuitem">
-						<a tabindex="1" ng-click="toggleEditorMode()">
-							Switch editor mode
-						</a>
-					</li>
-					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a>
-					</li>
-					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a>
-					</li>
-					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a>
-					</li>
-				</ul>
-			</div>
-		</li>
-		<li class="tight-form-item last">
-			<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
-				<i class="fa fa-remove"></i>
-			</a>
-		</li>
-	</ul>
+<query-editor-row ctrl="ctrl">
 
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="min-width: 15px; text-align: center">
-			{{target.refId}}
-		</li>
-		<li>
-			<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
-				<i class="fa fa-eye"></i>
-			</a>
-		</li>
-	</ul>
+	<li class="tight-form-flex-wrapper" ng-show="ctrl.target.textEditor">
+		<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="ctrl.target.target" give-focus="ctrl.target.textEditor" spellcheck='false' ng-model-onblur ng-change="ctrl.targetTextChanged()"></input>
+	</li>
 
-	<span style="display: block; overflow: hidden;">
-		<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="target.target" give-focus="target.textEditor" spellcheck='false' ng-model-onblur ng-change="panelCtrl.getData()" ng-show="target.textEditor"></input>
-	</span>
+	<li ng-hide-start="ctrl.target.textEditor"></li>
 
-	<ul class="tight-form-list" role="menu" ng-hide="target.textEditor">
-		<li ng-repeat="segment in segments" role="menuitem">
-			<metric-segment segment="segment" get-options="getAltSegments($index)" on-change="segmentValueChanged(segment, $index)"></metric-segment>
-		</li>
-		<li ng-repeat="func in functions">
-			<span graphite-func-editor class="tight-form-item tight-form-func">
-			</span>
-		</li>
-		<li class="dropdown" graphite-add-func>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
+	<li ng-repeat="segment in ctrl.segments" role="menuitem">
+		<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
+	</li>
+	<li ng-repeat="func in ctrl.functions">
+		<span graphite-func-editor class="tight-form-item tight-form-func">
+		</span>
+	</li>
+	<li class="dropdown" graphite-add-func>
+	</li>
+
+	<li ng-hide-end></li>
+
+</query-editor-row>

+ 13 - 14
public/app/plugins/datasource/graphite/partials/query.options.html

@@ -1,5 +1,4 @@
 <section class="grafana-metric-options">
-
 	<div class="tight-form">
 		<ul class="tight-form-list">
 			<li class="tight-form-item tight-form-item-icon">
@@ -11,7 +10,7 @@
 			<li>
 				<input type="text"
 					class="input-mini tight-form-input"
-					ng-model="ctrl.panel.cacheTimeout"
+					ng-model="ctrl.panelCtrl.panel.cacheTimeout"
 					bs-tooltip="'Graphite parameter to override memcache default timeout (unit is seconds)'"
 					data-placement="right"
 					spellcheck='false'
@@ -23,10 +22,10 @@
 			<li>
 				<input type="text"
 					class="input-mini tight-form-input"
-					ng-model="ctrl.panel.maxDataPoints"
+					ng-model="ctrl.panelCtrl.panel.maxDataPoints"
 					bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
 					data-placement="right"
-					ng-model-onblur ng-change="ctrl.refresh()"
+					ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
 					spellcheck='false'
 					placeholder="auto"></input>
 			</li>
@@ -39,27 +38,27 @@
 				<i class="fa fa-info-circle"></i>
 			</li>
 			<li class="tight-form-item">
-				<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
 					shorter legend names
 				</a>
 			</li>
 			<li class="tight-form-item">
-				<a ng-click="ctrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
 					series as parameters
 				</a>
 			</li>
 			<li class="tight-form-item">
-				<a ng-click="ctrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
 					stacking
 				</a>
 			</li>
 			<li class="tight-form-item">
-				<a ng-click="ctrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
 					templating
 				</a>
 			</li>
 			<li class="tight-form-item">
-				<a ng-click="ctrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
 					max data points
 				</a>
 			</li>
@@ -71,7 +70,7 @@
 <div class="editor-row">
 	<div class="pull-left" style="margin-top: 30px;">
 
-		<div class="grafana-info-box span8" ng-if="ctrl.editorHelpIndex === 1">
+		<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
 			<h5>Shorter legend names</h5>
 			<ul>
 				<li>alias() function to specify a custom series name</li>
@@ -81,7 +80,7 @@
 			</ul>
 		</div>
 
-		<div class="grafana-info-box span8" ng-if="ctrl.editorHelpIndex === 2">
+		<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
 			<h5>Series as parameter</h5>
 			<ul>
 				<li>Some graphite functions allow you to have many series arguments</li>
@@ -99,7 +98,7 @@
 			</ul>
 		</div>
 
-		<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 3">
+		<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
 			<h5>Stacking</h5>
 			<ul>
 				<li>You find the stacking option under Display Styles tab</li>
@@ -107,7 +106,7 @@
 			</ul>
 		</div>
 
-		<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 4">
+		<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 4">
 			<h5>Templating</h5>
 			<ul>
 				<li>You can use a template variable in place of metric names</li>
@@ -116,7 +115,7 @@
 			</ul>
 		</div>
 
-		<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 5">
+		<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 5">
 			<h5>Max data points</h5>
 			<ul>
 				<li>Every graphite request is issued with a maxDataPoints parameter</li>

+ 0 - 292
public/app/plugins/datasource/graphite/query_ctrl.js

@@ -1,292 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/config',
-  './gfunc',
-  './parser'
-],
-function (angular, _, config, gfunc, Parser) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('GraphiteQueryCtrl', function($scope, uiSegmentSrv, templateSrv) {
-    var panelCtrl = $scope.panelCtrl = $scope.ctrl;
-    var datasource = $scope.datasource;
-
-    $scope.init = function() {
-      if ($scope.target) {
-        $scope.target.target = $scope.target.target || '';
-        parseTarget();
-      }
-    };
-
-    $scope.toggleEditorMode = function() {
-      $scope.target.textEditor = !$scope.target.textEditor;
-      parseTarget();
-    };
-
-    // The way parsing and the target editor works needs
-    // to be rewritten to handle functions that take multiple series
-    function parseTarget() {
-      $scope.functions = [];
-      $scope.segments = [];
-      delete $scope.parserError;
-
-      if ($scope.target.textEditor) {
-        return;
-      }
-
-      var parser = new Parser($scope.target.target);
-      var astNode = parser.getAst();
-      if (astNode === null) {
-        checkOtherSegments(0);
-        return;
-      }
-
-      if (astNode.type === 'error') {
-        $scope.parserError = astNode.message + " at position: " + astNode.pos;
-        $scope.target.textEditor = true;
-        return;
-      }
-
-      try {
-        parseTargeRecursive(astNode);
-      }
-      catch (err) {
-        console.log('error parsing target:', err.message);
-        $scope.parserError = err.message;
-        $scope.target.textEditor = true;
-      }
-
-      checkOtherSegments($scope.segments.length - 1);
-    }
-
-    function addFunctionParameter(func, value, index, shiftBack) {
-      if (shiftBack) {
-        index = Math.max(index - 1, 0);
-      }
-      func.params[index] = value;
-    }
-
-    function parseTargeRecursive(astNode, func, index) {
-      if (astNode === null) {
-        return null;
-      }
-
-      switch(astNode.type) {
-      case 'function':
-        var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
-
-        _.each(astNode.params, function(param, index) {
-          parseTargeRecursive(param, innerFunc, index);
-        });
-
-        innerFunc.updateText();
-        $scope.functions.push(innerFunc);
-        break;
-
-      case 'series-ref':
-        addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
-        break;
-      case 'bool':
-      case 'string':
-      case 'number':
-        if ((index-1) >= func.def.params.length) {
-          throw { message: 'invalid number of parameters to method ' + func.def.name };
-        }
-        addFunctionParameter(func, astNode.value, index, true);
-        break;
-      case 'metric':
-        if ($scope.segments.length > 0) {
-          if (astNode.segments.length !== 1) {
-            throw { message: 'Multiple metric params not supported, use text editor.' };
-          }
-          addFunctionParameter(func, astNode.segments[0].value, index, true);
-          break;
-        }
-
-        $scope.segments = _.map(astNode.segments, function(segment) {
-          return uiSegmentSrv.newSegment(segment);
-        });
-      }
-    }
-
-    function getSegmentPathUpTo(index) {
-      var arr = $scope.segments.slice(0, index);
-
-      return _.reduce(arr, function(result, segment) {
-        return result ? (result + "." + segment.value) : segment.value;
-      }, "");
-    }
-
-    function checkOtherSegments(fromIndex) {
-      if (fromIndex === 0) {
-        $scope.segments.push(uiSegmentSrv.newSelectMetric());
-        return;
-      }
-
-      var path = getSegmentPathUpTo(fromIndex + 1);
-      return datasource.metricFindQuery(path)
-        .then(function(segments) {
-          if (segments.length === 0) {
-            if (path !== '') {
-              $scope.segments = $scope.segments.splice(0, fromIndex);
-              $scope.segments.push(uiSegmentSrv.newSelectMetric());
-            }
-          } else if (segments[0].expandable) {
-            if ($scope.segments.length === fromIndex) {
-              $scope.segments.push(uiSegmentSrv.newSelectMetric());
-            }
-            else {
-              return checkOtherSegments(fromIndex + 1);
-            }
-          }
-        })
-        .then(null, function(err) {
-          $scope.parserError = err.message || 'Failed to issue metric query';
-        });
-    }
-
-    function setSegmentFocus(segmentIndex) {
-      _.each($scope.segments, function(segment, index) {
-        segment.focus = segmentIndex === index;
-      });
-    }
-
-    function wrapFunction(target, func) {
-      return func.render(target);
-    }
-
-    $scope.getAltSegments = function (index) {
-      var query = index === 0 ?  '*' : getSegmentPathUpTo(index) + '.*';
-
-      return datasource.metricFindQuery(query).then(function(segments) {
-        var altSegments = _.map(segments, function(segment) {
-          return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
-        });
-
-        if (altSegments.length === 0) { return altSegments; }
-
-        // add template variables
-        _.each(templateSrv.variables, function(variable) {
-          altSegments.unshift(uiSegmentSrv.newSegment({
-            type: 'template',
-            value: '$' + variable.name,
-            expandable: true,
-          }));
-        });
-
-        // add wildcard option
-        altSegments.unshift(uiSegmentSrv.newSegment('*'));
-        return altSegments;
-      })
-      .then(null, function(err) {
-        $scope.parserError = err.message || 'Failed to issue metric query';
-        return [];
-      });
-    };
-
-    $scope.segmentValueChanged = function (segment, segmentIndex) {
-      delete $scope.parserError;
-
-      if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
-        $scope.functions = [];
-      }
-
-      if (segment.expandable) {
-        return checkOtherSegments(segmentIndex + 1).then(function() {
-          setSegmentFocus(segmentIndex + 1);
-          $scope.targetChanged();
-        });
-      }
-      else {
-        $scope.segments = $scope.segments.splice(0, segmentIndex + 1);
-      }
-
-      setSegmentFocus(segmentIndex + 1);
-      $scope.targetChanged();
-    };
-
-    $scope.targetTextChanged = function() {
-      parseTarget();
-      panelCtrl.refresh();
-    };
-
-    $scope.targetChanged = function() {
-      if ($scope.parserError) {
-        return;
-      }
-
-      var oldTarget = $scope.target.target;
-      var target = getSegmentPathUpTo($scope.segments.length);
-      $scope.target.target = _.reduce($scope.functions, wrapFunction, target);
-
-      if ($scope.target.target !== oldTarget) {
-        if ($scope.segments[$scope.segments.length - 1].value !== 'select metric') {
-          panelCtrl.refresh();
-        }
-      }
-    };
-
-    $scope.removeFunction = function(func) {
-      $scope.functions = _.without($scope.functions, func);
-      $scope.targetChanged();
-    };
-
-    $scope.addFunction = function(funcDef) {
-      var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
-      newFunc.added = true;
-      $scope.functions.push(newFunc);
-
-      $scope.moveAliasFuncLast();
-      $scope.smartlyHandleNewAliasByNode(newFunc);
-
-      if ($scope.segments.length === 1 && $scope.segments[0].fake) {
-        $scope.segments = [];
-      }
-
-      if (!newFunc.params.length && newFunc.added) {
-        $scope.targetChanged();
-      }
-    };
-
-    $scope.moveAliasFuncLast = function() {
-      var aliasFunc = _.find($scope.functions, function(func) {
-        return func.def.name === 'alias' ||
-          func.def.name === 'aliasByNode' ||
-          func.def.name === 'aliasByMetric';
-      });
-
-      if (aliasFunc) {
-        $scope.functions = _.without($scope.functions, aliasFunc);
-        $scope.functions.push(aliasFunc);
-      }
-    };
-
-    $scope.smartlyHandleNewAliasByNode = function(func) {
-      if (func.def.name !== 'aliasByNode') {
-        return;
-      }
-      for(var i = 0; i < $scope.segments.length; i++) {
-        if ($scope.segments[i].value.indexOf('*') >= 0)  {
-          func.params[0] = i;
-          func.added = false;
-          $scope.targetChanged();
-          return;
-        }
-      }
-    };
-
-    $scope.toggleMetricOptions = function() {
-      $scope.panel.metricOptionsEnabled = !$scope.panel.metricOptionsEnabled;
-      if (!$scope.panel.metricOptionsEnabled) {
-        delete $scope.panel.cacheTimeout;
-      }
-    };
-
-    $scope.init();
-
-  });
-
-});

+ 276 - 0
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -0,0 +1,276 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import './add_graphite_func';
+import './func_editor';
+
+import angular from 'angular';
+import _ from 'lodash';
+import moment from 'moment';
+import gfunc from './gfunc';
+import {Parser} from './parser';
+import {QueryCtrl} from 'app/features/panel/panel';
+
+export class GraphiteQueryCtrl extends QueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.editor.html';
+
+  functions: any[];
+  segments: any[];
+
+  /** @ngInject **/
+  constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
+    super($scope, $injector);
+
+    if (this.target) {
+      this.target.target = this.target.target || '';
+      this.parseTarget();
+    }
+  }
+
+  toggleEditorMode() {
+    this.target.textEditor = !this.target.textEditor;
+    this.parseTarget();
+  }
+
+  parseTarget() {
+    this.functions = [];
+    this.segments = [];
+    this.error = null;
+
+    if (this.target.textEditor) {
+      return;
+    }
+
+    var parser = new Parser(this.target.target);
+    var astNode = parser.getAst();
+    if (astNode === null) {
+      this.checkOtherSegments(0);
+      return;
+    }
+
+    if (astNode.type === 'error') {
+      this.error = astNode.message + " at position: " + astNode.pos;
+      this.target.textEditor = true;
+      return;
+    }
+
+    try {
+      this.parseTargeRecursive(astNode, null, 0);
+    } catch (err) {
+      console.log('error parsing target:', err.message);
+      this.error = err.message;
+      this.target.textEditor = true;
+    }
+
+    this.checkOtherSegments(this.segments.length - 1);
+  }
+
+  addFunctionParameter(func, value, index, shiftBack) {
+    if (shiftBack) {
+      index = Math.max(index - 1, 0);
+    }
+    func.params[index] = value;
+  }
+
+  parseTargeRecursive(astNode, func, index) {
+    if (astNode === null) {
+      return null;
+    }
+
+    switch (astNode.type) {
+      case 'function':
+        var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
+        _.each(astNode.params, (param, index) => {
+          this.parseTargeRecursive(param, innerFunc, index);
+        });
+
+        innerFunc.updateText();
+        this.functions.push(innerFunc);
+        break;
+      case 'series-ref':
+        this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
+        break;
+      case 'bool':
+      case 'string':
+      case 'number':
+        if ((index-1) >= func.def.params.length) {
+          throw { message: 'invalid number of parameters to method ' + func.def.name };
+        }
+        this.addFunctionParameter(func, astNode.value, index, true);
+      break;
+      case 'metric':
+        if (this.segments.length > 0) {
+        if (astNode.segments.length !== 1) {
+          throw { message: 'Multiple metric params not supported, use text editor.' };
+        }
+        this.addFunctionParameter(func, astNode.segments[0].value, index, true);
+        break;
+      }
+
+      this.segments = _.map(astNode.segments, segment => {
+        return this.uiSegmentSrv.newSegment(segment);
+      });
+    }
+  }
+
+  getSegmentPathUpTo(index) {
+    var arr = this.segments.slice(0, index);
+
+    return _.reduce(arr, function(result, segment) {
+      return result ? (result + "." + segment.value) : segment.value;
+    }, "");
+  }
+
+  checkOtherSegments(fromIndex) {
+    if (fromIndex === 0) {
+      this.segments.push(this.uiSegmentSrv.newSelectMetric());
+      return;
+    }
+
+    var path = this.getSegmentPathUpTo(fromIndex + 1);
+    return this.datasource.metricFindQuery(path).then(segments => {
+      if (segments.length === 0) {
+        if (path !== '') {
+          this.segments = this.segments.splice(0, fromIndex);
+          this.segments.push(this.uiSegmentSrv.newSelectMetric());
+        }
+      } else if (segments[0].expandable) {
+        if (this.segments.length === fromIndex) {
+          this.segments.push(this.uiSegmentSrv.newSelectMetric());
+        } else {
+          return this.checkOtherSegments(fromIndex + 1);
+        }
+      }
+    }).catch(err => {
+      this.error = err.message || 'Failed to issue metric query';
+    });
+  }
+
+  setSegmentFocus(segmentIndex) {
+    _.each(this.segments, (segment, index) => {
+      segment.focus = segmentIndex === index;
+    });
+  }
+
+  wrapFunction(target, func) {
+    return func.render(target);
+  }
+
+  getAltSegments(index) {
+    var query = index === 0 ?  '*' : this.getSegmentPathUpTo(index) + '.*';
+
+    return this.datasource.metricFindQuery(query).then(segments => {
+      var altSegments = _.map(segments, segment => {
+        return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
+      });
+
+      if (altSegments.length === 0) { return altSegments; }
+
+      // add template variables
+      _.each(this.templateSrv.variables, variable => {
+        altSegments.unshift(this.uiSegmentSrv.newSegment({
+          type: 'template',
+          value: '$' + variable.name,
+          expandable: true,
+        }));
+      });
+
+      // add wildcard option
+      altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
+      return altSegments;
+    }).catch(err => {
+      this.error = err.message || 'Failed to issue metric query';
+      return [];
+    });
+  }
+
+  segmentValueChanged(segment, segmentIndex) {
+    this.error = null;
+
+    if (this.functions.length > 0 && this.functions[0].def.fake) {
+      this.functions = [];
+    }
+
+    if (segment.expandable) {
+      return this.checkOtherSegments(segmentIndex + 1).then(() => {
+        this.setSegmentFocus(segmentIndex + 1);
+        this.targetChanged();
+      });
+    } else {
+      this.segments = this.segments.splice(0, segmentIndex + 1);
+    }
+
+    this.setSegmentFocus(segmentIndex + 1);
+    this.targetChanged();
+  }
+
+  targetTextChanged() {
+    this.parseTarget();
+    this.panelCtrl.refresh();
+  }
+
+  targetChanged() {
+    if (this.error) {
+      return;
+    }
+
+    var oldTarget = this.target.target;
+    var target = this.getSegmentPathUpTo(this.segments.length);
+    this.target.target = _.reduce(this.functions, this.wrapFunction, target);
+
+    if (this.target.target !== oldTarget) {
+      if (this.segments[this.segments.length - 1].value !== 'select metric') {
+        this.panelCtrl.refresh();
+      }
+    }
+  }
+
+  removeFunction(func) {
+    this.functions = _.without(this.functions, func);
+    this.targetChanged();
+  }
+
+  addFunction(funcDef) {
+    var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
+    newFunc.added = true;
+    this.functions.push(newFunc);
+
+    this.moveAliasFuncLast();
+    this.smartlyHandleNewAliasByNode(newFunc);
+
+    if (this.segments.length === 1 && this.segments[0].fake) {
+      this.segments = [];
+    }
+
+    if (!newFunc.params.length && newFunc.added) {
+      this.targetChanged();
+    }
+  }
+
+  moveAliasFuncLast() {
+    var aliasFunc = _.find(this.functions, function(func) {
+      return func.def.name === 'alias' ||
+        func.def.name === 'aliasByNode' ||
+        func.def.name === 'aliasByMetric';
+    });
+
+    if (aliasFunc) {
+      this.functions = _.without(this.functions, aliasFunc);
+      this.functions.push(aliasFunc);
+    }
+  }
+
+  smartlyHandleNewAliasByNode(func) {
+    if (func.def.name !== 'aliasByNode') {
+      return;
+    }
+
+    for (var i = 0; i < this.segments.length; i++) {
+      if (this.segments[i].value.indexOf('*') >= 0)  {
+        func.params[0] = i;
+        func.added = false;
+        this.targetChanged();
+        return;
+      }
+    }
+  }
+}

+ 2 - 2
public/app/plugins/datasource/graphite/specs/datasource_specs.ts

@@ -1,7 +1,7 @@
 
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import helpers from 'test/specs/helpers';
-import Datasource from "../datasource";
+import {GraphiteDatasource} from "../datasource";
 
 describe('graphiteDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
@@ -18,7 +18,7 @@ describe('graphiteDatasource', function() {
   }));
 
   beforeEach(function() {
-    ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+    ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
   });
 
   describe('When querying influxdb with one target using query editor target spec', function() {

Некоторые файлы не были показаны из-за большого количества измененных файлов