diff --git a/CHANGELOG.md b/CHANGELOG.md index 225e1db96ac..978a531bcf5 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index 9deda2ee97e..b654554fa48 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/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. diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md new file mode 100644 index 00000000000..70431565741 --- /dev/null +++ b/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 `. + +## 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"} diff --git a/docs/sources/reference/table_panel.md b/docs/sources/reference/table_panel.md index 65437f85bd7..a857be38357 100644 --- a/docs/sources/reference/table_panel.md +++ b/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. - + 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 - + 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 diff --git a/package.json b/package.json index 03c5ab52bb4..7a358ce3e04 100644 --- a/package.json +++ b/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": { diff --git a/pkg/api/api.go b/pkg/api/api.go index 99ab8a51ff2..6e7015ed889 100644 --- a/pkg/api/api.go +++ b/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)) diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 7f8b30bf312..7a98386347c 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/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 +} diff --git a/pkg/api/cloudwatch/metrics_test.go b/pkg/api/cloudwatch/metrics_test.go new file mode 100644 index 00000000000..ec39452e116 --- /dev/null +++ b/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") + }) + }) + +} diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 79485fa8cca..c0c7cd0e4f5 100644 --- a/pkg/api/dashboard.go +++ b/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() diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index f2aee67205c..8ed848a7b11 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/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) +} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index a5e8e74fb46..83176e836e5 100644 --- a/pkg/api/dtos/models.go +++ b/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 { diff --git a/pkg/api/index.go b/pkg/api/index.go index d3bdff12b09..342de21c670 100644 --- a/pkg/api/index.go +++ b/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", diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index cc563646833..9bfbd06c1ef 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/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 +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 63ed1f5c006..1015dfbe28c 100644 --- a/pkg/models/dashboards.go +++ b/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 } diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index dc8862f2d57..fc94a91cce5 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/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 +} diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index b94f1a7d6ac..a4a8629b331 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/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, + })) } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 0b120dd85b7..55c0f4ec049 100644 --- a/public/app/core/core.ts +++ b/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'; diff --git a/public/app/core/directives/config_modal.js b/public/app/core/directives/config_modal.js deleted file mode 100644 index e37d6797fc4..00000000000 --- a/public/app/core/directives/config_modal.js +++ /dev/null @@ -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(); - }); - } - }; - }); -}); diff --git a/public/app/core/directives/misc.js b/public/app/core/directives/misc.js index b3d6de2585d..1f96422d49e 100644 --- a/public/app/core/directives/misc.js +++ b/public/app/core/directives/misc.js @@ -90,7 +90,6 @@ function (angular, coreModule, kbn) { var li = '' + '' + (item.text || '') + ''; if (item.submenu && item.submenu.length) { diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts new file mode 100644 index 00000000000..94e50bf5c86 --- /dev/null +++ b/public/app/core/directives/plugin_component.ts @@ -0,0 +1,199 @@ +/// + +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 = `${template}`; + 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); diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts new file mode 100644 index 00000000000..b807d5bc50d --- /dev/null +++ b/public/app/core/directives/rebuild_on_change.ts @@ -0,0 +1,75 @@ +/// + +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); diff --git a/public/app/core/plugins/directive.ts b/public/app/core/plugins/directive.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/core/routes/all.js b/public/app/core/routes/all.js index ab3bef61ed7..2d313d7322f 100644 --- a/public/app/core/routes/all.js +++ b/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', diff --git a/public/app/features/all.js b/public/app/features/all.js index d4436c9deaa..4ec4719ebdd 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -5,6 +5,7 @@ define([ './templating/templateSrv', './dashboard/all', './playlist/all', + './snapshot/all', './panel/all', './profile/profileCtrl', './profile/changePasswordCtrl', diff --git a/public/app/features/annotations/annotations_srv.js b/public/app/features/annotations/annotations_srv.js index 9dce53084fe..b0022135ef5 100644 --- a/public/app/features/annotations/annotations_srv.js +++ b/public/app/features/annotations/annotations_srv.js @@ -2,7 +2,6 @@ define([ 'angular', 'lodash', './editor_ctrl', - './query_editor' ], function (angular, _) { 'use strict'; diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 8894cb4b47f..f5d29e57677 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -91,8 +91,10 @@ - - + + + +
diff --git a/public/app/features/annotations/query_editor.ts b/public/app/features/annotations/query_editor.ts deleted file mode 100644 index 42d3e9047b5..00000000000 --- a/public/app/features/annotations/query_editor.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// - -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); diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index e4d74c985ba..be404dd1ce1 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/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; } diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 8e3c24f7202..5c0101be10f 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/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; diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index f582252cd3c..7af2abfe076 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -115,9 +115,9 @@
-
-
-
Dashboard info
+
+
Dashboard info
+
  • @@ -130,17 +130,6 @@
-
    -
  • - Created at: -
  • -
  • - {{formatDate(dashboardMeta.created)}} -
  • -
-
-
-
  • Last updated by: @@ -150,6 +139,39 @@
+
+
+
    +
  • + Created at: +
  • +
  • + {{formatDate(dashboardMeta.created)}} +
  • +
+
+
+
+
    +
  • + Created by: +
  • +
  • + {{dashboardMeta.createdBy}} +
  • +
+
+
+
+
    +
  • + Current version: +
  • +
  • + {{dashboardMeta.version}} +
  • +
+
diff --git a/public/app/features/dashboard/rowCtrl.js b/public/app/features/dashboard/rowCtrl.js index e8abb5bbb05..d7ccc22603f 100644 --- a/public/app/features/dashboard/rowCtrl.js +++ b/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); diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index abe1ec3279e..b18610d7c1d 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/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; } diff --git a/public/app/features/datasources/all.js b/public/app/features/datasources/all.js index 8a57bbc5b8a..b181fd475c2 100644 --- a/public/app/features/datasources/all.js +++ b/public/app/features/datasources/all.js @@ -1,5 +1,4 @@ define([ './list_ctrl', './edit_ctrl', - './config_view', ], function () {}); diff --git a/public/app/features/datasources/config_view.ts b/public/app/features/datasources/config_view.ts deleted file mode 100644 index 39f593ba508..00000000000 --- a/public/app/features/datasources/config_view.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// - -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); diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js index 2bd86e5f385..c3242f5b565 100644 --- a/public/app/features/datasources/edit_ctrl.js +++ b/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) { diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index 3ebae573ebd..00706cb4de5 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -41,7 +41,10 @@
- + + + +
Testing....
diff --git a/public/app/features/panel/all.js b/public/app/features/panel/all.js index c03089bbf9b..b3ec16e541f 100644 --- a/public/app/features/panel/all.js +++ b/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 () {}); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 76b6a7688e4..1e6f34775f5 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/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(); + try { + return datasource.query(metricsQuery).then(results => { + this.setTimeQueryEnd(); - if (this.dashboard.snapshot) { - this.panel.snapshotData = results; - } + if (this.dashboard.snapshot) { + this.panel.snapshotData = results; + } - return results; - }); - } - - addDataQuery(datasource) { - this.dashboard.addDataQueryTo(this.panel, datasource); - } - - removeDataQuery(query) { - this.dashboard.removeDataQuery(this.panel, query); - this.refresh(); - }; - - duplicateDataQuery(query) { - this.dashboard.duplicateDataQuery(this.panel, query); - } - - 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}; diff --git a/public/app/features/panel/panel.ts b/public/app/features/panel/panel.ts index 56749f90253..c2415aaa1da 100644 --- a/public/app/features/panel/panel.ts +++ b/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, } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index fa1b8663f9c..96b1a6d9d4d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/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 + }); + } } diff --git a/public/app/features/panel/panel_directive.js b/public/app/features/panel/panel_directive.js deleted file mode 100644 index da920094e97..00000000000 --- a/public/app/features/panel/panel_directive.js +++ /dev/null @@ -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: '', - 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); - }); - } - }; - }); - -}); diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts new file mode 100644 index 00000000000..757f9f8e43d --- /dev/null +++ b/public/app/features/panel/panel_directive.ts @@ -0,0 +1,101 @@ +/// + +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: '', + 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); + }); + } + }; +}); + + diff --git a/public/app/features/panel/panel_loader.ts b/public/app/features/panel/panel_loader.ts deleted file mode 100644 index 58d0fe47c73..00000000000 --- a/public/app/features/panel/panel_loader.ts +++ /dev/null @@ -1,88 +0,0 @@ -/// - -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 = `${template}`; - 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); diff --git a/public/app/features/panel/panel_menu.js b/public/app/features/panel/panel_menu.js index b23da0f2a88..0489489af8e 100644 --- a/public/app/features/panel/panel_menu.js +++ b/public/app/features/panel/panel_menu.js @@ -37,7 +37,7 @@ function (angular, $, _) { template += '
'; template += ''; template += ''; - template += ''; + template += ''; template += '
'; template += '
'; } @@ -53,7 +53,6 @@ function (angular, $, _) { template += ''; }); diff --git a/public/app/features/panel/partials/panel.html b/public/app/features/panel/partials/panel.html index 13c2178e0b4..b5228840687 100644 --- a/public/app/features/panel/partials/panel.html +++ b/public/app/features/panel/partials/panel.html @@ -1,6 +1,6 @@
- + diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html new file mode 100644 index 00000000000..81f7e4f6c98 --- /dev/null +++ b/public/app/features/panel/partials/query_editor_row.html @@ -0,0 +1,56 @@ +
+ + +
    +
  • + {{ctrl.target.refId}} +
  • +
  • + + + +
  • +
+ +
    +
+ +
+
diff --git a/public/app/features/panel/query_ctrl.ts b/public/app/features/panel/query_ctrl.ts new file mode 100644 index 00000000000..66370595252 --- /dev/null +++ b/public/app/features/panel/query_ctrl.ts @@ -0,0 +1,57 @@ +/// + +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(); + } +} + diff --git a/public/app/features/panel/query_editor.ts b/public/app/features/panel/query_editor.ts deleted file mode 100644 index 7a5c6126740..00000000000 --- a/public/app/features/panel/query_editor.ts +++ /dev/null @@ -1,48 +0,0 @@ -/// - -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); diff --git a/public/app/features/panel/query_editor_row.ts b/public/app/features/panel/query_editor_row.ts new file mode 100644 index 00000000000..a4cb6155139 --- /dev/null +++ b/public/app/features/panel/query_editor_row.ts @@ -0,0 +1,18 @@ +/// + +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); diff --git a/public/app/features/snapshot/all.ts b/public/app/features/snapshot/all.ts new file mode 100644 index 00000000000..521c7a4c111 --- /dev/null +++ b/public/app/features/snapshot/all.ts @@ -0,0 +1 @@ +import './snapshot_ctrl'; diff --git a/public/app/features/snapshot/partials/snapshots.html b/public/app/features/snapshot/partials/snapshots.html new file mode 100644 index 00000000000..3bbc671e184 --- /dev/null +++ b/public/app/features/snapshot/partials/snapshots.html @@ -0,0 +1,39 @@ + + +
+
+ +

Available snapshots

+ + + + + + + + + + + + + + + + +
NameSnapshot url
+ {{snapshot.name}} + + dashboard/snapshot/{{snapshot.key}} + + + + View + + + + + +
+ +
+
diff --git a/public/app/features/snapshot/snapshot_ctrl.ts b/public/app/features/snapshot/snapshot_ctrl.ts new file mode 100644 index 00000000000..7b38f9ad8eb --- /dev/null +++ b/public/app/features/snapshot/snapshot_ctrl.ts @@ -0,0 +1,42 @@ +/// + +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); diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 97db5617a1c..d63c0d01e29 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -65,7 +65,7 @@
  • - Row editor + Row editor
  • Delete row @@ -81,8 +81,8 @@
    - - + +
    diff --git a/public/app/partials/inspector.html b/public/app/partials/inspector.html index 5a83afe9a3b..9caa38bb39f 100644 --- a/public/app/partials/inspector.html +++ b/public/app/partials/inspector.html @@ -61,9 +61,9 @@
    -
    -			{{message}}
    -		
    +
    +{{message}}
    +
    diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html
    index 8a7eb1bfd71..f7b1672cd97 100644
    --- a/public/app/partials/metrics.html
    +++ b/public/app/partials/metrics.html
    @@ -1,8 +1,12 @@
     
    - - +
    + + + + +
    @@ -26,7 +30,11 @@
    - + + + + +
    diff --git a/public/app/plugins/datasource/cloudwatch/datasource.d.ts b/public/app/plugins/datasource/cloudwatch/datasource.d.ts index a50d7ca49cc..afc49d07dea 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.d.ts +++ b/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}; diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 6f9f24ff867..c77e9f95bd6 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/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 + }; }); diff --git a/public/app/plugins/datasource/cloudwatch/module.js b/public/app/plugins/datasource/cloudwatch/module.js deleted file mode 100644 index 6f15457a43e..00000000000 --- a/public/app/plugins/datasource/cloudwatch/module.js +++ /dev/null @@ -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, - }; -}); diff --git a/public/app/plugins/datasource/cloudwatch/module.ts b/public/app/plugins/datasource/cloudwatch/module.ts new file mode 100644 index 00000000000..674506310ab --- /dev/null +++ b/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, +}; + diff --git a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html index 583a578134d..050698f3ff2 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html @@ -1 +1 @@ - + diff --git a/public/app/plugins/datasource/cloudwatch/partials/edit_view.html b/public/app/plugins/datasource/cloudwatch/partials/config.html similarity index 76% rename from public/app/plugins/datasource/cloudwatch/partials/edit_view.html rename to public/app/plugins/datasource/cloudwatch/partials/config.html index fbb641633ac..92a963da7c6 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/edit_view.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -9,7 +9,7 @@ Credentials profile nameCredentials profile name, as specified in ~/.aws/credentials, leave blank for default
  • - +
  • @@ -19,12 +19,12 @@
  • Default RegionSpecify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
  • -
  • - +
  • diff --git a/public/app/plugins/datasource/cloudwatch/partials/query.editor.html b/public/app/plugins/datasource/cloudwatch/partials/query.editor.html index fa5e74c33c2..a64635dc397 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/query.editor.html +++ b/public/app/plugins/datasource/cloudwatch/partials/query.editor.html @@ -1,38 +1,4 @@ -
    - + + -
      -
    • - {{target.refId}} -
    • -
    • - - - -
    • -
    - -
    -
    - - + diff --git a/public/app/plugins/datasource/cloudwatch/query_ctrl.js b/public/app/plugins/datasource/cloudwatch/query_ctrl.js deleted file mode 100644 index 927c08b3f2d..00000000000 --- a/public/app/plugins/datasource/cloudwatch/query_ctrl.js +++ /dev/null @@ -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}} {{}}'; - }; - - $scope.refreshMetricData = function() { - if (!_.isEqual($scope.oldTarget, $scope.target)) { - $scope.oldTarget = angular.copy($scope.target); - $scope.ctrl.refresh(); - } - }; - - $scope.init(); - - }); - -}); diff --git a/public/app/plugins/datasource/cloudwatch/query_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_ctrl.ts new file mode 100644 index 00000000000..daacd7fe198 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/query_ctrl.ts @@ -0,0 +1,17 @@ +/// + +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}} {{}}'; + } +} diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js index 3be1f0d5a01..c239291ed8e 100644 --- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js +++ b/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)); }; diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index ed5f9418f8f..cec32420aea 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/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() { diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts new file mode 100644 index 00000000000..4c14e1133bf --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts @@ -0,0 +1,34 @@ +/// + +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'; + } +} + diff --git a/public/app/plugins/datasource/elasticsearch/datasource.d.ts b/public/app/plugins/datasource/elasticsearch/datasource.d.ts index a50d7ca49cc..3682abfb614 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.d.ts +++ b/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}; diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 0d2fd174c4d..294f666abc7 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -304,5 +304,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes }; } - return ElasticDatasource; + return { + ElasticDatasource: ElasticDatasource + }; }); diff --git a/public/app/plugins/datasource/elasticsearch/edit_view.ts b/public/app/plugins/datasource/elasticsearch/edit_view.ts deleted file mode 100644 index c7e17d4fbea..00000000000 --- a/public/app/plugins/datasource/elasticsearch/edit_view.ts +++ /dev/null @@ -1,39 +0,0 @@ -/// - -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; diff --git a/public/app/plugins/datasource/elasticsearch/module.js b/public/app/plugins/datasource/elasticsearch/module.js deleted file mode 100644 index d38afe8e936..00000000000 --- a/public/app/plugins/datasource/elasticsearch/module.js +++ /dev/null @@ -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, - }; - -}); diff --git a/public/app/plugins/datasource/elasticsearch/module.ts b/public/app/plugins/datasource/elasticsearch/module.ts new file mode 100644 index 00000000000..438649017e5 --- /dev/null +++ b/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, +}; diff --git a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html index 0b7070904de..8f761b67865 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html @@ -1,14 +1,14 @@
    -
    +
    Index name
    - +
    Search query (lucene) Use [[filterName]] in query to replace part of the query with a filter value
    - +
    @@ -18,22 +18,22 @@
    Field mappings
    - +
    - +
    - +
    - +
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/edit_view.html b/public/app/plugins/datasource/elasticsearch/partials/config.html similarity index 67% rename from public/app/plugins/datasource/elasticsearch/partials/edit_view.html rename to public/app/plugins/datasource/elasticsearch/partials/config.html index bf43012c72e..05df80143e3 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/edit_view.html +++ b/public/app/plugins/datasource/elasticsearch/partials/config.html @@ -1,4 +1,5 @@ - + +

    Elasticsearch details

    @@ -8,13 +9,13 @@ Index name
  • - +
  • Pattern
  • - +
  • @@ -25,7 +26,7 @@ Time field name
  • - +
  • @@ -36,7 +37,7 @@ Version
  • - +
  • @@ -52,7 +53,7 @@ Group by time interval
  • -
  • diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html index bbf5964b220..017f5cf1a42 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html @@ -1,77 +1,32 @@ -
    - - -
      -
    • - {{target.refId}} -
    • -
    • - - - -
    • -
    - -
      -
    • - Query -
    • -
    • - -
    • -
    • - Alias -
    • -
    • - -
    • -
    -
    +
    + +
    -
    -
    - - -
    - -
    - - -
    - +
    + +
    + diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.options.html b/public/app/plugins/datasource/elasticsearch/partials/query.options.html index 628a0a0bf3d..bac22df7136 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/query.options.html +++ b/public/app/plugins/datasource/elasticsearch/partials/query.options.html @@ -8,7 +8,7 @@ Group by time interval
  • -
  • @@ -23,7 +23,7 @@
  • - + alias patterns
  • @@ -34,7 +34,7 @@
    -
    +
    Alias patterns
    • {{term fieldname}} = replaced with value of term group by
    • diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.js b/public/app/plugins/datasource/elasticsearch/query_ctrl.js deleted file mode 100644 index 90214dff17c..00000000000 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.js +++ /dev/null @@ -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(); - - }); - -}); diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts new file mode 100644 index 00000000000..fb04f39b8ac --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -0,0 +1,45 @@ +/// + +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 []; + } +} diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index a7e6a642550..133d138be82 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/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() { diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts deleted file mode 100644 index aceef08a3d9..00000000000 --- a/public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts +++ /dev/null @@ -1,29 +0,0 @@ -/// -/// -/// - -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(); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana/module.ts b/public/app/plugins/datasource/grafana/module.ts index 7eba430ee43..19233c4f975 100644 --- a/public/app/plugins/datasource/grafana/module.ts +++ b/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, }; diff --git a/public/app/plugins/datasource/grafana/partials/query.editor.html b/public/app/plugins/datasource/grafana/partials/query.editor.html index 15297d5c3f0..3131b4cc346 100644 --- a/public/app/plugins/datasource/grafana/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana/partials/query.editor.html @@ -1,56 +1,5 @@ -
      - - -
        -
      • - {{target.refId}} -
      • -
      • - - - -
      • -
      • - Test metric (fake data source) -
      • -
      -
      -
      + +
    • + Test metric (fake data source) +
    • +
      diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index 46d93f6481f..6c218513d57 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/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'); diff --git a/public/app/plugins/datasource/graphite/datasource.d.ts b/public/app/plugins/datasource/graphite/datasource.d.ts deleted file mode 100644 index a50d7ca49cc..00000000000 --- a/public/app/plugins/datasource/graphite/datasource.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare var Datasource: any; -export default Datasource; - diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js deleted file mode 100644 index 3a169eaae4e..00000000000 --- a/public/app/plugins/datasource/graphite/datasource.js +++ /dev/null @@ -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; -}); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts new file mode 100644 index 00000000000..c57e5fcfd26 --- /dev/null +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -0,0 +1,281 @@ +/// + +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; + }; +} diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 63ee4b3f7cf..cd1f747bc4d 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/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; } diff --git a/public/app/plugins/datasource/graphite/lexer.js b/public/app/plugins/datasource/graphite/lexer.js deleted file mode 100644 index 2b8affd04e2..00000000000 --- a/public/app/plugins/datasource/graphite/lexer.js +++ /dev/null @@ -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; - -}); diff --git a/public/app/plugins/datasource/graphite/lexer.ts b/public/app/plugins/datasource/graphite/lexer.ts new file mode 100644 index 00000000000..1835921d40b --- /dev/null +++ b/public/app/plugins/datasource/graphite/lexer.ts @@ -0,0 +1,678 @@ +/// + +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 + }; + }, + + }; + diff --git a/public/app/plugins/datasource/graphite/module.js b/public/app/plugins/datasource/graphite/module.js deleted file mode 100644 index 64f0945aef3..00000000000 --- a/public/app/plugins/datasource/graphite/module.js +++ /dev/null @@ -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, - }; -}); diff --git a/public/app/plugins/datasource/graphite/module.ts b/public/app/plugins/datasource/graphite/module.ts new file mode 100644 index 00000000000..63c04833fc2 --- /dev/null +++ b/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, +}; + diff --git a/public/app/plugins/datasource/graphite/parser.js b/public/app/plugins/datasource/graphite/parser.js deleted file mode 100644 index 43fd148f1ea..00000000000 --- a/public/app/plugins/datasource/graphite/parser.js +++ /dev/null @@ -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; -}); diff --git a/public/app/plugins/datasource/graphite/parser.ts b/public/app/plugins/datasource/graphite/parser.ts new file mode 100644 index 00000000000..bfafdda7815 --- /dev/null +++ b/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)); + }, +}; + diff --git a/public/app/plugins/datasource/graphite/partials/annotations.editor.html b/public/app/plugins/datasource/graphite/partials/annotations.editor.html index 9253bf614a6..ea5c2f7f50f 100644 --- a/public/app/plugins/datasource/graphite/partials/annotations.editor.html +++ b/public/app/plugins/datasource/graphite/partials/annotations.editor.html @@ -1,14 +1,14 @@
      - +
      - +
      diff --git a/public/app/plugins/datasource/graphite/partials/config.html b/public/app/plugins/datasource/graphite/partials/config.html index 9f5259cb2ea..d7a0f739ccb 100644 --- a/public/app/plugins/datasource/graphite/partials/config.html +++ b/public/app/plugins/datasource/graphite/partials/config.html @@ -1,2 +1,3 @@ - + + diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 9cb2b454132..90dfe0794c0 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -1,73 +1,21 @@ -
      - + -
        -
      • - {{target.refId}} -
      • -
      • - - - -
      • -
      +
    • + +
    • - - - +
    • - -
      -
      +
    • + +
    • +
    • + + +
    • + + +
    • + + diff --git a/public/app/plugins/datasource/graphite/partials/query.options.html b/public/app/plugins/datasource/graphite/partials/query.options.html index f20b55b1348..35c95d03e88 100644 --- a/public/app/plugins/datasource/graphite/partials/query.options.html +++ b/public/app/plugins/datasource/graphite/partials/query.options.html @@ -1,5 +1,4 @@
      -
      • @@ -11,7 +10,7 @@
      • @@ -39,27 +38,27 @@
      • - + shorter legend names
      • - + series as parameters
      • - + stacking
      • - + templating
      • - + max data points
      • @@ -71,7 +70,7 @@
        -
        +
        Shorter legend names
        • alias() function to specify a custom series name
        • @@ -81,7 +80,7 @@
        -
        +
        Series as parameter
        • Some graphite functions allow you to have many series arguments
        • @@ -99,7 +98,7 @@
        -
        +
        Stacking
        • You find the stacking option under Display Styles tab
        • @@ -107,7 +106,7 @@
        -
        +
        Templating
        • You can use a template variable in place of metric names
        • @@ -116,7 +115,7 @@
        -
        +
        Max data points
        • Every graphite request is issued with a maxDataPoints parameter
        • diff --git a/public/app/plugins/datasource/graphite/query_ctrl.js b/public/app/plugins/datasource/graphite/query_ctrl.js deleted file mode 100644 index 55083aefaa2..00000000000 --- a/public/app/plugins/datasource/graphite/query_ctrl.js +++ /dev/null @@ -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(); - - }); - -}); diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts new file mode 100644 index 00000000000..23c6c2aa6d6 --- /dev/null +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -0,0 +1,276 @@ +/// + +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; + } + } + } +} diff --git a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts index 810e1be3516..2125922b350 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts +++ b/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() { diff --git a/public/app/plugins/datasource/graphite/specs/lexer_specs.ts b/public/app/plugins/datasource/graphite/specs/lexer_specs.ts new file mode 100644 index 00000000000..1f4be736536 --- /dev/null +++ b/public/app/plugins/datasource/graphite/specs/lexer_specs.ts @@ -0,0 +1,118 @@ + +import {describe, it, expect} from 'test/lib/common'; +import {Lexer} from '../lexer'; + +describe('when lexing graphite expression', function() { + + it('should tokenize metric expression', function() { + var lexer = new Lexer('metric.test.*.asd.count'); + var tokens = lexer.tokenize(); + expect(tokens[0].value).to.be('metric'); + expect(tokens[1].value).to.be('.'); + expect(tokens[2].type).to.be('identifier'); + expect(tokens[4].type).to.be('identifier'); + expect(tokens[4].pos).to.be(13); + }); + + it('should tokenize metric expression with dash', function() { + var lexer = new Lexer('metric.test.se1-server-*.asd.count'); + var tokens = lexer.tokenize(); + expect(tokens[4].type).to.be('identifier'); + expect(tokens[4].value).to.be('se1-server-*'); + }); + + it('should tokenize metric expression with dash2', function() { + var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*'); + var tokens = lexer.tokenize(); + expect(tokens[0].value).to.be('net'); + expect(tokens[2].value).to.be('192-168-1-1'); + }); + + it('should tokenize metric expression with equal sign', function() { + var lexer = new Lexer('apps=test'); + var tokens = lexer.tokenize(); + expect(tokens[0].value).to.be('apps=test'); + }); + + it('simple function2', function() { + var lexer = new Lexer('offset(test.metric, -100)'); + var tokens = lexer.tokenize(); + expect(tokens[2].type).to.be('identifier'); + expect(tokens[4].type).to.be('identifier'); + expect(tokens[6].type).to.be('number'); + }); + + it('should tokenize metric expression with curly braces', function() { + var lexer = new Lexer('metric.se1-{first, second}.count'); + var tokens = lexer.tokenize(); + expect(tokens.length).to.be(10); + expect(tokens[3].type).to.be('{'); + expect(tokens[4].value).to.be('first'); + expect(tokens[5].value).to.be(','); + expect(tokens[6].value).to.be('second'); + }); + + it('should tokenize metric expression with number segments', function() { + var lexer = new Lexer("metric.10.12_10.test"); + var tokens = lexer.tokenize(); + expect(tokens[0].type).to.be('identifier'); + expect(tokens[2].type).to.be('identifier'); + expect(tokens[2].value).to.be('10'); + expect(tokens[4].value).to.be('12_10'); + expect(tokens[4].type).to.be('identifier'); + }); + + it('should tokenize func call with numbered metric and number arg', function() { + var lexer = new Lexer("scale(metric.10, 15)"); + var tokens = lexer.tokenize(); + expect(tokens[0].type).to.be('identifier'); + expect(tokens[2].type).to.be('identifier'); + expect(tokens[2].value).to.be('metric'); + expect(tokens[4].value).to.be('10'); + expect(tokens[4].type).to.be('number'); + expect(tokens[6].type).to.be('number'); + }); + + it('should tokenize metric with template parameter', function() { + var lexer = new Lexer("metric.[[server]].test"); + var tokens = lexer.tokenize(); + expect(tokens[2].type).to.be('identifier'); + expect(tokens[2].value).to.be('[[server]]'); + expect(tokens[4].type).to.be('identifier'); + }); + + it('should tokenize metric with question mark', function() { + var lexer = new Lexer("metric.server_??.test"); + var tokens = lexer.tokenize(); + expect(tokens[2].type).to.be('identifier'); + expect(tokens[2].value).to.be('server_??'); + expect(tokens[4].type).to.be('identifier'); + }); + + it('should handle error with unterminated string', function() { + var lexer = new Lexer("alias(metric, 'asd)"); + var tokens = lexer.tokenize(); + expect(tokens[0].value).to.be('alias'); + expect(tokens[1].value).to.be('('); + expect(tokens[2].value).to.be('metric'); + expect(tokens[3].value).to.be(','); + expect(tokens[4].type).to.be('string'); + expect(tokens[4].isUnclosed).to.be(true); + expect(tokens[4].pos).to.be(20); + }); + + it('should handle float parameters', function() { + var lexer = new Lexer("alias(metric, 0.002)"); + var tokens = lexer.tokenize(); + expect(tokens[4].type).to.be('number'); + expect(tokens[4].value).to.be('0.002'); + }); + + it('should handle bool parameters', function() { + var lexer = new Lexer("alias(metric, true, false)"); + var tokens = lexer.tokenize(); + expect(tokens[4].type).to.be('bool'); + expect(tokens[4].value).to.be('true'); + expect(tokens[6].type).to.be('bool'); + }); +}); diff --git a/public/app/plugins/datasource/graphite/specs/parser_specs.ts b/public/app/plugins/datasource/graphite/specs/parser_specs.ts new file mode 100644 index 00000000000..a69dcb56dfb --- /dev/null +++ b/public/app/plugins/datasource/graphite/specs/parser_specs.ts @@ -0,0 +1,183 @@ +import {describe, it, expect} from 'test/lib/common'; +import {Parser} from '../parser'; + +describe('when parsing', function() { + + it('simple metric expression', function() { + var parser = new Parser('metric.test.*.asd.count'); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('metric'); + expect(rootNode.segments.length).to.be(5); + expect(rootNode.segments[0].value).to.be('metric'); + }); + + it('simple metric expression with numbers in segments', function() { + var parser = new Parser('metric.10.15_20.5'); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('metric'); + expect(rootNode.segments.length).to.be(4); + expect(rootNode.segments[1].value).to.be('10'); + expect(rootNode.segments[2].value).to.be('15_20'); + expect(rootNode.segments[3].value).to.be('5'); + }); + + it('simple metric expression with curly braces', function() { + var parser = new Parser('metric.se1-{count, max}'); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('metric'); + expect(rootNode.segments.length).to.be(2); + expect(rootNode.segments[1].value).to.be('se1-{count,max}'); + }); + + it('simple metric expression with curly braces at start of segment and with post chars', function() { + var parser = new Parser('metric.{count, max}-something.count'); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('metric'); + expect(rootNode.segments.length).to.be(3); + expect(rootNode.segments[1].value).to.be('{count,max}-something'); + }); + + it('simple function', function() { + var parser = new Parser('sum(test)'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params.length).to.be(1); + }); + + it('simple function2', function() { + var parser = new Parser('offset(test.metric, -100)'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params[0].type).to.be('metric'); + expect(rootNode.params[1].type).to.be('number'); + }); + + it('simple function with string arg', function() { + var parser = new Parser("randomWalk('test')"); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params.length).to.be(1); + expect(rootNode.params[0].type).to.be('string'); + }); + + it('function with multiple args', function() { + var parser = new Parser("sum(test, 1, 'test')"); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('function'); + expect(rootNode.params.length).to.be(3); + expect(rootNode.params[0].type).to.be('metric'); + expect(rootNode.params[1].type).to.be('number'); + expect(rootNode.params[2].type).to.be('string'); + }); + + it('function with nested function', function() { + var parser = new Parser("sum(scaleToSeconds(test, 1))"); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('function'); + expect(rootNode.params.length).to.be(1); + expect(rootNode.params[0].type).to.be('function'); + expect(rootNode.params[0].name).to.be('scaleToSeconds'); + expect(rootNode.params[0].params.length).to.be(2); + expect(rootNode.params[0].params[0].type).to.be('metric'); + expect(rootNode.params[0].params[1].type).to.be('number'); + }); + + it('function with multiple series', function() { + var parser = new Parser("sum(test.test.*.count, test.timers.*.count)"); + var rootNode = parser.getAst(); + + expect(rootNode.type).to.be('function'); + expect(rootNode.params.length).to.be(2); + expect(rootNode.params[0].type).to.be('metric'); + expect(rootNode.params[1].type).to.be('metric'); + }); + + it('function with templated series', function() { + var parser = new Parser("sum(test.[[server]].count)"); + var rootNode = parser.getAst(); + + expect(rootNode.message).to.be(undefined); + expect(rootNode.params[0].type).to.be('metric'); + expect(rootNode.params[0].segments[1].type).to.be('segment'); + expect(rootNode.params[0].segments[1].value).to.be('[[server]]'); + }); + + it('invalid metric expression', function() { + var parser = new Parser('metric.test.*.asd.'); + var rootNode = parser.getAst(); + + expect(rootNode.message).to.be('Expected metric identifier instead found end of string'); + expect(rootNode.pos).to.be(19); + }); + + it('invalid function expression missing closing parenthesis', function() { + var parser = new Parser('sum(test'); + var rootNode = parser.getAst(); + + expect(rootNode.message).to.be('Expected closing parenthesis instead found end of string'); + expect(rootNode.pos).to.be(9); + }); + + it('unclosed string in function', function() { + var parser = new Parser("sum('test)"); + var rootNode = parser.getAst(); + + expect(rootNode.message).to.be('Unclosed string parameter'); + expect(rootNode.pos).to.be(11); + }); + + it('handle issue #69', function() { + var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + }); + + it('handle float function arguments', function() { + var parser = new Parser('scale(test, 0.002)'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params[1].type).to.be('number'); + expect(rootNode.params[1].value).to.be(0.002); + }); + + it('handle curly brace pattern at start', function() { + var parser = new Parser('{apps}.test'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('metric'); + expect(rootNode.segments[0].value).to.be('{apps}'); + expect(rootNode.segments[1].value).to.be('test'); + }); + + it('series parameters', function() { + var parser = new Parser('asPercent(#A, #B)'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params[0].type).to.be('series-ref'); + expect(rootNode.params[0].value).to.be('#A'); + expect(rootNode.params[1].value).to.be('#B'); + }); + + it('series parameters, issue 2788', function() { + var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)"); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params[0].type).to.be('function'); + expect(rootNode.params[1].value).to.be('10m'); + expect(rootNode.params[3].type).to.be('bool'); + }); + + it('should parse metric expression with ip number segments', function() { + var parser = new Parser('5.10.123.5'); + var rootNode = parser.getAst(); + expect(rootNode.segments[0].value).to.be('5'); + expect(rootNode.segments[1].value).to.be('10'); + expect(rootNode.segments[2].value).to.be('123'); + expect(rootNode.segments[3].value).to.be('5'); + }); +}); diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts index 195e872efc7..8c191bba528 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts @@ -5,6 +5,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co import gfunc from '../gfunc'; import helpers from 'test/specs/helpers'; +import {GraphiteQueryCtrl} from '../query_ctrl'; describe('GraphiteQueryCtrl', function() { var ctx = new helpers.ControllerTestContext(); @@ -17,53 +18,47 @@ describe('GraphiteQueryCtrl', function() { beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { ctx.$q = $q; ctx.scope = $rootScope.$new(); - ctx.scope.ctrl = {panel: ctx.panel}; - ctx.panelCtrl = ctx.scope.ctrl; - ctx.scope.datasource = ctx.datasource; - ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); - ctx.controller = $controller('GraphiteQueryCtrl', {$scope: ctx.scope}); + ctx.target = {target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)'}; + ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); + ctx.panelCtrl = {panel: {}}; + ctx.panelCtrl.refresh = sinon.spy(); + + ctx.ctrl = $controller(GraphiteQueryCtrl, {$scope: ctx.scope}, { + panelCtrl: ctx.panelCtrl, + datasource: ctx.datasource, + target: ctx.target + }); + ctx.scope.$digest(); })); - beforeEach(function() { - ctx.scope.target = {target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)'}; - }); - describe('init', function() { - beforeEach(function() { - ctx.scope.init(); - ctx.scope.$digest(); - }); - it('should validate metric key exists', function() { - expect(ctx.scope.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*'); + expect(ctx.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*'); }); it('should delete last segment if no metrics are found', function() { - expect(ctx.scope.segments[2].value).to.be('select metric'); + expect(ctx.ctrl.segments[2].value).to.be('select metric'); }); it('should parse expression and build function model', function() { - expect(ctx.scope.functions.length).to.be(2); + expect(ctx.ctrl.functions.length).to.be(2); }); }); describe('when adding function', function() { beforeEach(function() { - ctx.scope.target.target = 'test.prod.*.count'; - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}])); - ctx.scope.init(); - ctx.scope.$digest(); - - ctx.panelCtrl.refresh = sinon.spy(); - ctx.scope.addFunction(gfunc.getFuncDef('aliasByNode')); + ctx.ctrl.target.target = 'test.prod.*.count'; + ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}])); + ctx.ctrl.parseTarget(); + ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode')); }); it('should add function with correct node number', function() { - expect(ctx.scope.functions[0].params[0]).to.be(2); + expect(ctx.ctrl.functions[0].params[0]).to.be(2); }); it('should update target', function() { - expect(ctx.scope.target.target).to.be('aliasByNode(test.prod.*.count, 2)'); + expect(ctx.ctrl.target.target).to.be('aliasByNode(test.prod.*.count, 2)'); }); it('should call refresh', function() { @@ -73,78 +68,72 @@ describe('GraphiteQueryCtrl', function() { describe('when adding function before any metric segment', function() { beforeEach(function() { - ctx.scope.target.target = ''; - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}])); - ctx.scope.init(); - ctx.scope.$digest(); - ctx.scope.addFunction(gfunc.getFuncDef('asPercent')); + ctx.ctrl.target.target = ''; + ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}])); + ctx.ctrl.parseTarget(); + ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent')); }); it('should add function and remove select metric link', function() { - expect(ctx.scope.segments.length).to.be(0); + expect(ctx.ctrl.segments.length).to.be(0); }); }); describe('when initalizing target without metric expression and only function', function() { beforeEach(function() { - ctx.scope.target.target = 'asPercent(#A, #B)'; - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); - ctx.scope.init(); + ctx.ctrl.target.target = 'asPercent(#A, #B)'; + ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.ctrl.parseTarget(); ctx.scope.$digest(); }); it('should not add select metric segment', function() { - expect(ctx.scope.segments.length).to.be(0); + expect(ctx.ctrl.segments.length).to.be(0); }); it('should add both series refs as params', function() { - expect(ctx.scope.functions[0].params.length).to.be(2); + expect(ctx.ctrl.functions[0].params.length).to.be(2); }); - }); describe('when initializing a target with single param func using variable', function() { beforeEach(function() { - ctx.scope.target.target = 'movingAverage(prod.count, $var)'; - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); - ctx.scope.init(); - ctx.scope.$digest(); + ctx.ctrl.target.target = 'movingAverage(prod.count, $var)'; + ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.ctrl.parseTarget(); }); it('should add 2 segments', function() { - expect(ctx.scope.segments.length).to.be(2); + expect(ctx.ctrl.segments.length).to.be(2); }); it('should add function param', function() { - expect(ctx.scope.functions[0].params.length).to.be(1); + expect(ctx.ctrl.functions[0].params.length).to.be(1); }); - }); describe('when initalizing target without metric expression and function with series-ref', function() { beforeEach(function() { - ctx.scope.target.target = 'asPercent(metric.node.count, #A)'; - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); - ctx.scope.init(); - ctx.scope.$digest(); - ctx.scope.$parent = { get_data: sinon.spy() }; + ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)'; + ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.ctrl.parseTarget(); }); it('should add segments', function() { - expect(ctx.scope.segments.length).to.be(3); + expect(ctx.ctrl.segments.length).to.be(3); }); it('should have correct func params', function() { - expect(ctx.scope.functions[0].params.length).to.be(1); + expect(ctx.ctrl.functions[0].params.length).to.be(1); }); }); describe('when getting altSegments and metricFindQuery retuns empty array', function() { beforeEach(function() { - ctx.scope.target.target = 'test.count'; - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); - ctx.scope.init(); - ctx.scope.getAltSegments(1).then(function(results) { + ctx.ctrl.target.target = 'test.count'; + ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.ctrl.parseTarget(); + ctx.ctrl.getAltSegments(1).then(function(results) { ctx.altSegments = results; }); ctx.scope.$digest(); @@ -153,22 +142,18 @@ describe('GraphiteQueryCtrl', function() { it('should have no segments', function() { expect(ctx.altSegments.length).to.be(0); }); - }); describe('targetChanged', function() { beforeEach(function() { - ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}])); - ctx.scope.init(); - ctx.scope.$digest(); - - ctx.panelCtrl.refresh = sinon.spy(); - ctx.scope.target.target = ''; - ctx.scope.targetChanged(); + ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}])); + ctx.ctrl.parseTarget(); + ctx.ctrl.target.target = ''; + ctx.ctrl.targetChanged(); }); it('should rebuld target after expression model', function() { - expect(ctx.scope.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)'); + expect(ctx.ctrl.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)'); }); it('should call panelCtrl.refresh', function() { diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js deleted file mode 100644 index 8d171deb7f9..00000000000 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ /dev/null @@ -1,221 +0,0 @@ -define([ - 'angular', - 'lodash', - 'app/core/utils/datemath', - './influx_series', - './influx_query', - './query_ctrl', -], -function (angular, _, dateMath, InfluxSeries, InfluxQuery) { - 'use strict'; - - InfluxQuery = InfluxQuery.default; - - /** @ngInject */ - function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) { - this.type = 'influxdb'; - this.urls = _.map(instanceSettings.url.split(','), function(url) { - return url.trim(); - }); - - this.username = instanceSettings.username; - this.password = instanceSettings.password; - this.name = instanceSettings.name; - this.database = instanceSettings.database; - this.basicAuth = instanceSettings.basicAuth; - - this.supportAnnotations = true; - this.supportMetrics = true; - - this.query = function(options) { - var timeFilter = getTimeFilter(options); - var queryTargets = []; - var i, y; - - var allQueries = _.map(options.targets, function(target) { - if (target.hide) { return []; } - - queryTargets.push(target); - - // build query - var queryModel = new InfluxQuery(target); - var query = queryModel.render(); - query = query.replace(/\$interval/g, (target.interval || options.interval)); - return query; - - }).join("\n"); - - // replace grafana variables - allQueries = allQueries.replace(/\$timeFilter/g, timeFilter); - - // replace templated variables - allQueries = templateSrv.replace(allQueries, options.scopedVars); - - return this._seriesQuery(allQueries).then(function(data) { - if (!data || !data.results) { - return []; - } - - var seriesList = []; - for (i = 0; i < data.results.length; i++) { - var result = data.results[i]; - if (!result || !result.series) { continue; } - - var target = queryTargets[i]; - var alias = target.alias; - if (alias) { - alias = templateSrv.replace(target.alias, options.scopedVars); - } - - var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }); - - switch(target.resultFormat) { - case 'table': { - seriesList.push(influxSeries.getTable()); - break; - } - default: { - var timeSeries = influxSeries.getTimeSeries(); - for (y = 0; y < timeSeries.length; y++) { - seriesList.push(timeSeries[y]); - } - break; - } - } - } - - return { data: seriesList }; - }); - }; - - this.annotationQuery = function(options) { - var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw}); - var query = options.annotation.query.replace('$timeFilter', timeFilter); - query = templateSrv.replace(query); - - return this._seriesQuery(query).then(function(data) { - if (!data || !data.results || !data.results[0]) { - throw { message: 'No results in response from InfluxDB' }; - } - return new InfluxSeries({series: data.results[0].series, annotation: options.annotation}).getAnnotations(); - }); - }; - - this.metricFindQuery = function (query) { - var interpolated; - try { - interpolated = templateSrv.replace(query); - } - catch (err) { - return $q.reject(err); - } - - return this._seriesQuery(interpolated).then(function (results) { - if (!results || results.results.length === 0) { return []; } - - var influxResults = results.results[0]; - if (!influxResults.series) { - return []; - } - - var series = influxResults.series[0]; - return _.map(series.values, function(value) { - if (_.isArray(value)) { - return { text: value[0] }; - } else { - return { text: value }; - } - }); - }); - }; - - this._seriesQuery = function(query) { - return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); - }; - - this.testDatasource = function() { - return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { - return { status: "success", message: "Data source is working", title: "Success" }; - }); - }; - - this._influxRequest = function(method, url, data) { - var self = this; - - var currentUrl = self.urls.shift(); - self.urls.push(currentUrl); - - var params = { - u: self.username, - p: self.password, - }; - - if (self.database) { - params.db = self.database; - } - - if (method === 'GET') { - _.extend(params, data); - data = null; - } - - var options = { - method: method, - url: currentUrl + url, - params: params, - data: data, - precision: "ms", - inspect: { type: 'influxdb' }, - }; - - options.headers = options.headers || {}; - if (self.basicAuth) { - options.headers.Authorization = self.basicAuth; - } - - return backendSrv.datasourceRequest(options).then(function(result) { - return result.data; - }, function(err) { - if (err.status !== 0 || err.status >= 300) { - if (err.data && err.data.error) { - throw { message: 'InfluxDB Error Response: ' + err.data.error, data: err.data, config: err.config }; - } - else { - throw { message: 'InfluxDB Error: ' + err.message, data: err.data, config: err.config }; - } - } - }); - }; - - function getTimeFilter(options) { - var from = getInfluxTime(options.rangeRaw.from, false); - var until = getInfluxTime(options.rangeRaw.to, true); - var fromIsAbsolute = from[from.length-1] === 's'; - - if (until === 'now()' && !fromIsAbsolute) { - return 'time > ' + from; - } - - return 'time > ' + from + ' and time < ' + until; - } - - function getInfluxTime(date, roundUp) { - if (_.isString(date)) { - if (date === 'now') { - return 'now()'; - } - - var parts = /^now-(\d+)([d|h|m|s])$/.exec(date); - if (parts) { - var amount = parseInt(parts[1]); - var unit = parts[2]; - return 'now() - ' + amount + unit; - } - date = dateMath.parse(date, roundUp); - } - return (date.valueOf() / 1000).toFixed(0) + 's'; - } - } - - return InfluxDatasource; -}); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts new file mode 100644 index 00000000000..2ef73dc8f2d --- /dev/null +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -0,0 +1,213 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; + +import * as dateMath from 'app/core/utils/datemath'; +import InfluxSeries from './influx_series'; +import InfluxQuery from './influx_query'; + +/** @ngInject */ +export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) { + this.type = 'influxdb'; + this.urls = _.map(instanceSettings.url.split(','), function(url) { + return url.trim(); + }); + + this.username = instanceSettings.username; + this.password = instanceSettings.password; + this.name = instanceSettings.name; + this.database = instanceSettings.database; + this.basicAuth = instanceSettings.basicAuth; + + this.supportAnnotations = true; + this.supportMetrics = true; + + this.query = function(options) { + var timeFilter = getTimeFilter(options); + var queryTargets = []; + var i, y; + + var allQueries = _.map(options.targets, function(target) { + if (target.hide) { return []; } + + queryTargets.push(target); + + // build query + var queryModel = new InfluxQuery(target); + var query = queryModel.render(); + query = query.replace(/\$interval/g, (target.interval || options.interval)); + return query; + + }).join("\n"); + + // replace grafana variables + allQueries = allQueries.replace(/\$timeFilter/g, timeFilter); + + // replace templated variables + allQueries = templateSrv.replace(allQueries, options.scopedVars); + + return this._seriesQuery(allQueries).then(function(data): any { + if (!data || !data.results) { + return []; + } + + var seriesList = []; + for (i = 0; i < data.results.length; i++) { + var result = data.results[i]; + if (!result || !result.series) { continue; } + + var target = queryTargets[i]; + var alias = target.alias; + if (alias) { + alias = templateSrv.replace(target.alias, options.scopedVars); + } + + var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }); + + switch (target.resultFormat) { + case 'table': { + seriesList.push(influxSeries.getTable()); + break; + } + default: { + var timeSeries = influxSeries.getTimeSeries(); + for (y = 0; y < timeSeries.length; y++) { + seriesList.push(timeSeries[y]); + } + break; + } + } + } + + return { data: seriesList }; + }); + }; + + this.annotationQuery = function(options) { + var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw}); + var query = options.annotation.query.replace('$timeFilter', timeFilter); + query = templateSrv.replace(query); + + return this._seriesQuery(query).then(function(data) { + if (!data || !data.results || !data.results[0]) { + throw { message: 'No results in response from InfluxDB' }; + } + return new InfluxSeries({series: data.results[0].series, annotation: options.annotation}).getAnnotations(); + }); + }; + + this.metricFindQuery = function (query) { + var interpolated; + try { + interpolated = templateSrv.replace(query); + } catch (err) { + return $q.reject(err); + } + + return this._seriesQuery(interpolated).then(function (results) { + if (!results || results.results.length === 0) { return []; } + + var influxResults = results.results[0]; + if (!influxResults.series) { + return []; + } + + var series = influxResults.series[0]; + return _.map(series.values, function(value) { + if (_.isArray(value)) { + return { text: value[0] }; + } else { + return { text: value }; + } + }); + }); + }; + + this._seriesQuery = function(query) { + return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); + }; + + this.testDatasource = function() { + return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }); + }; + + this._influxRequest = function(method, url, data) { + var self = this; + + var currentUrl = self.urls.shift(); + self.urls.push(currentUrl); + + var params: any = { + u: self.username, + p: self.password, + }; + + if (self.database) { + params.db = self.database; + } + + if (method === 'GET') { + _.extend(params, data); + data = null; + } + + var options: any = { + method: method, + url: currentUrl + url, + params: params, + data: data, + precision: "ms", + inspect: { type: 'influxdb' }, + }; + + options.headers = options.headers || {}; + if (self.basicAuth) { + options.headers.Authorization = self.basicAuth; + } + + return backendSrv.datasourceRequest(options).then(function(result) { + return result.data; + }, function(err) { + if (err.status !== 0 || err.status >= 300) { + if (err.data && err.data.error) { + throw { message: 'InfluxDB Error Response: ' + err.data.error, data: err.data, config: err.config }; + } else { + throw { message: 'InfluxDB Error: ' + err.message, data: err.data, config: err.config }; + } + } + }); + }; + + function getTimeFilter(options) { + var from = getInfluxTime(options.rangeRaw.from, false); + var until = getInfluxTime(options.rangeRaw.to, true); + var fromIsAbsolute = from[from.length-1] === 's'; + + if (until === 'now()' && !fromIsAbsolute) { + return 'time > ' + from; + } + + return 'time > ' + from + ' and time < ' + until; + } + + function getInfluxTime(date, roundUp) { + if (_.isString(date)) { + if (date === 'now') { + return 'now()'; + } + + var parts = /^now-(\d+)([d|h|m|s])$/.exec(date); + if (parts) { + var amount = parseInt(parts[1]); + var unit = parts[2]; + return 'now() - ' + amount + unit; + } + date = dateMath.parse(date, roundUp); + } + return (date.valueOf() / 1000).toFixed(0) + 's'; + } +} + diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index d932e7dd8d7..6eb2d84aa49 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -175,7 +175,7 @@ export default class InfluxQuery { } if (!target.measurement) { - throw "Metric measurement is missing"; + throw {message: "Metric measurement is missing"}; } var query = 'SELECT '; diff --git a/public/app/plugins/datasource/influxdb/influx_series.js b/public/app/plugins/datasource/influxdb/influx_series.js index 86f018ee11d..19e20390fd2 100644 --- a/public/app/plugins/datasource/influxdb/influx_series.js +++ b/public/app/plugins/datasource/influxdb/influx_series.js @@ -133,14 +133,18 @@ function (_, TableModel) { if (series.values) { for (i = 0; i < series.values.length; i++) { var values = series.values[i]; + var reordered = [values[0]]; if (series.tags) { for (var key in series.tags) { if (series.tags.hasOwnProperty(key)) { - values.splice(1, 0, series.tags[key]); + reordered.push(series.tags[key]); } } } - table.rows.push(values); + for (j = 1; j < values.length; j++) { + reordered.push(values[j]); + } + table.rows.push(reordered); } } }); diff --git a/public/app/plugins/datasource/influxdb/module.js b/public/app/plugins/datasource/influxdb/module.js deleted file mode 100644 index b7f266f9f89..00000000000 --- a/public/app/plugins/datasource/influxdb/module.js +++ /dev/null @@ -1,30 +0,0 @@ -define([ - './datasource', -], -function (InfluxDatasource) { - 'use strict'; - - function influxMetricsQueryEditor() { - return {controller: 'InfluxQueryCtrl', templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.editor.html'}; - } - - function influxMetricsQueryOptions() { - return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.options.html'}; - } - - function influxAnnotationsQueryEditor() { - return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html'}; - } - - function influxConfigView() { - return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/config.html'}; - } - - return { - Datasource: InfluxDatasource, - metricsQueryEditor: influxMetricsQueryEditor, - metricsQueryOptions: influxMetricsQueryOptions, - annotationsQueryEditor: influxAnnotationsQueryEditor, - configView: influxConfigView, - }; -}); diff --git a/public/app/plugins/datasource/influxdb/module.ts b/public/app/plugins/datasource/influxdb/module.ts new file mode 100644 index 00000000000..fc19413f18d --- /dev/null +++ b/public/app/plugins/datasource/influxdb/module.ts @@ -0,0 +1,24 @@ +import {InfluxDatasource} from './datasource'; +import {InfluxQueryCtrl} from './query_ctrl'; + +class InfluxConfigCtrl { + static templateUrl = 'public/app/plugins/datasource/influxdb/partials/config.html'; +} + +class InfluxQueryOptionsCtrl { + static templateUrl = 'public/app/plugins/datasource/influxdb/partials/query.options.html'; +} + +class InfluxAnnotationsQueryCtrl { + static templateUrl = 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html'; +} + +export { + InfluxDatasource as Datasource, + InfluxQueryCtrl as QueryCtrl, + InfluxConfigCtrl as ConfigCtrl, + InfluxQueryOptionsCtrl as QueryOptionsCtrl, + InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl, +}; + + diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 9d1c967c668..a371fc9a7c8 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -1,4 +1,5 @@ - + +

          InfluxDB Details

          @@ -8,7 +9,7 @@ Database
        • - +
        @@ -19,13 +20,13 @@ User
      • - +
      • Password
      • - +
      diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 3322be6c423..140f298f458 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,119 +1,73 @@ -
      -
      - - -
        -
      • - {{target.refId}} -
      • -
      • - - - -
      • -
      - -