mirror of
https://github.com/grafana/grafana.git
synced 2026-01-08 21:22:59 +08:00
Compare commits
68 Commits
v4.1.0-bet
...
v4.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b510b7581 | ||
|
|
0c7d101a97 | ||
|
|
4c192714e4 | ||
|
|
d01cf16dea | ||
|
|
9f0b6f5aca | ||
|
|
46c1f72c03 | ||
|
|
ec033840bc | ||
|
|
f09c114019 | ||
|
|
225bf094b2 | ||
|
|
98b648dabe | ||
|
|
da7f1f29de | ||
|
|
0719a66d47 | ||
|
|
65ac0bc68b | ||
|
|
8862ee1ae6 | ||
|
|
3205d1e754 | ||
|
|
eac75ec303 | ||
|
|
45d2e70ba7 | ||
|
|
c472054f21 | ||
|
|
e61d049623 | ||
|
|
7433e8c58f | ||
|
|
20b7c2f2e2 | ||
|
|
293fd93e6c | ||
|
|
6960fb46c2 | ||
|
|
813232315e | ||
|
|
63f6e86e46 | ||
|
|
658d39944b | ||
|
|
b97e784555 | ||
|
|
df1e90c723 | ||
|
|
36929c2a92 | ||
|
|
65057212e7 | ||
|
|
d8ebebc612 | ||
|
|
9a3e51894b | ||
|
|
9dc42648cf | ||
|
|
2e7d222f6e | ||
|
|
d9131be0a5 | ||
|
|
2be5ee0bd5 | ||
|
|
083a42942f | ||
|
|
8030b56bba | ||
|
|
9855ea8c6c | ||
|
|
08bad530dc | ||
|
|
8ed8922525 | ||
|
|
c97131d1a0 | ||
|
|
48b57afe84 | ||
|
|
797e2ea25d | ||
|
|
716d6473b7 | ||
|
|
d662961ebe | ||
|
|
27d83f414e | ||
|
|
f98e07e012 | ||
|
|
fef511d403 | ||
|
|
c18741c605 | ||
|
|
02bf83b37e | ||
|
|
8859eeb151 | ||
|
|
a7b3ef06a8 | ||
|
|
70f37651be | ||
|
|
1ab66ac5d7 | ||
|
|
cb21b20905 | ||
|
|
4e306590f8 | ||
|
|
8cef4cc74e | ||
|
|
c3005397f5 | ||
|
|
c18b410104 | ||
|
|
5440804109 | ||
|
|
1c8865e702 | ||
|
|
1507d6c872 | ||
|
|
4c5bdd9da4 | ||
|
|
48fbd7e134 | ||
|
|
afa5e9507a | ||
|
|
13c4ce68ee | ||
|
|
fa01022494 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,4 +1,17 @@
|
||||
# 4.1-beta (unreleased)
|
||||
# 4.2.0 (unreleased)
|
||||
|
||||
# 4.0.0 (unreleased)
|
||||
|
||||
### Bugfixes
|
||||
* **Server side PNG rendering**: Fixed issue with y-axis label rotation in phantomjs rendered images [#6924](https://github.com/grafana/grafana/issues/6924)
|
||||
* **Graph**: Fixed centering of y-axis label [#7099](https://github.com/grafana/grafana/issues/7099)
|
||||
* **Graph**: Fixed graph legend table mode and always visible scrollbar [#6828](https://github.com/grafana/grafana/issues/6828)
|
||||
* **Templating**: Fixed template variable value groups/tags feature [#6752](https://github.com/grafana/grafana/issues/6752)
|
||||
|
||||
## Enhancements
|
||||
* **Elasticsearch**: Added support for all moving average options [#7154](https://github.com/grafana/grafana/pull/7154), thx [@vaibhavinbayarea](https://github.com/vaibhavinbayarea)
|
||||
|
||||
# 4.1-beta1 (2016-12-21)
|
||||
|
||||
### Enhancements
|
||||
* **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
|
||||
@@ -17,6 +30,7 @@
|
||||
* **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
|
||||
* **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
|
||||
* **Alerting**: Add api endpoint for pausing all alerts. [#6589](https://github.com/grafana/grafana/issues/6589)
|
||||
* **Panel**: Added help text for panels. [#4079](https://github.com/grafana/grafana/issues/4079), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||
|
||||
### Bugfixes
|
||||
* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
|
||||
@@ -25,6 +39,11 @@
|
||||
* **Notifications**: Remove html escaping the email subject. [#6905](https://github.com/grafana/grafana/issues/6905)
|
||||
* **Influxdb**: Fixes broken field dropdown when using template vars as measurement. [#6473](https://github.com/grafana/grafana/issues/6473)
|
||||
|
||||
# 4.0.3 (unreleased)
|
||||
|
||||
### Bugfixes
|
||||
* **Influxdb**: Handles time(auto) the same way as time($interval) [#6997](https://github.com/grafana/grafana/issues/6997)
|
||||
|
||||
# 4.0.2 (2016-12-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
2
Makefile
2
Makefile
@@ -4,7 +4,7 @@ deps-go:
|
||||
go run build.go setup
|
||||
|
||||
deps-js:
|
||||
npm install
|
||||
yarn install
|
||||
|
||||
deps: deps-go deps-js
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Grafana is an open source, feature rich metrics dashboard and graph editor for
|
||||
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||
|
||||

|
||||

|
||||
|
||||
- [Install instructions](http://docs.grafana.org/installation/)
|
||||
- [What's New in Grafana 2.0](http://docs.grafana.org/guides/whats-new-in-v2/)
|
||||
@@ -18,6 +18,7 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||
- [What's New in Grafana 2.5](http://docs.grafana.org/guides/whats-new-in-v2-5/)
|
||||
- [What's New in Grafana 3.0](http://docs.grafana.org/guides/whats-new-in-v3/)
|
||||
- [What's New in Grafana 4.0](http://docs.grafana.org/guides/whats-new-in-v4/)
|
||||
- [What's New in Grafana 4.1 beta](http://docs.grafana.org/guides/whats-new-in-v4-1/)
|
||||
|
||||
## Features
|
||||
### Graphite Target Editor
|
||||
@@ -113,7 +114,8 @@ To build less to css for the frontend you will need a recent version of **node (
|
||||
npm (v2.5.0) and grunt (v0.4.5). Run the following:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ os: Windows Server 2012 R2
|
||||
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
|
||||
environment:
|
||||
nodejs_version: "5"
|
||||
nodejs_version: "6"
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
# install nodejs and npm
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install
|
||||
- npm install -g yarn
|
||||
- yarn install
|
||||
- npm install -g grunt-cli
|
||||
# install gcc (needed for sqlite3)
|
||||
- choco install -y --limit-output mingw
|
||||
|
||||
16
circle.yml
16
circle.yml
@@ -1,6 +1,6 @@
|
||||
machine:
|
||||
node:
|
||||
version: 5.11.1
|
||||
version: 6.9.2
|
||||
environment:
|
||||
GOPATH: "/home/ubuntu/.go_workspace"
|
||||
ORG_PATH: "github.com/grafana"
|
||||
@@ -22,10 +22,10 @@ test:
|
||||
override:
|
||||
- bash scripts/circle-test.sh
|
||||
|
||||
deployment:
|
||||
master:
|
||||
branch: master
|
||||
owner: grafana
|
||||
commands:
|
||||
- ./scripts/trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
- ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN}
|
||||
# deployment:
|
||||
# master:
|
||||
# branch: master
|
||||
# owner: grafana
|
||||
# commands:
|
||||
# - ./scripts/trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
# - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN}
|
||||
|
||||
@@ -19,6 +19,7 @@ ssl_skip_verify = false
|
||||
# Search user bind dn
|
||||
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# Search user bind password
|
||||
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
|
||||
bind_password = 'grafana'
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
|
||||
70
docs/sources/guides/whats-new-in-v4-1.md
Normal file
70
docs/sources/guides/whats-new-in-v4-1.md
Normal file
@@ -0,0 +1,70 @@
|
||||
+++
|
||||
title = "What's New in Grafana v4.1 beta"
|
||||
description = "Feature & improvement highlights for Grafana v4.1 beta"
|
||||
keywords = ["grafana", "new", "documentation", "4.1.0-beta1"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 4.1 beta"
|
||||
identifier = "v4.1"
|
||||
parent = "whatsnew"
|
||||
weight = -1
|
||||
+++
|
||||
|
||||
|
||||
## Whats new in Grafana v4.1 beta
|
||||
- **Graph**: Support for shared tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
|
||||
- **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411), thx [@ichekrygin](https://github.com/ichekrygin)
|
||||
- **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)
|
||||
- **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
|
||||
- **Elasticsearch**: Added support for Elasticsearch 5.x [#5740](https://github.com/grafana/grafana/issues/5740), thx [@lpic10](https://github.com/lpic10)
|
||||
- **Panel**: Added help text for panels. [#4079](https://github.com/grafana/grafana/issues/4079), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||
- [Full changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md)
|
||||
|
||||
### Shared tooltip
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/shared_tooltip.gif" caption="Shared tooltip" >}}
|
||||
|
||||
Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it.
|
||||
You can enable/disable the shared tooltip from the dashboard settings menu or cycle between default, shared tooltip and shared crosshair by pressing `CTRL + O` or `CMD + O`.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Help text for panel
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/helptext_for_panel_settings.png" caption="Hovering help text" >}}
|
||||
|
||||
You can set a help text in the general tab on any panel. The help text is using Markdown to enable better formating and linking to other sites that can provide more information.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/helptext_hover.png" caption="Hovering help text" >}}
|
||||
|
||||
Panels with a help text available have a little indicator in the top left corner. You can show the help text by hovering the icon.
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
### Easier Cloudwatch configuration
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/cloudwatch_settings.png" caption="Cloudwatch configuration" >}}
|
||||
|
||||
In Grafana 4.1.0 you can configure your Cloudwatch data source with `access key` and `secret key` directly in the data source configuration page.
|
||||
This enables people to use the Cloudwatch data source without having access to the filesystem where Grafana is running.
|
||||
|
||||
Once the `access key` and `secret key` have been saved the user will no longer be able to view them.
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Upgrade & Breaking changes
|
||||
|
||||
Elasticsearch 1.x is no longer supported. Please upgrade to Elasticsearch 2.x or 5.x. Otherwise Grafana 4.1.0-beta1 contains no breaking changes.
|
||||
|
||||
## Changelog
|
||||
|
||||
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
|
||||
of new features, changes, and bug fixes.
|
||||
|
||||
## Download
|
||||
|
||||
Head to [v4.1 download page](/download/4_1_0/) for download links & instructions.
|
||||
|
||||
## Thanks
|
||||
A big thanks to all the Grafana users who contribute by submitting PRs, bug reports & feedback!
|
||||
216
docs/sources/http_api/alerting.md
Normal file
216
docs/sources/http_api/alerting.md
Normal file
@@ -0,0 +1,216 @@
|
||||
+++
|
||||
title = "Alerting HTTP API "
|
||||
description = "Grafana Alerting HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "alerting"]
|
||||
aliases = ["/http_api/alerting/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Alerting"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
|
||||
# Alerting API
|
||||
|
||||
You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert.
|
||||
To create new alerts or modify them you need to update the dashboard json that contains the alerts.
|
||||
|
||||
This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
## Get alerts
|
||||
|
||||
`GET /api/alerts/`
|
||||
|
||||
**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,
|
||||
"dashboardId": 1,
|
||||
"panelId": 1,
|
||||
"name": "fire place sensor",
|
||||
"message": "Someone is trying to break in through the fire place",
|
||||
"state": "alerting",
|
||||
"newStateDate": "2016-12-25",
|
||||
"executionError": "",
|
||||
"dashboardUri": "http://grafana.com/dashboard/db/sensors"
|
||||
}
|
||||
]
|
||||
|
||||
## Get one alert
|
||||
|
||||
`GET /api/alerts/:id`
|
||||
|
||||
**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,
|
||||
"dashboardId": 1,
|
||||
"panelId": 1,
|
||||
"name": "fire place sensor",
|
||||
"message": "Someone is trying to break in through the fire place",
|
||||
"state": "alerting",
|
||||
"newStateDate": "2016-12-25",
|
||||
"executionError": "",
|
||||
"dashboardUri": "http://grafana.com/dashboard/db/sensors"
|
||||
}
|
||||
|
||||
|
||||
## Pause alert
|
||||
|
||||
`POST /api/alerts/:id/pause`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/org HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"alertId": 1,
|
||||
"paused: true
|
||||
}
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"alertId": 1,
|
||||
"state": "Paused",
|
||||
"message": "alert paused"
|
||||
}
|
||||
|
||||
## Get alert notifications
|
||||
|
||||
`GET /api/alert-notifications`
|
||||
|
||||
**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": "Team A",
|
||||
"type": "email",
|
||||
"isDefault": true,
|
||||
"created": "2017-01-01 12:45",
|
||||
"updated": "2017-01-01 12:45"
|
||||
}
|
||||
|
||||
## Create alert notification
|
||||
|
||||
`POST /api/alerts-notifications`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/org HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"name": "new alert notification", //Required
|
||||
"type": "email", //Required
|
||||
"isDefault": false,
|
||||
"settings": {
|
||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "new alert notification",
|
||||
"type": "email",
|
||||
"isDefault": false,
|
||||
"settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
|
||||
## Update alert notification
|
||||
|
||||
`PUT /api/alerts-notifications/1`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/org HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "new alert notification", //Required
|
||||
"type": "email", //Required
|
||||
"isDefault": false,
|
||||
"settings": {
|
||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "new alert notification",
|
||||
"type": "email",
|
||||
"isDefault": false,
|
||||
"settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
|
||||
## Delete alert notification
|
||||
|
||||
`DELETE /api/alerts-notifications/1`
|
||||
|
||||
**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
|
||||
{
|
||||
"message": "Notification deleted"
|
||||
}
|
||||
@@ -158,7 +158,7 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":1,"message":"Datasource added"}
|
||||
{"id":1,"message":"Datasource added", "name": "test_datasource"}
|
||||
|
||||
## Update an existing data source
|
||||
|
||||
@@ -193,7 +193,7 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Datasource updated"}
|
||||
{"message":"Datasource updated", "id": 1, "name": "test_datasource"}
|
||||
|
||||
## Delete an existing data source
|
||||
|
||||
|
||||
@@ -144,6 +144,10 @@ Grafana needs a database to store users and dashboards (and other
|
||||
things). By default it is configured to use `sqlite3` which is an
|
||||
embedded database (included in the main Grafana binary).
|
||||
|
||||
### url
|
||||
Use either URL or or the other fields below to configure the database
|
||||
Example: `mysql://user:secret@host:port/database`
|
||||
|
||||
### type
|
||||
|
||||
Either `mysql`, `postgres` or `sqlite3`, it's your choice.
|
||||
@@ -244,7 +248,10 @@ organization to be created for that new user.
|
||||
|
||||
The role new users will be assigned for the main organization (if the
|
||||
above setting is set to true). Defaults to `Viewer`, other valid
|
||||
options are `Admin` and `Editor` and `Read-Only Editor`.
|
||||
options are `Admin` and `Editor` and `Read Only Editor`. e.g. :
|
||||
|
||||
`auto_assign_org_role = Read Only Editor`
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ weight = 1
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for Debian-based Linux | [4.0.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb)
|
||||
Latest beta for Debian-based Linux | [4.1.0-beta1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.0-1482230757beta1_amd64.deb)
|
||||
|
||||
## Install Stable
|
||||
|
||||
@@ -25,6 +26,14 @@ $ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb
|
||||
```
|
||||
|
||||
## Install Latest Beta
|
||||
|
||||
```
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.0-1482230757beta1_amd64.deb
|
||||
$ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_4.1.0-1482230757beta1_amd64.deb
|
||||
```
|
||||
|
||||
## APT Repository
|
||||
|
||||
Add the following line to your `/etc/apt/sources.list` file.
|
||||
|
||||
@@ -43,6 +43,7 @@ ssl_skip_verify = false
|
||||
# Search user bind dn
|
||||
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# Search user bind password
|
||||
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
|
||||
bind_password = 'grafana'
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
|
||||
@@ -16,6 +16,7 @@ weight = 2
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm)
|
||||
Latest beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.1.0-beta1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.0-1482230757beta1.x86_64.rpm)
|
||||
|
||||
## Install Stable
|
||||
|
||||
@@ -34,6 +35,21 @@ Or install manually using `rpm`.
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-4.0.2-1481203731.x86_64.rpm
|
||||
|
||||
## Or Install Latest Beta
|
||||
|
||||
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.0-1482230757beta1.x86_64.rpm
|
||||
|
||||
Or install manually using `rpm`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-4.1.0-1482230757beta1.x86_64.rpm
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-4.1.0-1482230757beta1.x86_64.rpm
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
|
||||
|
||||
@@ -14,6 +14,7 @@ weight = 3
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Latest stable package for Windows | [grafana.4.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2.windows-x64.zip)
|
||||
Latest beta package for Windows | [grafana.4.1.0-beta1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.0-beta1.windows-x64.zip)
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ To build less to css for the frontend you will need a recent version of node (v0
|
||||
npm (v2.5.0) and grunt (v0.4.5). Run the following:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
npm install -g grunt-cli
|
||||
grunt
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "4.1.0-beta1",
|
||||
"version": "4.1.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@@ -48,7 +48,7 @@
|
||||
"karma-phantomjs-launcher": "1.0.2",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mocha": "3.2.0",
|
||||
"phantomjs-prebuilt": "^2.1.13",
|
||||
"phantomjs-prebuilt": "^2.1.14",
|
||||
"reflect-metadata": "0.1.8",
|
||||
"rxjs": "^5.0.0-rc.5",
|
||||
"sass-lint": "^1.10.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
deb_ver=4.0.2-1481203731
|
||||
rpm_ver=4.0.2-1481203731
|
||||
deb_ver=4.1.1-1484211277
|
||||
rpm_ver=4.1.1-1484211277
|
||||
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
|
||||
|
||||
4
packaging/publish/publish_testing.sh
Normal file → Executable file
4
packaging/publish/publish_testing.sh
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
deb_ver=4.0.2-1481203731
|
||||
rpm_ver=4.0.2-1481203731
|
||||
deb_ver=4.1.0-1482230757beta1
|
||||
rpm_ver=4.1.0-1482230757beta1
|
||||
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ func GetAlerts(c *middleware.Context) Response {
|
||||
Name: alert.Name,
|
||||
Message: alert.Message,
|
||||
State: alert.State,
|
||||
EvalDate: alert.EvalDate,
|
||||
NewStateDate: alert.NewStateDate,
|
||||
ExecutionError: alert.ExecutionError,
|
||||
})
|
||||
|
||||
@@ -124,6 +124,7 @@ func Register(r *macaron.Macaron) {
|
||||
// users (admin permission required)
|
||||
r.Group("/users", func() {
|
||||
r.Get("/", wrap(SearchUsers))
|
||||
r.Get("/search", wrap(SearchUsersWithPaging))
|
||||
r.Get("/:id", wrap(GetUserById))
|
||||
r.Get("/:id/orgs", wrap(GetUserOrgList))
|
||||
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
|
||||
@@ -194,7 +195,7 @@ func Register(r *macaron.Macaron) {
|
||||
|
||||
// Data sources
|
||||
r.Group("/datasources", func() {
|
||||
r.Get("/", GetDataSources)
|
||||
r.Get("/", wrap(GetDataSources))
|
||||
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
|
||||
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
|
||||
r.Delete("/:id", DeleteDataSource)
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -90,7 +89,7 @@ type cache struct {
|
||||
var awsCredentialCache map[string]cache = make(map[string]cache)
|
||||
var credentialCacheLock sync.RWMutex
|
||||
|
||||
func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
|
||||
credentialCacheLock.RLock()
|
||||
if _, ok := awsCredentialCache[cacheKey]; ok {
|
||||
@@ -98,7 +97,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
|
||||
result := awsCredentialCache[cacheKey].credential
|
||||
credentialCacheLock.RUnlock()
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
credentialCacheLock.RUnlock()
|
||||
@@ -130,8 +129,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
svc := sts.New(session.New(stsConfig), stsConfig)
|
||||
resp, err := svc.AssumeRole(params)
|
||||
if err != nil {
|
||||
// ignore
|
||||
log.Error(3, "CloudWatch: Failed to assume role", err)
|
||||
return nil, err
|
||||
}
|
||||
if resp.Credentials != nil {
|
||||
accessKeyId = *resp.Credentials.AccessKeyId
|
||||
@@ -165,19 +163,28 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
}
|
||||
credentialCacheLock.Unlock()
|
||||
|
||||
return creds
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func getAwsConfig(req *cwRequest) *aws.Config {
|
||||
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
|
||||
creds, err := getCredentials(req.GetDatasourceInfo())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(req.Region),
|
||||
Credentials: getCredentials(req.GetDatasourceInfo()),
|
||||
Credentials: creds,
|
||||
}
|
||||
return cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@@ -220,7 +227,11 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@@ -239,7 +250,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err := svc.ListMetricsPages(params,
|
||||
err = svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
@@ -257,7 +268,11 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@@ -296,7 +311,11 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@@ -336,7 +355,11 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@@ -368,7 +391,11 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@@ -388,7 +415,7 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
var resp ec2.DescribeInstancesOutput
|
||||
err := svc.DescribeInstancesPages(params,
|
||||
err = svc.DescribeInstancesPages(params,
|
||||
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
|
||||
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
|
||||
for _, reservation := range reservations {
|
||||
|
||||
@@ -140,8 +140,8 @@ func init() {
|
||||
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
|
||||
func handleGetRegions(req *cwRequest, c *middleware.Context) {
|
||||
regions := []string{
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-1",
|
||||
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
|
||||
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
|
||||
}
|
||||
|
||||
result := []interface{}{}
|
||||
@@ -248,9 +248,13 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||
creds, err := getCredentials(cwData)
|
||||
if err != nil {
|
||||
return cloudwatch.ListMetricsOutput{}, err
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(cwData.Region),
|
||||
Credentials: getCredentials(cwData),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
@@ -260,7 +264,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
}
|
||||
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err := svc.ListMetricsPages(params,
|
||||
err = svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
|
||||
@@ -11,12 +11,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func GetDataSources(c *middleware.Context) {
|
||||
func GetDataSources(c *middleware.Context) Response {
|
||||
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
c.JsonApiErr(500, "Failed to query datasources", err)
|
||||
return
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
@@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) {
|
||||
}
|
||||
|
||||
sort.Sort(result)
|
||||
c.JSON(200, result)
|
||||
|
||||
return Json(200, &result)
|
||||
}
|
||||
|
||||
func GetDataSourceById(c *middleware.Context) Response {
|
||||
@@ -100,7 +100,7 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
|
||||
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name})
|
||||
}
|
||||
|
||||
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
|
||||
@@ -117,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
|
||||
return ApiError(500, "Failed to update datasource", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{"message": "Datasource updated"})
|
||||
return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name})
|
||||
}
|
||||
|
||||
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
|
||||
|
||||
132
pkg/api/datasources_test.go
Normal file
132
pkg/api/datasources_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
const (
|
||||
TestOrgID = 1
|
||||
TestUserID = 1
|
||||
)
|
||||
|
||||
func TestDataSourcesProxy(t *testing.T) {
|
||||
Convey("Given a user is logged in", t, func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) {
|
||||
|
||||
// Stubs the database query
|
||||
bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error {
|
||||
So(query.OrgId, ShouldEqual, TestOrgID)
|
||||
query.Result = []*models.DataSource{
|
||||
{Name: "mmm"},
|
||||
{Name: "ZZZ"},
|
||||
{Name: "BBB"},
|
||||
{Name: "aaa"},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// handler func being tested
|
||||
sc.handlerFunc = GetDataSources
|
||||
sc.fakeReq("GET", "/api/datasources").exec()
|
||||
|
||||
respJSON := []map[string]interface{}{}
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&respJSON)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() {
|
||||
So(respJSON[0]["name"], ShouldEqual, "aaa")
|
||||
So(respJSON[1]["name"], ShouldEqual, "BBB")
|
||||
So(respJSON[2]["name"], ShouldEqual, "mmm")
|
||||
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{
|
||||
url: url,
|
||||
}
|
||||
viewsPath, _ := filepath.Abs("../../public/views")
|
||||
|
||||
sc.m = macaron.New()
|
||||
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: viewsPath,
|
||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||
}))
|
||||
|
||||
sc.m.Use(middleware.GetContextHandler())
|
||||
sc.m.Use(middleware.Sessioner(&session.Options{}))
|
||||
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = models.ROLE_EDITOR
|
||||
if sc.handlerFunc != nil {
|
||||
return sc.handlerFunc(sc.context)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get(url, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *middleware.Context
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) exec() {
|
||||
sc.m.ServeHTTP(sc.resp, sc.req)
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *middleware.Context) Response
|
||||
@@ -91,7 +91,7 @@ func (slice DataSourceList) Len() int {
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Less(i, j int) bool {
|
||||
return slice[i].Name < slice[j].Name
|
||||
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Swap(i, j int) {
|
||||
|
||||
@@ -186,14 +186,46 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
|
||||
|
||||
// GET /api/users
|
||||
func SearchUsers(c *middleware.Context) Response {
|
||||
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result.Users)
|
||||
}
|
||||
|
||||
// GET /api/paged-users
|
||||
func SearchUsersWithPaging(c *middleware.Context) Response {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
|
||||
perPage := c.QueryInt("perpage")
|
||||
if perPage <= 0 {
|
||||
perPage = 1000
|
||||
}
|
||||
page := c.QueryInt("page")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
query := &m.SearchUsersQuery{Query: "", Page: page, Limit: perPage}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func SetHelpFlag(c *middleware.Context) Response {
|
||||
flag := c.ParamsInt64(":id")
|
||||
|
||||
|
||||
109
pkg/api/user_test.go
Normal file
109
pkg/api/user_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUserApiEndpoint(t *testing.T) {
|
||||
Convey("Given a user is logged in", t, func() {
|
||||
mockResult := models.SearchUserQueryResult{
|
||||
Users: []*models.UserSearchHitDTO{
|
||||
{Name: "user1"},
|
||||
{Name: "user2"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
|
||||
loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsers
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
So(len(respJSON.MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsers
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsersWithPaging
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
|
||||
So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsersWithPaging
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -73,7 +73,6 @@ type Alert struct {
|
||||
Frequency int64
|
||||
|
||||
EvalData *simplejson.Json
|
||||
EvalDate time.Time
|
||||
NewStateDate time.Time
|
||||
StateChanges int
|
||||
|
||||
|
||||
@@ -130,7 +130,14 @@ type SearchUsersQuery struct {
|
||||
Page int
|
||||
Limit int
|
||||
|
||||
Result []*UserSearchHitDTO
|
||||
Result SearchUserQueryResult
|
||||
}
|
||||
|
||||
type SearchUserQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Users []*UserSearchHitDTO `json:"users"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
type GetUserOrgListQuery struct {
|
||||
|
||||
@@ -22,7 +22,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
return &WebhookNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
User: model.Settings.Get("user").MustString(),
|
||||
User: model.Settings.Get("username").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
|
||||
log: log.New("alerting.notifier.webhook"),
|
||||
|
||||
@@ -133,10 +133,16 @@ func UpdateOrg(cmd *m.UpdateOrgCommand) error {
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
if _, err := sess.Id(cmd.OrgId).Update(&org); err != nil {
|
||||
affectedRows, err := sess.Id(cmd.OrgId).Update(&org)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
return m.ErrOrgNotFound
|
||||
}
|
||||
|
||||
sess.publishAfterCommit(&events.OrgUpdated{
|
||||
Timestamp: org.Updated,
|
||||
Id: org.Id,
|
||||
|
||||
@@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) {
|
||||
err := SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result[0].Email, ShouldEqual, "ac1@test.com")
|
||||
So(query.Result[1].Email, ShouldEqual, "ac2@test.com")
|
||||
So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com")
|
||||
So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com")
|
||||
})
|
||||
|
||||
Convey("Given an added org user", func() {
|
||||
|
||||
@@ -344,12 +344,21 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||
}
|
||||
|
||||
func SearchUsers(query *m.SearchUsersQuery) error {
|
||||
query.Result = make([]*m.UserSearchHitDTO, 0)
|
||||
query.Result = m.SearchUserQueryResult{
|
||||
Users: make([]*m.UserSearchHitDTO, 0),
|
||||
}
|
||||
sess := x.Table("user")
|
||||
sess.Where("email LIKE ?", query.Query+"%")
|
||||
sess.Limit(query.Limit, query.Limit*query.Page)
|
||||
offset := query.Limit * (query.Page - 1)
|
||||
sess.Limit(query.Limit, offset)
|
||||
sess.Cols("id", "email", "name", "login", "is_admin")
|
||||
err := sess.Find(&query.Result)
|
||||
if err := sess.Find(&query.Result.Users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := m.User{}
|
||||
count, err := x.Count(&user)
|
||||
query.Result.TotalCount = count
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
45
pkg/services/sqlstore/user_test.go
Normal file
45
pkg/services/sqlstore/user_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestUserDataAccess(t *testing.T) {
|
||||
|
||||
Convey("Testing DB", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
err = CreateUser(&models.CreateUserCommand{
|
||||
Email: fmt.Sprint("user", i, "@test.com"),
|
||||
Name: fmt.Sprint("user", i),
|
||||
Login: fmt.Sprint("user", i),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("Can return the first page of users and a total count", func() {
|
||||
query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 3)
|
||||
So(query.Result.TotalCount, ShouldEqual, 5)
|
||||
})
|
||||
|
||||
Convey("Can return the second page of users and a total count", func() {
|
||||
query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 2)
|
||||
So(query.Result.TotalCount, ShouldEqual, 5)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) string {
|
||||
|
||||
func functionRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string {
|
||||
for i, param := range part.Params {
|
||||
if param == "$interval" {
|
||||
if param == "$interval" || param == "auto" {
|
||||
if query.Interval != "" {
|
||||
part.Params[i] = getDefinedInterval(query, queryContext)
|
||||
} else {
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
|
||||
So(res, ShouldEqual, "bottom(value, 3)")
|
||||
})
|
||||
|
||||
Convey("render time", func() {
|
||||
Convey("render time with $interval", func() {
|
||||
part, err := NewQueryPart("time", []string{"$interval"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -45,6 +45,14 @@ func TestInfluxdbQueryPart(t *testing.T) {
|
||||
So(res, ShouldEqual, "time(200ms)")
|
||||
})
|
||||
|
||||
Convey("render time with auto", func() {
|
||||
part, err := NewQueryPart("time", []string{"auto"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res := part.Render(query, queryContext, "")
|
||||
So(res, ShouldEqual, "time(200ms)")
|
||||
})
|
||||
|
||||
Convey("render time interval >10s", func() {
|
||||
part, err := NewQueryPart("time", []string{"$interval"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -75,7 +75,7 @@ function (angular, _, coreModule) {
|
||||
tag.selected = !tag.selected;
|
||||
var tagValuesPromise;
|
||||
if (!tag.values) {
|
||||
tagValuesPromise = vm.getValuesForTag({tagKey: tag.text});
|
||||
tagValuesPromise = vm.variable.getValuesForTag(tag.text);
|
||||
} else {
|
||||
tagValuesPromise = $q.when(tag.values);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ function (angular, _, coreModule) {
|
||||
|
||||
coreModule.default.directive('valueSelectDropdown', function($compile, $window, $timeout, $rootScope) {
|
||||
return {
|
||||
scope: { variable: "=", onUpdated: "&", getValuesForTag: "&" },
|
||||
scope: { variable: "=", onUpdated: "&"},
|
||||
templateUrl: 'public/app/partials/valueSelectDropdown.html',
|
||||
controller: 'ValueSelectDropdownCtrl',
|
||||
controllerAs: 'vm',
|
||||
|
||||
@@ -113,6 +113,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
.when('/admin/users', {
|
||||
templateUrl: 'public/app/features/admin/partials/users.html',
|
||||
controller : 'AdminListUsersCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadAdminBundle,
|
||||
})
|
||||
.when('/admin/users/create', {
|
||||
|
||||
@@ -97,10 +97,19 @@ function (angular, _, coreModule, config) {
|
||||
}
|
||||
|
||||
metricSources.sort(function(a, b) {
|
||||
if (a.meta.builtIn || a.name > b.name) {
|
||||
if (a.meta.builtIn) {
|
||||
return 1;
|
||||
}
|
||||
if (a.name < b.name) {
|
||||
|
||||
if (b.meta.builtIn) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -399,6 +399,7 @@ function($, _) {
|
||||
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
|
||||
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
|
||||
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
|
||||
kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽');
|
||||
|
||||
// Data (Binary)
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
@@ -701,6 +702,7 @@ function($, _) {
|
||||
{text: 'Pounds (£)', value: 'currencyGBP'},
|
||||
{text: 'Euro (€)', value: 'currencyEUR'},
|
||||
{text: 'Yen (¥)', value: 'currencyJPY'},
|
||||
{text: 'Rubles (₽)', value: 'currencyRUB'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function assignModelProperties(target, source, defaults) {
|
||||
export function assignModelProperties(target, source, defaults, removeDefaults?) {
|
||||
for (var key in defaults) {
|
||||
if (!defaults.hasOwnProperty(key)) {
|
||||
continue;
|
||||
|
||||
17
public/app/core/utils/sort_by_keys.ts
Normal file
17
public/app/core/utils/sort_by_keys.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function sortByKeys(input) {
|
||||
if (_.isArray(input)) {
|
||||
return input.map(sortByKeys);
|
||||
}
|
||||
|
||||
if (_.isPlainObject(input)) {
|
||||
var sortedObject = {};
|
||||
for (let key of _.keys(input).sort()) {
|
||||
sortedObject[key] = sortByKeys(input[key]);
|
||||
}
|
||||
return sortedObject;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import './adminListUsersCtrl';
|
||||
import AdminListUsersCtrl from './admin_list_users_ctrl';
|
||||
import './adminListOrgsCtrl';
|
||||
import './adminEditOrgCtrl';
|
||||
import './adminEditUserCtrl';
|
||||
@@ -37,3 +37,4 @@ export class AdminStatsCtrl {
|
||||
coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl);
|
||||
coreModule.controller('AdminHomeCtrl', AdminHomeCtrl);
|
||||
coreModule.controller('AdminStatsCtrl', AdminStatsCtrl);
|
||||
coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('AdminListUsersCtrl', function($scope, backendSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getUsers();
|
||||
};
|
||||
|
||||
$scope.getUsers = function() {
|
||||
backendSrv.get('/api/users').then(function(users) {
|
||||
$scope.users = users;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Do you want to delete ' + user.login + '?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: function() {
|
||||
backendSrv.delete('/api/admin/users/' + user.id).then(function() {
|
||||
$scope.getUsers();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
49
public/app/features/admin/admin_list_users_ctrl.ts
Normal file
49
public/app/features/admin/admin_list_users_ctrl.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
export default class AdminListUsersCtrl {
|
||||
users: any;
|
||||
pages = [];
|
||||
perPage = 1000;
|
||||
page = 1;
|
||||
totalPages: number;
|
||||
showPaging = false;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private backendSrv) {
|
||||
this.getUsers();
|
||||
}
|
||||
|
||||
getUsers() {
|
||||
this.backendSrv.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}`).then((result) => {
|
||||
this.users = result.users;
|
||||
this.page = result.page;
|
||||
this.perPage = result.perPage;
|
||||
this.totalPages = Math.ceil(result.totalCount / result.perPage);
|
||||
this.showPaging = this.totalPages > 1;
|
||||
this.pages = [];
|
||||
|
||||
for (var i = 1; i < this.totalPages+1; i++) {
|
||||
this.pages.push({ page: i, current: i === this.page});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigateToPage(page) {
|
||||
this.page = page.page;
|
||||
this.getUsers();
|
||||
}
|
||||
|
||||
deleteUser(user) {
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Do you want to delete ' + user.login + '?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
this.backendSrv.delete('/api/admin/users/' + user.id).then(() => {
|
||||
this.getUsers();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,62 @@
|
||||
<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
|
||||
<a href="admin/users" class="navbar-page-btn">
|
||||
<i class="icon-gf icon-gf-users"></i>
|
||||
Users
|
||||
</a>
|
||||
<a href="admin/users" class="navbar-page-btn">
|
||||
<i class="icon-gf icon-gf-users"></i>
|
||||
Users
|
||||
</a>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>Users</h1>
|
||||
<div class="page-header">
|
||||
<h1>Users</h1>
|
||||
|
||||
<a class="btn btn-success" href="admin/users/create">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add new user
|
||||
</a>
|
||||
</div>
|
||||
<a class="btn btn-success" href="admin/users/create">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add new user
|
||||
</a>
|
||||
</div>
|
||||
<div class="admin-list-table">
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th style="white-space: nowrap">Grafana Admin</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in ctrl.users">
|
||||
<td>{{user.id}}</td>
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.isAdmin}}</td>
|
||||
<td class="text-right">
|
||||
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="ctrl.deleteUser(user)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th style="white-space: nowrap">Grafana Admin</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{user.id}}</td>
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.isAdmin}}</td>
|
||||
<td class="text-right">
|
||||
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-list-paging" ng-if="ctrl.showPaging">
|
||||
<ol>
|
||||
<li ng-repeat="page in ctrl.pages">
|
||||
<button
|
||||
class="btn btn-small"
|
||||
ng-class="{'btn-secondary': page.current, 'btn-inverse': !page.current}"
|
||||
ng-click="ctrl.navigateToPage(page)">{{page.page}}</button>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,18 +145,14 @@ export class DashboardExporter {
|
||||
}
|
||||
}
|
||||
|
||||
requires = _.map(requires, req => {
|
||||
return req;
|
||||
});
|
||||
|
||||
// make inputs and requires a top thing
|
||||
var newObj = {};
|
||||
newObj["__inputs"] = inputs;
|
||||
newObj["__requires"] = requires;
|
||||
newObj["__requires"] = _.sortBy(requires, ['id']);
|
||||
|
||||
_.defaults(newObj, saveModel);
|
||||
|
||||
return newObj;
|
||||
|
||||
}).catch(err => {
|
||||
console.log('Export failed:', err);
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import $ from 'jquery';
|
||||
|
||||
import {Emitter, contextSrv, appEvents} from 'app/core/core';
|
||||
import {DashboardRow} from './row/row_model';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
|
||||
export class DashboardModel {
|
||||
id: any;
|
||||
@@ -36,7 +37,7 @@ export class DashboardModel {
|
||||
events: any;
|
||||
editMode: boolean;
|
||||
|
||||
constructor(data, meta) {
|
||||
constructor(data, meta?) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
@@ -107,7 +108,10 @@ export class DashboardModel {
|
||||
this.rows = _.map(rows, row => row.getSaveModel());
|
||||
this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
|
||||
|
||||
// make clone
|
||||
var copy = $.extend(true, {}, this);
|
||||
// sort clone
|
||||
copy = sortByKeys(copy);
|
||||
|
||||
// restore properties
|
||||
this.events = events;
|
||||
|
||||
367
public/app/features/dashboard/specs/dashboard_model_specs.ts
Normal file
367
public/app/features/dashboard/specs/dashboard_model_specs.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import _ from 'lodash';
|
||||
import {DashboardModel} from '../model';
|
||||
|
||||
describe('DashboardModel', function() {
|
||||
|
||||
describe('when creating new dashboard model defaults only', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({}, {});
|
||||
});
|
||||
|
||||
it('should have title', function() {
|
||||
expect(model.title).to.be('No Title');
|
||||
});
|
||||
|
||||
it('should have meta', function() {
|
||||
expect(model.meta.canSave).to.be(true);
|
||||
expect(model.meta.canShare).to.be(true);
|
||||
});
|
||||
|
||||
it('should have default properties', function() {
|
||||
expect(model.rows.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getting next panel id', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
rows: [{ panels: [{ id: 5 }]}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should return max id + 1', function() {
|
||||
expect(model.getNextPanelId()).to.be(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSaveModelClone', function() {
|
||||
it('should sort keys', () => {
|
||||
var model = new DashboardModel({});
|
||||
var saveModel = model.getSaveModelClone();
|
||||
var keys = _.keys(saveModel);
|
||||
|
||||
expect(keys[0]).to.be('addEmptyRow');
|
||||
expect(keys[1]).to.be('addPanel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('row and panel manipulation', function() {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
dashboard = new DashboardModel({});
|
||||
});
|
||||
|
||||
it('adding default should split span in half', function() {
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel({span: 12});
|
||||
dashboard.rows[0].addPanel({span: 12});
|
||||
|
||||
expect(dashboard.rows[0].panels[0].span).to.be(6);
|
||||
expect(dashboard.rows[0].panels[1].span).to.be(6);
|
||||
});
|
||||
|
||||
it('duplicate panel should try to add it to same row', function() {
|
||||
var panel = { span: 4, attr: '123', id: 10 };
|
||||
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel(panel);
|
||||
dashboard.duplicatePanel(panel, dashboard.rows[0]);
|
||||
|
||||
expect(dashboard.rows[0].panels[0].span).to.be(4);
|
||||
expect(dashboard.rows[0].panels[1].span).to.be(4);
|
||||
expect(dashboard.rows[0].panels[1].attr).to.be('123');
|
||||
expect(dashboard.rows[0].panels[1].id).to.be(11);
|
||||
});
|
||||
|
||||
it('duplicate panel should remove repeat data', function() {
|
||||
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
|
||||
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel(panel);
|
||||
dashboard.duplicatePanel(panel, dashboard.rows[0]);
|
||||
|
||||
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
|
||||
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard with old schema', function() {
|
||||
var model;
|
||||
var graph;
|
||||
var singlestat;
|
||||
var table;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }},
|
||||
pulldowns: [
|
||||
{type: 'filtering', enable: true},
|
||||
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
panels: [
|
||||
{
|
||||
type: 'graph', legend: true, aliasYAxis: { test: 2 },
|
||||
y_formats: ['kbyte', 'ms'],
|
||||
grid: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
rightMin: 5,
|
||||
rightMax: 15,
|
||||
leftLogBase: 1,
|
||||
rightLogBase: 2,
|
||||
threshold1: 200,
|
||||
threshold2: 400,
|
||||
threshold1Color: 'yellow',
|
||||
threshold2Color: 'red',
|
||||
},
|
||||
leftYAxisLabel: 'left label',
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
|
||||
targets: [{refId: 'A'}, {}],
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
graph = model.rows[0].panels[0];
|
||||
singlestat = model.rows[0].panels[1];
|
||||
table = model.rows[0].panels[2];
|
||||
});
|
||||
|
||||
it('should have title', function() {
|
||||
expect(model.title).to.be('No Title');
|
||||
});
|
||||
|
||||
it('should have panel id', function() {
|
||||
expect(graph.id).to.be(1);
|
||||
});
|
||||
|
||||
it('should move time and filtering list', function() {
|
||||
expect(model.time.from).to.be('now-1d');
|
||||
expect(model.templating.list[0].allFormat).to.be('glob');
|
||||
});
|
||||
|
||||
it('graphite panel should change name too graph', function() {
|
||||
expect(graph.type).to.be('graph');
|
||||
});
|
||||
|
||||
it('single stat panel should have two thresholds', function() {
|
||||
expect(singlestat.thresholds).to.be('20,30');
|
||||
});
|
||||
|
||||
it('queries without refId should get it', function() {
|
||||
expect(graph.targets[1].refId).to.be('B');
|
||||
});
|
||||
|
||||
it('update legend setting', function() {
|
||||
expect(graph.legend.show).to.be(true);
|
||||
});
|
||||
|
||||
it('move aliasYAxis to series override', function() {
|
||||
expect(graph.seriesOverrides[0].alias).to.be("test");
|
||||
expect(graph.seriesOverrides[0].yaxis).to.be(2);
|
||||
});
|
||||
|
||||
it('should move pulldowns to new schema', function() {
|
||||
expect(model.annotations.list[0].name).to.be('old');
|
||||
});
|
||||
|
||||
it('table panel should only have two thresholds values', function() {
|
||||
expect(table.styles[0].thresholds[0]).to.be("20");
|
||||
expect(table.styles[0].thresholds[1]).to.be("30");
|
||||
expect(table.styles[1].thresholds[0]).to.be("200");
|
||||
expect(table.styles[1].thresholds[1]).to.be("300");
|
||||
});
|
||||
|
||||
it('graph grid to yaxes options', function() {
|
||||
expect(graph.yaxes[0].min).to.be(1);
|
||||
expect(graph.yaxes[0].max).to.be(10);
|
||||
expect(graph.yaxes[0].format).to.be('kbyte');
|
||||
expect(graph.yaxes[0].label).to.be('left label');
|
||||
expect(graph.yaxes[0].logBase).to.be(1);
|
||||
expect(graph.yaxes[1].min).to.be(5);
|
||||
expect(graph.yaxes[1].max).to.be(15);
|
||||
expect(graph.yaxes[1].format).to.be('ms');
|
||||
expect(graph.yaxes[1].logBase).to.be(2);
|
||||
|
||||
expect(graph.grid.rightMax).to.be(undefined);
|
||||
expect(graph.grid.rightLogBase).to.be(undefined);
|
||||
expect(graph.y_formats).to.be(undefined);
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', function() {
|
||||
expect(model.schemaVersion).to.be(14);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', function() {
|
||||
expect(graph.thresholds.length).to.be(2);
|
||||
expect(graph.thresholds[0].op).to.be('gt');
|
||||
expect(graph.thresholds[0].value).to.be(200);
|
||||
expect(graph.thresholds[0].fillColor).to.be('yellow');
|
||||
expect(graph.thresholds[1].value).to.be(400);
|
||||
expect(graph.thresholds[1].fillColor).to.be('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
annotations: {
|
||||
enable: true,
|
||||
},
|
||||
templating: {
|
||||
enable: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given editable false dashboard', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({editable: false});
|
||||
});
|
||||
|
||||
it('Should set meta canEdit and canSave to false', function() {
|
||||
expect(model.meta.canSave).to.be(false);
|
||||
expect(model.meta.canEdit).to.be(false);
|
||||
});
|
||||
|
||||
it('getSaveModelClone should remove meta', function() {
|
||||
var clone = model.getSaveModelClone();
|
||||
expect(clone.meta).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading dashboard with old influxdb query schema', function() {
|
||||
var model;
|
||||
var target;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
rows: [{
|
||||
panels: [{
|
||||
type: 'graph',
|
||||
grid: {},
|
||||
yaxes: [{}, {}],
|
||||
targets: [{
|
||||
"alias": "$tag_datacenter $tag_source $col",
|
||||
"column": "value",
|
||||
"measurement": "logins.count",
|
||||
"fields": [
|
||||
{
|
||||
"func": "mean",
|
||||
"name": "value",
|
||||
"mathExpr": "*2",
|
||||
"asExpr": "value"
|
||||
},
|
||||
{
|
||||
"name": "one-minute",
|
||||
"func": "mean",
|
||||
"mathExpr": "*3",
|
||||
"asExpr": "one-minute"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"fill": "previous",
|
||||
"function": "mean",
|
||||
"groupBy": [
|
||||
{
|
||||
"interval": "auto",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"type": "tag",
|
||||
"key": "datacenter"
|
||||
}
|
||||
],
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
target = model.rows[0].panels[0].targets[0];
|
||||
});
|
||||
|
||||
it('should update query schema', function() {
|
||||
expect(target.fields).to.be(undefined);
|
||||
expect(target.select.length).to.be(2);
|
||||
expect(target.select[0].length).to.be(4);
|
||||
expect(target.select[0][0].type).to.be('field');
|
||||
expect(target.select[0][1].type).to.be('mean');
|
||||
expect(target.select[0][2].type).to.be('math');
|
||||
expect(target.select[0][3].type).to.be('alias');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
annotations: {
|
||||
enable: true,
|
||||
},
|
||||
templating: {
|
||||
enable: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Formatting epoch timestamp when timezone is set as utc', function() {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
dashboard = new DashboardModel({timezone: 'utc'});
|
||||
});
|
||||
|
||||
it('Should format timestamp with second resolution by default', function() {
|
||||
expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30');
|
||||
});
|
||||
|
||||
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
|
||||
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30');
|
||||
});
|
||||
|
||||
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
|
||||
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,370 +9,4 @@ describe('dashboardSrv', function() {
|
||||
_dashboardSrv = new DashboardSrv({}, {}, {});
|
||||
});
|
||||
|
||||
describe('when creating new dashboard with defaults only', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({}, {});
|
||||
});
|
||||
|
||||
it('should have title', function() {
|
||||
expect(model.title).to.be('No Title');
|
||||
});
|
||||
|
||||
it('should have meta', function() {
|
||||
expect(model.meta.canSave).to.be(true);
|
||||
expect(model.meta.canShare).to.be(true);
|
||||
});
|
||||
|
||||
it('should have default properties', function() {
|
||||
expect(model.rows.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getting next panel id', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
rows: [{ panels: [{ id: 5 }]}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should return max id + 1', function() {
|
||||
expect(model.getNextPanelId()).to.be(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('row and panel manipulation', function() {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
dashboard = _dashboardSrv.create({});
|
||||
});
|
||||
|
||||
it('adding default should split span in half', function() {
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel({span: 12});
|
||||
dashboard.rows[0].addPanel({span: 12});
|
||||
|
||||
expect(dashboard.rows[0].panels[0].span).to.be(6);
|
||||
expect(dashboard.rows[0].panels[1].span).to.be(6);
|
||||
});
|
||||
|
||||
it('duplicate panel should try to add it to same row', function() {
|
||||
var panel = { span: 4, attr: '123', id: 10 };
|
||||
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel(panel);
|
||||
dashboard.duplicatePanel(panel, dashboard.rows[0]);
|
||||
|
||||
expect(dashboard.rows[0].panels[0].span).to.be(4);
|
||||
expect(dashboard.rows[0].panels[1].span).to.be(4);
|
||||
expect(dashboard.rows[0].panels[1].attr).to.be('123');
|
||||
expect(dashboard.rows[0].panels[1].id).to.be(11);
|
||||
});
|
||||
|
||||
it('duplicate panel should remove repeat data', function() {
|
||||
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
|
||||
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel(panel);
|
||||
dashboard.duplicatePanel(panel, dashboard.rows[0]);
|
||||
|
||||
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
|
||||
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard with editable false', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
editable: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should set editable false', function() {
|
||||
expect(model.editable).to.be(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard with old schema', function() {
|
||||
var model;
|
||||
var graph;
|
||||
var singlestat;
|
||||
var table;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }},
|
||||
pulldowns: [
|
||||
{type: 'filtering', enable: true},
|
||||
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
panels: [
|
||||
{
|
||||
type: 'graph', legend: true, aliasYAxis: { test: 2 },
|
||||
y_formats: ['kbyte', 'ms'],
|
||||
grid: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
rightMin: 5,
|
||||
rightMax: 15,
|
||||
leftLogBase: 1,
|
||||
rightLogBase: 2,
|
||||
threshold1: 200,
|
||||
threshold2: 400,
|
||||
threshold1Color: 'yellow',
|
||||
threshold2Color: 'red',
|
||||
},
|
||||
leftYAxisLabel: 'left label',
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
|
||||
targets: [{refId: 'A'}, {}],
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
graph = model.rows[0].panels[0];
|
||||
singlestat = model.rows[0].panels[1];
|
||||
table = model.rows[0].panels[2];
|
||||
});
|
||||
|
||||
it('should have title', function() {
|
||||
expect(model.title).to.be('No Title');
|
||||
});
|
||||
|
||||
it('should have panel id', function() {
|
||||
expect(graph.id).to.be(1);
|
||||
});
|
||||
|
||||
it('should move time and filtering list', function() {
|
||||
expect(model.time.from).to.be('now-1d');
|
||||
expect(model.templating.list[0].allFormat).to.be('glob');
|
||||
});
|
||||
|
||||
it('graphite panel should change name too graph', function() {
|
||||
expect(graph.type).to.be('graph');
|
||||
});
|
||||
|
||||
it('single stat panel should have two thresholds', function() {
|
||||
expect(singlestat.thresholds).to.be('20,30');
|
||||
});
|
||||
|
||||
it('queries without refId should get it', function() {
|
||||
expect(graph.targets[1].refId).to.be('B');
|
||||
});
|
||||
|
||||
it('update legend setting', function() {
|
||||
expect(graph.legend.show).to.be(true);
|
||||
});
|
||||
|
||||
it('move aliasYAxis to series override', function() {
|
||||
expect(graph.seriesOverrides[0].alias).to.be("test");
|
||||
expect(graph.seriesOverrides[0].yaxis).to.be(2);
|
||||
});
|
||||
|
||||
it('should move pulldowns to new schema', function() {
|
||||
expect(model.annotations.list[0].name).to.be('old');
|
||||
});
|
||||
|
||||
it('table panel should only have two thresholds values', function() {
|
||||
expect(table.styles[0].thresholds[0]).to.be("20");
|
||||
expect(table.styles[0].thresholds[1]).to.be("30");
|
||||
expect(table.styles[1].thresholds[0]).to.be("200");
|
||||
expect(table.styles[1].thresholds[1]).to.be("300");
|
||||
});
|
||||
|
||||
it('graph grid to yaxes options', function() {
|
||||
expect(graph.yaxes[0].min).to.be(1);
|
||||
expect(graph.yaxes[0].max).to.be(10);
|
||||
expect(graph.yaxes[0].format).to.be('kbyte');
|
||||
expect(graph.yaxes[0].label).to.be('left label');
|
||||
expect(graph.yaxes[0].logBase).to.be(1);
|
||||
expect(graph.yaxes[1].min).to.be(5);
|
||||
expect(graph.yaxes[1].max).to.be(15);
|
||||
expect(graph.yaxes[1].format).to.be('ms');
|
||||
expect(graph.yaxes[1].logBase).to.be(2);
|
||||
|
||||
expect(graph.grid.rightMax).to.be(undefined);
|
||||
expect(graph.grid.rightLogBase).to.be(undefined);
|
||||
expect(graph.y_formats).to.be(undefined);
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', function() {
|
||||
expect(model.schemaVersion).to.be(14);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', function() {
|
||||
expect(graph.thresholds.length).to.be(2);
|
||||
expect(graph.thresholds[0].op).to.be('gt');
|
||||
expect(graph.thresholds[0].value).to.be(200);
|
||||
expect(graph.thresholds[0].fillColor).to.be('yellow');
|
||||
expect(graph.thresholds[1].value).to.be(400);
|
||||
expect(graph.thresholds[1].fillColor).to.be('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
annotations: {
|
||||
enable: true,
|
||||
},
|
||||
templating: {
|
||||
enable: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given editable false dashboard', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
editable: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should set meta canEdit and canSave to false', function() {
|
||||
expect(model.meta.canSave).to.be(false);
|
||||
expect(model.meta.canEdit).to.be(false);
|
||||
});
|
||||
|
||||
it('getSaveModelClone should remove meta', function() {
|
||||
var clone = model.getSaveModelClone();
|
||||
expect(clone.meta).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading dashboard with old influxdb query schema', function() {
|
||||
var model;
|
||||
var target;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
rows: [{
|
||||
panels: [{
|
||||
type: 'graph',
|
||||
grid: {},
|
||||
yaxes: [{}, {}],
|
||||
targets: [{
|
||||
"alias": "$tag_datacenter $tag_source $col",
|
||||
"column": "value",
|
||||
"measurement": "logins.count",
|
||||
"fields": [
|
||||
{
|
||||
"func": "mean",
|
||||
"name": "value",
|
||||
"mathExpr": "*2",
|
||||
"asExpr": "value"
|
||||
},
|
||||
{
|
||||
"name": "one-minute",
|
||||
"func": "mean",
|
||||
"mathExpr": "*3",
|
||||
"asExpr": "one-minute"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"fill": "previous",
|
||||
"function": "mean",
|
||||
"groupBy": [
|
||||
{
|
||||
"interval": "auto",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"type": "tag",
|
||||
"key": "datacenter"
|
||||
}
|
||||
],
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
target = model.rows[0].panels[0].targets[0];
|
||||
});
|
||||
|
||||
it('should update query schema', function() {
|
||||
expect(target.fields).to.be(undefined);
|
||||
expect(target.select.length).to.be(2);
|
||||
expect(target.select[0].length).to.be(4);
|
||||
expect(target.select[0][0].type).to.be('field');
|
||||
expect(target.select[0][1].type).to.be('mean');
|
||||
expect(target.select[0][2].type).to.be('math');
|
||||
expect(target.select[0][3].type).to.be('alias');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
annotations: {
|
||||
enable: true,
|
||||
},
|
||||
templating: {
|
||||
enable: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Formatting epoch timestamp when timezone is set as utc', function() {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
dashboard = _dashboardSrv.create({
|
||||
timezone: 'utc',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should format timestamp with second resolution by default', function() {
|
||||
expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30');
|
||||
});
|
||||
|
||||
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
|
||||
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30');
|
||||
});
|
||||
|
||||
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
|
||||
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
||||
{{variable.label || variable.name}}
|
||||
</label>
|
||||
<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
|
||||
<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
||||
</div>
|
||||
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,6 @@ export class SubmenuCtrl {
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
}
|
||||
|
||||
getValuesForTag(variable, tagKey) {
|
||||
return this.variableSrv.getValuesForTag(variable, tagKey);
|
||||
}
|
||||
|
||||
variableUpdated(variable) {
|
||||
this.variableSrv.variableUpdated(variable).then(() => {
|
||||
this.$rootScope.$emit('template-variable-value-updated');
|
||||
|
||||
@@ -252,7 +252,7 @@ export class PanelCtrl {
|
||||
if (!!this.panel.description) {
|
||||
return 'info';
|
||||
}
|
||||
if (this.panel.links.length > 0) {
|
||||
if (this.panel.links && this.panel.links.length) {
|
||||
return 'links';
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -190,7 +190,7 @@ module.directive('grafanaPanel', function($rootScope) {
|
||||
module.directive('panelResizer', function($rootScope) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<span class="resize-panel-handle fa fa-signal"></span>',
|
||||
template: '<span class="resize-panel-handle icon-gf icon-gf-grabber"></span>',
|
||||
link: function(scope, elem) {
|
||||
var resizing = false;
|
||||
var lastPanel;
|
||||
|
||||
@@ -111,14 +111,14 @@
|
||||
<span class="gf-form-label width-9">Values</span>
|
||||
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()" required></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Auto option</span>
|
||||
<editor-checkbox text="Enable" model="current.auto" change="runQuery()"></editor-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Auto Option" label-class="width-9" checked="current.auto" on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9" ng-show="current.auto">
|
||||
Auto steps <tip>How many times should the current time range be divided to calculate the value</tip>
|
||||
Step count <tip>How many times should the current time range be divided to calculate the value</tip>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
|
||||
<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [2,3,4,5,10,20,30,40,50,100,200,300,400,500]" ng-change="runQuery()"></select>
|
||||
@@ -257,27 +257,26 @@
|
||||
|
||||
<div class="gf-form-group" ng-if="current.type === 'query'">
|
||||
<h5>Value groups/tags (Experimental feature)</h5>
|
||||
<div class="gf-form">
|
||||
<editor-checkbox text="Enable" model="current.useTags" change="runQuery()"></editor-checkbox>
|
||||
</div>
|
||||
<div class="gf-form last" ng-if="current.useTags">
|
||||
<span class="gf-form-label width-10">Tags query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.useTags">
|
||||
<li class="gf-form-label width-10">Tag values query</li>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Enabled" label-class="width-10" checked="current.useTags" on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form last" ng-if="current.useTags">
|
||||
<span class="gf-form-label width-10">Tags query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.useTags">
|
||||
<li class="gf-form-label width-10">Tag values query</li>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-show="current.options.length">
|
||||
<h5>Preview of values (shows max 20)</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
|
||||
<span class="gf-form-label">{{option.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-show="current.options.length">
|
||||
<h5>Preview of values (shows max 20)</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
|
||||
<span class="gf-form-label">{{option.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info gf-form-group" ng-if="infoText">
|
||||
{{infoText}}
|
||||
|
||||
@@ -21,6 +21,10 @@ export class QueryVariable implements Variable {
|
||||
name: string;
|
||||
multi: boolean;
|
||||
includeAll: boolean;
|
||||
useTags: boolean;
|
||||
tagsQuery: string;
|
||||
tagValuesQuery: string;
|
||||
tags: any[];
|
||||
|
||||
defaults = {
|
||||
type: 'query',
|
||||
@@ -37,8 +41,10 @@ export class QueryVariable implements Variable {
|
||||
allValue: null,
|
||||
options: [],
|
||||
current: {},
|
||||
tagsQuery: null,
|
||||
tagValuesQuery: null,
|
||||
tags: [],
|
||||
useTags: false,
|
||||
tagsQuery: "",
|
||||
tagValuesQuery: "",
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
@@ -77,9 +83,37 @@ export class QueryVariable implements Variable {
|
||||
updateOptions() {
|
||||
return this.datasourceSrv.get(this.datasource)
|
||||
.then(this.updateOptionsFromMetricFindQuery.bind(this))
|
||||
.then(this.updateTags.bind(this))
|
||||
.then(this.variableSrv.validateVariableSelectionState.bind(this.variableSrv, this));
|
||||
}
|
||||
|
||||
updateTags(datasource) {
|
||||
if (this.useTags) {
|
||||
return datasource.metricFindQuery(this.tagsQuery).then(results => {
|
||||
this.tags = [];
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
this.tags.push(results[i].text);
|
||||
}
|
||||
return datasource;
|
||||
});
|
||||
} else {
|
||||
delete this.tags;
|
||||
}
|
||||
|
||||
return datasource;
|
||||
}
|
||||
|
||||
getValuesForTag(tagKey) {
|
||||
return this.datasourceSrv.get(this.datasource).then(datasource => {
|
||||
var query = this.tagValuesQuery.replace('$tag', tagKey);
|
||||
return datasource.metricFindQuery(query).then(function (results) {
|
||||
return _.map(results, function(value) {
|
||||
return value.text;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateOptionsFromMetricFindQuery(datasource) {
|
||||
return datasource.metricFindQuery(this.query).then(results => {
|
||||
this.options = this.metricNamesToVariableValues(results);
|
||||
@@ -147,11 +181,11 @@ export class QueryVariable implements Variable {
|
||||
} else if (sortType === 2) {
|
||||
options = _.sortBy(options, function(opt) {
|
||||
var matches = opt.text.match(/.*?(\d+).*/);
|
||||
if (!matches) {
|
||||
return 0;
|
||||
} else {
|
||||
return parseInt(matches[1], 10);
|
||||
}
|
||||
if (!matches) {
|
||||
return 0;
|
||||
} else {
|
||||
return parseInt(matches[1], 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
|
||||
<dash-row class="dash-row" ng-repeat="row in dashboard.rows" row="row" dashboard="dashboard">
|
||||
</dash-row>
|
||||
</div>
|
||||
|
||||
<div ng-show='dashboardMeta.canEdit' class="row-fluid add-row-panel-hint">
|
||||
<div class="span12" style="text-align:left;">
|
||||
<span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
|
||||
<span><i class="fa fa-plus"></i> ADD ROW</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show='dashboardMeta.canEdit' class="add-row-panel-hint">
|
||||
<div class="span12" style="text-align:left;">
|
||||
<span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
|
||||
<span><i class="fa fa-plus"></i> ADD ROW</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +1,17 @@
|
||||
{
|
||||
"revision": 6,
|
||||
"title": "TestData - Graph Panel Last 1h",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"sharedCrosshair": false,
|
||||
"hideControls": false,
|
||||
"time": {
|
||||
"from": "2016-11-16T16:59:38.294Z",
|
||||
"to": "2016-11-16T17:09:01.532Z"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"refresh": false,
|
||||
"schemaVersion": 13,
|
||||
"version": 4,
|
||||
"links": [],
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"hideControls": false,
|
||||
"links": [],
|
||||
"refresh": false,
|
||||
"revision": 8,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
@@ -63,7 +22,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 1,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -137,7 +95,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -211,7 +168,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -278,17 +234,15 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "New row",
|
||||
"showTitle": false,
|
||||
"titleSize": "h6",
|
||||
"isNew": false,
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"repeatIteration": null
|
||||
"showTitle": false,
|
||||
"title": "New row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
@@ -299,7 +253,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -370,7 +323,6 @@
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 6,
|
||||
"isNew": true,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"span": 4,
|
||||
@@ -378,17 +330,15 @@
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"title": "New row",
|
||||
"showTitle": false,
|
||||
"titleSize": "h6",
|
||||
"isNew": false,
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"repeatIteration": null
|
||||
"showTitle": false,
|
||||
"title": "New row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": 336,
|
||||
"panels": [
|
||||
{
|
||||
@@ -399,7 +349,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 5,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -481,7 +430,6 @@
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 7,
|
||||
"isNew": true,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"span": 4,
|
||||
@@ -489,17 +437,15 @@
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"title": "New row",
|
||||
"showTitle": false,
|
||||
"titleSize": "h6",
|
||||
"isNew": false,
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"repeatIteration": null
|
||||
"showTitle": false,
|
||||
"title": "New row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
@@ -510,7 +456,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 8,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -584,7 +529,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 10,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -655,7 +599,6 @@
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 13,
|
||||
"isNew": true,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"span": 4,
|
||||
@@ -663,17 +606,16 @@
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"title": "New row",
|
||||
"showTitle": false,
|
||||
"titleSize": "h6",
|
||||
"isNew": false,
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"repeatIteration": null
|
||||
"showTitle": false,
|
||||
"title": "New row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"isNew": false,
|
||||
"title": "Dashboard Row",
|
||||
"collapse": false,
|
||||
"height": 250,
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
@@ -683,7 +625,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 9,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -776,7 +717,6 @@
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 14,
|
||||
"isNew": true,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"span": 4,
|
||||
@@ -784,17 +724,16 @@
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"showTitle": false,
|
||||
"titleSize": "h6",
|
||||
"height": 250,
|
||||
"repeat": null,
|
||||
"repeatRowId": null,
|
||||
"repeatIteration": null,
|
||||
"collapse": false
|
||||
"repeatRowId": null,
|
||||
"showTitle": false,
|
||||
"title": "Dashboard Row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"isNew": false,
|
||||
"title": "Dashboard Row",
|
||||
"collapse": false,
|
||||
"height": 250,
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
@@ -804,7 +743,6 @@
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 12,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
@@ -833,12 +771,12 @@
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"alias": "",
|
||||
"hide": false,
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
|
||||
"target": "",
|
||||
"alias": ""
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
@@ -898,7 +836,6 @@
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 15,
|
||||
"isNew": true,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"span": 4,
|
||||
@@ -906,13 +843,606 @@
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"showTitle": false,
|
||||
"titleSize": "h6",
|
||||
"height": 250,
|
||||
"repeat": null,
|
||||
"repeatRowId": null,
|
||||
"repeatIteration": null,
|
||||
"collapse": false
|
||||
"repeatRowId": null,
|
||||
"showTitle": false,
|
||||
"title": "Dashboard Row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"height": 250,
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"decimals": 3,
|
||||
"fill": 1,
|
||||
"id": 20,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": true,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 12,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Legend Table Single Series Should Take Minium Height",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"showTitle": false,
|
||||
"title": "Dashboard Row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"height": 250,
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"decimals": 3,
|
||||
"fill": 1,
|
||||
"id": 16,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": true,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Legend Table No Scroll Visible",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"decimals": 3,
|
||||
"fill": 1,
|
||||
"id": 17,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": true,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Legend Table Should Scroll",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"showTitle": false,
|
||||
"title": "Dashboard Row",
|
||||
"titleSize": "h6"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"height": 250,
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"decimals": 3,
|
||||
"fill": 1,
|
||||
"id": 18,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": true,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Legend Table No Scroll Visible",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"decimals": 3,
|
||||
"fill": 1,
|
||||
"id": 19,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": true,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Legend Table No Scroll Visible",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"showTitle": false,
|
||||
"title": "Dashboard Row",
|
||||
"titleSize": "h6"
|
||||
}
|
||||
]
|
||||
],
|
||||
"schemaVersion": 14,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "TestData - Graph Panel Last 1h",
|
||||
"version": 2
|
||||
}
|
||||
|
||||
1
public/app/plugins/app/testdata/module.ts
vendored
1
public/app/plugins/app/testdata/module.ts
vendored
@@ -5,6 +5,7 @@ export class ConfigCtrl {
|
||||
|
||||
appEditCtrl: any;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private backendSrv) {
|
||||
this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
|
||||
}
|
||||
|
||||
2
public/app/plugins/app/testdata/plugin.json
vendored
2
public/app/plugins/app/testdata/plugin.json
vendored
@@ -9,7 +9,7 @@
|
||||
"name": "Grafana Project",
|
||||
"url": "http://grafana.org"
|
||||
},
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"updated": "2016-09-26"
|
||||
},
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-13">Default Region</label>
|
||||
<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2']"></select>
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2']"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
|
||||
</info-popover>
|
||||
|
||||
@@ -73,7 +73,7 @@ function (angular, _, queryDef) {
|
||||
$scope.validateModel = function() {
|
||||
$scope.index = _.indexOf(bucketAggs, $scope.agg);
|
||||
$scope.isFirst = $scope.index === 0;
|
||||
$scope.isLast = $scope.index === bucketAggs.length - 1;
|
||||
$scope.bucketAggCount = bucketAggs.length;
|
||||
|
||||
var settingsLinkText = "";
|
||||
var settings = $scope.agg.settings || {};
|
||||
|
||||
@@ -231,6 +231,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
|
||||
this.getFields = function(query) {
|
||||
return this._get('/_mapping').then(function(result) {
|
||||
|
||||
var typeMap = {
|
||||
'float': 'number',
|
||||
'double': 'number',
|
||||
@@ -238,12 +239,28 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
'long': 'number',
|
||||
'date': 'date',
|
||||
'string': 'string',
|
||||
'text': 'string',
|
||||
'scaled_float': 'number',
|
||||
'nested': 'nested'
|
||||
};
|
||||
|
||||
function shouldAddField(obj, key, query) {
|
||||
if (key[0] === '_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!query.type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// equal query type filter, or via typemap translation
|
||||
return query.type === obj.type || query.type === typeMap[obj.type];
|
||||
}
|
||||
|
||||
// Store subfield names: [system, process, cpu, total] -> system.process.cpu.total
|
||||
var fieldNameParts = [];
|
||||
var fields = {};
|
||||
|
||||
function getFieldsRecursively(obj) {
|
||||
for (var key in obj) {
|
||||
var subObj = obj[key];
|
||||
@@ -256,10 +273,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
var fieldName = fieldNameParts.concat(key).join('.');
|
||||
|
||||
// Hide meta-fields and check field type
|
||||
if (key[0] !== '_' &&
|
||||
(!query.type ||
|
||||
query.type && typeMap[subObj.type] === query.type)) {
|
||||
|
||||
if (shouldAddField(subObj, key, query)) {
|
||||
fields[fieldName] = {
|
||||
text: fieldName,
|
||||
type: subObj.type
|
||||
|
||||
@@ -29,6 +29,7 @@ function (angular, _, queryDef) {
|
||||
$scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion);
|
||||
$scope.extendedStats = queryDef.extendedStats;
|
||||
$scope.pipelineAggOptions = [];
|
||||
$scope.modelSettingsValues = {};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.agg = metricAggs[$scope.index];
|
||||
@@ -95,6 +96,12 @@ function (angular, _, queryDef) {
|
||||
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
|
||||
break;
|
||||
}
|
||||
case 'moving_avg': {
|
||||
$scope.movingAvgModelTypes = queryDef.movingAvgModelOptions;
|
||||
$scope.modelSettings = queryDef.getMovingAvgSettings($scope.agg.settings.model, true);
|
||||
$scope.updateMovingAvgModelSettings();
|
||||
break;
|
||||
}
|
||||
case 'raw_document': {
|
||||
$scope.target.metrics = [$scope.agg];
|
||||
$scope.target.bucketAggs = [];
|
||||
@@ -127,6 +134,25 @@ function (angular, _, queryDef) {
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.updateMovingAvgModelSettings = function () {
|
||||
var modelSettingsKeys = [];
|
||||
var modelSettings = queryDef.getMovingAvgSettings($scope.agg.settings.model, false);
|
||||
for (var i=0; i < modelSettings.length; i++) {
|
||||
modelSettingsKeys.push(modelSettings[i].value);
|
||||
}
|
||||
|
||||
for (var key in $scope.agg.settings.settings) {
|
||||
if (($scope.agg.settings.settings[key] === null) || (modelSettingsKeys.indexOf(key) === -1)) {
|
||||
delete $scope.agg.settings.settings[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onChangeClearInternal = function() {
|
||||
delete $scope.agg.settings.minimize;
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.onTypeChange = function() {
|
||||
$scope.agg.settings = {};
|
||||
$scope.agg.meta = {};
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<label class="gf-form-label" ng-if="isFirst">
|
||||
<a class="pointer" ng-click="addBucketAgg()"><i class="fa fa-plus"></i></a>
|
||||
</label>
|
||||
<label class="gf-form-label" ng-if="!isFirst">
|
||||
<label class="gf-form-label" ng-if="bucketAggCount > 1">
|
||||
<a class="pointer" ng-click="removeBucketAgg()"><i class="fa fa-minus"></i></a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -37,52 +37,62 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="showOptions">
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'derivative'">
|
||||
<label class="gf-form-label width-10">Unit</label>
|
||||
<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.unit" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'moving_avg'">
|
||||
<label class="gf-form-label width-10">Window</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.window" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||
</div>
|
||||
<div ng-if="agg.type === 'moving_avg'">
|
||||
<div class="gf-form offset-width-7">
|
||||
<label class="gf-form-label width-10">Model</label>
|
||||
<metric-segment-model property="agg.settings.model" options="movingAvgModelTypes" on-change="onChangeClearInternal()" custom="false" css-class="width-12"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'moving_avg'">
|
||||
<label class="gf-form-label width-10">Model</label>
|
||||
<input type="text" class="gf-form-input max-width-12" ng-change="onChangeInternal()" ng-model="agg.settings.model" blur="onChange()" spellcheck='false'>
|
||||
</div>
|
||||
<div class="gf-form offset-width-7">
|
||||
<label class="gf-form-label width-10">Window</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.window" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'moving_avg'">
|
||||
<label class="gf-form-label width-10">Predict</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.predict" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||
</div>
|
||||
<div class="gf-form offset-width-7">
|
||||
<label class="gf-form-label width-10">Predict</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.predict" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'percentiles'">
|
||||
<label class="gf-form-label width-10">Percentiles</label>
|
||||
<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'cardinality'">
|
||||
<label class="gf-form-label width-10">Precision threshold</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.precision_threshold" ng-blur="onChange()"></input>
|
||||
</div>
|
||||
<div class="gf-form offset-width-7" ng-repeat="setting in modelSettings">
|
||||
<label class="gf-form-label width-10">{{setting.text}}</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.settings[setting.value]" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||
</div>
|
||||
|
||||
<div ng-if="agg.type === 'extended_stats'">
|
||||
<gf-form-switch ng-repeat="stat in extendedStats" class="gf-form offset-width-7" label="{{stat.text}}" label-class="width-10" checked="agg.meta[stat.value]" on-change="onChangeInternal()"></gf-form-switch>
|
||||
<gf-form-switch ng-if="agg.settings.model == 'holt_winters'" class="gf-form offset-width-7" label="Pad" label-class="width-10" checked="agg.settings.settings.pad" on-change="onChangeInternal()"></gf-form-switch>
|
||||
<gf-form-switch ng-if="agg.settings.model.match('ewma|holt_winters|holt') !== null" class="gf-form offset-width-7" label="Minimize" label-class="width-10" checked="agg.settings.minimize" on-change="onChangeInternal()"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7">
|
||||
<label class="gf-form-label width-10">Sigma</label>
|
||||
<input type="number" class="gf-form-input max-width-12" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'percentiles'">
|
||||
<label class="gf-form-label width-10">Percentiles</label>
|
||||
<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="aggDef.supportsInlineScript">
|
||||
<label class="gf-form-label width-10">Script</label>
|
||||
<input type="text" class="gf-form-input max-width-12" empty-to-null ng-model="agg.inlineScript" ng-blur="onChangeInternal()" spellcheck='false' placeholder="_value * 1">
|
||||
</div>
|
||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'cardinality'">
|
||||
<label class="gf-form-label width-10">Precision threshold</label>
|
||||
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.precision_threshold" ng-blur="onChange()"></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="aggDef.supportsMissing">
|
||||
<div ng-if="agg.type === 'extended_stats'">
|
||||
<gf-form-switch ng-repeat="stat in extendedStats" class="gf-form offset-width-7" label="{{stat.text}}" label-class="width-10" checked="agg.meta[stat.value]" on-change="onChangeInternal()"></gf-form-switch>
|
||||
|
||||
<div class="gf-form offset-width-7">
|
||||
<label class="gf-form-label width-10">Sigma</label>
|
||||
<input type="number" class="gf-form-input max-width-12" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="aggDef.supportsInlineScript">
|
||||
<label class="gf-form-label width-10">Script</label>
|
||||
<input type="text" class="gf-form-input max-width-12" empty-to-null ng-model="agg.inlineScript" ng-blur="onChangeInternal()" spellcheck='false' placeholder="_value * 1">
|
||||
</div>
|
||||
|
||||
<div class="gf-form offset-width-7" ng-if="aggDef.supportsMissing">
|
||||
<label class="gf-form-label width-10">
|
||||
Missing
|
||||
<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
|
||||
|
||||
@@ -69,17 +69,44 @@ function (_) {
|
||||
{text: '1d', value: '1d'},
|
||||
],
|
||||
|
||||
movingAvgModelOptions: [
|
||||
{text: 'Simple', value: 'simple'},
|
||||
{text: 'Linear', value: 'linear'},
|
||||
{text: 'Exponentially Weighted', value: 'ewma'},
|
||||
{text: 'Holt Linear', value: 'holt'},
|
||||
{text: 'Holt Winters', value: 'holt_winters'},
|
||||
],
|
||||
|
||||
pipelineOptions: {
|
||||
'moving_avg' : [
|
||||
{text: 'window', default: 5},
|
||||
{text: 'model', default: 'simple'},
|
||||
{text: 'predict', default: 0}
|
||||
{text: 'predict', default: undefined},
|
||||
{text: 'minimize', default: false},
|
||||
],
|
||||
'derivative': [
|
||||
{text: 'unit', default: undefined},
|
||||
]
|
||||
},
|
||||
|
||||
movingAvgModelSettings: {
|
||||
'simple' : [],
|
||||
'linear' : [],
|
||||
'ewma' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined}],
|
||||
'holt' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined},
|
||||
{text: "Beta", value: "beta", default: undefined},
|
||||
],
|
||||
'holt_winters' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined},
|
||||
{text: "Beta", value: "beta", default: undefined},
|
||||
{text: "Gamma", value: "gamma", default: undefined},
|
||||
{text: "Period", value: "period", default: undefined},
|
||||
{text: "Pad", value: "pad", default: undefined, isCheckbox: true},
|
||||
],
|
||||
},
|
||||
|
||||
getMetricAggTypes: function(esVersion) {
|
||||
return _.filter(this.metricAggTypes, function(f) {
|
||||
if (f.minVersion) {
|
||||
@@ -119,6 +146,19 @@ function (_) {
|
||||
return result;
|
||||
},
|
||||
|
||||
getMovingAvgSettings: function(model, filtered) {
|
||||
var filteredResult = [];
|
||||
if (filtered) {
|
||||
_.each(this.movingAvgModelSettings[model], function(setting) {
|
||||
if (!(setting.isCheckbox)) {
|
||||
filteredResult.push(setting);
|
||||
}
|
||||
});
|
||||
return filteredResult;
|
||||
}
|
||||
return this.movingAvgModelSettings[model];
|
||||
},
|
||||
|
||||
getOrderByOptions: function(target) {
|
||||
var self = this;
|
||||
var metricRefs = [];
|
||||
|
||||
@@ -173,6 +173,15 @@ register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'mode',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
params: [],
|
||||
defaultParams: [],
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'sum',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
@@ -229,6 +238,15 @@ register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'cumulative_sum',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
params: [],
|
||||
defaultParams: [],
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'stddev',
|
||||
addStrategy: addTransformationStrategy,
|
||||
@@ -249,7 +267,7 @@ register({
|
||||
register({
|
||||
type: 'fill',
|
||||
category: groupByTimeFunctions,
|
||||
params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }],
|
||||
params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous', 'linear'] }],
|
||||
defaultParams: ['null'],
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
@@ -17,8 +17,6 @@ import {appEvents, coreModule} from 'app/core/core';
|
||||
import GraphTooltip from './graph_tooltip';
|
||||
import {ThresholdManager} from './threshold_manager';
|
||||
|
||||
var labelWidthCache = {};
|
||||
|
||||
coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -119,16 +117,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelWidth(text, elem) {
|
||||
var labelWidth = labelWidthCache[text];
|
||||
|
||||
if (!labelWidth) {
|
||||
labelWidth = labelWidthCache[text] = elem.width();
|
||||
}
|
||||
|
||||
return labelWidth;
|
||||
}
|
||||
|
||||
function drawHook(plot) {
|
||||
// Update legend values
|
||||
var yaxis = plot.getYAxes();
|
||||
@@ -156,8 +144,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
var yaxisLabel = $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
|
||||
.text(panel.yaxes[0].label)
|
||||
.appendTo(elem);
|
||||
|
||||
yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
// add right axis labels
|
||||
@@ -165,8 +151,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
var rightLabel = $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
|
||||
.text(panel.yaxes[1].label)
|
||||
.appendTo(elem);
|
||||
|
||||
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
thresholdManager.draw(plot);
|
||||
|
||||
@@ -124,6 +124,7 @@ function (angular, _, $) {
|
||||
|
||||
$container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
||||
|
||||
var tableHeaderElem;
|
||||
if (panel.legend.alignAsTable) {
|
||||
var header = '<tr>';
|
||||
header += '<th colspan="2" style="text-align:left"></th>';
|
||||
@@ -135,7 +136,7 @@ function (angular, _, $) {
|
||||
header += getTableHeaderHtml('total');
|
||||
}
|
||||
header += '</tr>';
|
||||
$container.append($(header));
|
||||
tableHeaderElem = $(header);
|
||||
}
|
||||
|
||||
if (panel.legend.sort) {
|
||||
@@ -148,6 +149,8 @@ function (angular, _, $) {
|
||||
}
|
||||
|
||||
var seriesShown = 0;
|
||||
var seriesElements = [];
|
||||
|
||||
for (i = 0; i < seriesList.length; i++) {
|
||||
var series = seriesList[i];
|
||||
|
||||
@@ -156,6 +159,7 @@ function (angular, _, $) {
|
||||
}
|
||||
|
||||
var html = '<div class="graph-legend-series';
|
||||
|
||||
if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
|
||||
if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
|
||||
html += '" data-series-index="' + i + '">';
|
||||
@@ -180,7 +184,7 @@ function (angular, _, $) {
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$container.append($(html));
|
||||
seriesElements.push($(html));
|
||||
|
||||
seriesShown++;
|
||||
}
|
||||
@@ -193,9 +197,13 @@ function (angular, _, $) {
|
||||
}
|
||||
|
||||
var topPadding = 6;
|
||||
$container.css("max-height", maxHeight - topPadding);
|
||||
var tbodyElem = $('<tbody></tbody>');
|
||||
tbodyElem.css("max-height", maxHeight - topPadding);
|
||||
tbodyElem.append(tableHeaderElem);
|
||||
tbodyElem.append(seriesElements);
|
||||
$container.append(tbodyElem);
|
||||
} else {
|
||||
$container.css("max-height", "");
|
||||
$container.append(seriesElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,14 +153,16 @@ describe('when transforming time series table', () => {
|
||||
|
||||
describe('Annnotations', () => {
|
||||
var panel = {transform: 'annotations'};
|
||||
var rawData = [
|
||||
{
|
||||
min: 1000,
|
||||
text: 'hej',
|
||||
tags: ['tags', 'asd'],
|
||||
title: 'title',
|
||||
}
|
||||
];
|
||||
var rawData = {
|
||||
annotations: [
|
||||
{
|
||||
min: 1000,
|
||||
text: 'hej',
|
||||
tags: ['tags', 'asd'],
|
||||
title: 'title',
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
table = transformDataToTable(rawData, panel);
|
||||
|
||||
@@ -125,12 +125,12 @@ transformers['annotations'] = {
|
||||
model.columns.push({text: 'Text'});
|
||||
model.columns.push({text: 'Tags'});
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
if (!data || !data.annotations || data.annotations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var evt = data[i];
|
||||
for (var i = 0; i < data.annotations.length; i++) {
|
||||
var evt = data.annotations[i];
|
||||
model.rows.push([evt.min, evt.title, evt.text, evt.tags]);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -59,4 +59,5 @@
|
||||
<glyph unicode="" glyph-name="gf-page" horiz-adv-x="1325" d="M329.8 818c0-20.435-16.565-37-37-37s-37 16.565-37 37c0 20.435 16.565 37 37 37s37-16.565 37-37zM424.3 818c0-20.435-16.565-37-37-37s-37 16.565-37 37c0 20.435 16.565 37 37 37s37-16.565 37-37zM650.2 206h369.7v-82.2h-369.7v82.2zM650.2 345.6h369.7v-82.2h-369.7v82.2zM650.2 485.3h369.7v-82.2h-369.7v82.2zM292.8 624.9h722.9v-82.2h-722.9v82.2zM288.7 485.3h295.7v-361.5h-295.7v361.5zM987 957.6h-649c-102.7 0-184.8-82.2-184.8-184.8v-649c0-102.7 82.1-184.8 184.8-184.8h649c102.7 0 184.8 82.2 184.8 184.8v649c-4.1 102.7-86.2 184.8-184.8 184.8zM1130.8 127.9c0-78-65.7-143.8-143.8-143.8h-649c-78 0-143.8 65.7-143.8 143.8v571h936.4l0.2-571zM1130.8 735.8h-932.5v37c0 78 65.7 143.8 143.8 143.8h649c78 0 143.8-65.7 143.8-143.8v-37h-4.1z" />
|
||||
<glyph unicode="" glyph-name="gf-pending" horiz-adv-x="1325" d="M663.5 960c-283 0-512.5-229.5-512.5-512.5s229.5-512.5 512.5-512.5c283 0 512.5 229.5 512.5 512.5s-229.5 512.5-512.5 512.5zM895.6 86.1l-13.6 23.6c-5.7 9.9-18.3 13.3-28.2 7.6s-13.3-18.3-7.6-28.2l13.6-23.7c-53.1-27.4-112.6-43.8-175.7-46.8v27.9c0 11.4-9.2 20.6-20.6 20.6s-20.6-9.2-20.6-20.6v-27.9c-63.2 3-122.8 19.5-176.1 47l14.4 25c5.7 9.9 2.3 22.5-7.5 28.2-9.8 5.6-22.5 2.3-28.2-7.6l-14.4-24.9c-51.7 33.4-95.7 77.5-129 129.2l25.5 14.7c9.9 5.7 13.3 18.3 7.5 28.2-5.6 9.9-18.3 13.3-28.2 7.6l-25.5-14.7c-27.6 53.4-44 113.3-46.9 176.7h29.8c11.4 0 20.6 9.2 20.6 20.6s-9.2 20.6-20.6 20.6h-29.7c3.2 63.4 19.9 123.3 47.8 176.6l26-15c3.3-1.9 6.8-2.8 10.3-2.8 7.1 0 14.1 3.7 17.9 10.3 5.7 9.9 2.3 22.5-7.5 28.2l-25.8 14.9c33.7 51.6 78.1 95.4 130 128.5l14.7-25.5c3.8-6.6 10.8-10.3 17.9-10.3 3.5 0 7.1 0.9 10.3 2.8 9.9 5.7 13.3 18.3 7.5 28.2l-14.6 25.3c53.5 27.2 113.3 43.4 176.7 46v-28.7c0-11.4 9.2-20.6 20.6-20.6s20.6 9.2 20.6 20.6v28.4c63.1-3.5 122.7-20.3 175.7-48.1l-14.1-24.4c-5.7-9.9-2.3-22.5 7.5-28.2 3.3-1.9 6.8-2.8 10.3-2.8 7.1 0 14.1 3.7 17.9 10.3l13.9 24.1c51.2-33.6 94.7-77.8 127.6-129.5l-23.6-13.6c-9.9-5.7-13.3-18.3-7.5-28.2 3.8-6.6 10.8-10.3 17.9-10.3 3.5 0 7.1 0.9 10.3 2.8l23.4 13.5c27.1-53.2 43.3-112.6 46-175.6h-26.8c-11.4 0-20.6-9.2-20.6-20.6s9.2-20.6 20.6-20.6h26.7c-3.3-62.9-19.8-122.2-47.4-175.1l-23.4 13.5c-9.9 5.7-22.6 2.3-28.2-7.6-5.7-9.9-2.3-22.5 7.5-28.2l23.2-13.4c-33-51.2-76.9-94.8-128.3-128zM479.7 648.9c-15.3-16.9-13.9-43 3-58.3l142.5-128.8v-311.1c0-22.8 18.5-41.3 41.3-41.3s41.3 18.5 41.3 41.3v347.8l-169.8 153.4c-16.8 15.2-43 13.9-58.3-3z" />
|
||||
<glyph unicode="" glyph-name="gf-verified" horiz-adv-x="1325" d="M1046.8 328.4l-1 16.8 9.3 14.1 58.2 88.7-58.2 88.7-9.3 14.1 1 16.9 6.3 105.7-110 55.1-55.1 110-122.7-7.2-14.1 9.3-88.7 58.2-102.8-67.5-122.6 7.2-47.4-94.6-7.6-15.3-15.3-7.6-94.8-47.5 6.2-105.8 1-16.8-9.3-14.1-58.3-88.8 58.2-88.7 9.3-14-1-16.9-6.1-105.8 109.9-55 7.6-15.3 47.4-94.7 122.7 7.2 14.1-9.3 88.7-58.2 102.6 67.4 122.8-7.2 47.5 94.8 7.6 15.3 110 55.1-6.1 105.7zM579.3 211.2l-198.4 198.4 83.1 93.5 126.7-126.7 266.2 266.2 76.8-76.8-354.4-354.6zM1174.5 448l-76.8 116.6 8.2 139.3-124.8 62.7-62.6 124.8-139.3-8.2-116.7 76.8-116.6-76.8-139.3 8.2-62.6-124.8-124.9-62.6 8.2-139.3-76.8-116.7 76.8-116.6-8.2-139.3 124.8-62.6 62.6-124.9 139.3 8.2 116.7-76.8 116.6 76.8 139.3-8.2 62.6 124.8 124.8 62.6-8.2 139.3 76.9 116.7zM944.3 147.7l-50.5-100.7-126 7.4-11.1-7.4-94.2-61.8-105.5 69.3-126-7.4-50.4 100.6-6.1 12.3-12.3 6.1-100.5 50.4 6.6 112.4 0.8 13.5-7.5 11.3-61.9 94.3 61.8 94.2 7.5 11.3-0.8 13.5-6.6 112.4 112.9 56.6 6.1 12.3 50.4 100.5 125.9-7.4 11.3 7.5 94.2 61.8 105.5-69.3 126 7.4 50.4-100.6 6.1-12.3 12.3-6.1 100.6-50.4-6.7-112.3-0.8-13.5 7.5-11.3 61.8-94.2-61.8-94.2-7.5-11.3 0.8-13.5 6.6-112.5-112.9-56.5-6-12.4z" />
|
||||
<glyph unicode="" glyph-name="gf-grabber" horiz-adv-x="1280" d="M990 942.5c-86.6 0-154.5-67.8-154.5-154.5s67.9-154.4 154.5-154.4 154.5 67.8 154.5 154.5-67.9 154.4-154.5 154.4v0 0zM990 602.5c-86.6 0-154.5-67.8-154.5-154.5s67.9-154.5 154.5-154.5 154.5 67.8 154.5 154.5-67.9 154.5-154.5 154.5v0zM990 262.4c-86.6 0-154.5-67.8-154.5-154.5s67.8-154.5 154.5-154.5 154.5 67.8 154.5 154.5-67.9 154.5-154.5 154.5v0zM650 602.5c-86.6 0-154.5-67.8-154.5-154.5s67.9-154.5 154.5-154.5 154.5 67.9 154.5 154.5-67.9 154.5-154.5 154.5v0zM650 262.4c-86.6 0-154.5-67.8-154.5-154.5s67.8-154.5 154.5-154.5 154.5 67.8 154.5 154.5-67.9 154.5-154.5 154.5v0zM310 262.4c-86.6 0-154.5-67.8-154.5-154.5s67.8-154.5 154.5-154.5 154.5 67.8 154.5 154.5-67.9 154.5-154.5 154.5v0z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
Binary file not shown.
Binary file not shown.
@@ -143,6 +143,9 @@
|
||||
.icon-gf-bulk_action:before {
|
||||
content: "\e61c";
|
||||
}
|
||||
.icon-gf-grabber:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-gf-users:before {
|
||||
content: "\e622";
|
||||
}
|
||||
|
||||
@@ -85,8 +85,11 @@
|
||||
}
|
||||
|
||||
.graph-legend-table {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
tbody {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.graph-legend-series {
|
||||
display: table-row;
|
||||
@@ -297,16 +300,18 @@
|
||||
|
||||
.left-yaxis-label {
|
||||
top: 50%;
|
||||
left: -5px;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: left top;
|
||||
left: 0;
|
||||
transform: translateX(-50%) translateY(-50%) rotate(-90deg);
|
||||
// this is needed for phantomsjs 2.1
|
||||
-webkit-transform: translateX(-50%) translateY(-50%) rotate(-90deg);
|
||||
}
|
||||
|
||||
.right-yaxis-label {
|
||||
top: 50%;
|
||||
right: -5px;
|
||||
transform: rotate(90deg);
|
||||
transform-origin: right top;
|
||||
right: 0;
|
||||
transform: translateX(50%) translateY(-50%) rotate(90deg);
|
||||
// this is needed for phantomsjs 2.1
|
||||
-webkit-transform: translateX(50%) translateY(-50%) rotate(90deg);
|
||||
}
|
||||
|
||||
.axisLabel {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
padding: 0;
|
||||
|
||||
.tabbed-view-header {
|
||||
padding: 0;
|
||||
/* padding: 0; */
|
||||
background-color: $body-bg;
|
||||
padding: 1.5em 1rem 0 1rem;
|
||||
}
|
||||
|
||||
@@ -8,3 +8,15 @@ td.admin-settings-key {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.admin-list-table {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-list-paging {
|
||||
float: right;
|
||||
li {
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,16 +68,6 @@ div.flot-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.panel-links-btn {
|
||||
margin-left: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-help-text {
|
||||
margin-left: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-loading {
|
||||
position:absolute;
|
||||
top: -3px;
|
||||
@@ -89,6 +79,23 @@ div.flot-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.panel-info-corner-inner {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@mixin panel-corner-color($corner-bg) {
|
||||
.panel-info-corner-inner {
|
||||
border-left: 27px solid $corner-bg;
|
||||
border-right: none;
|
||||
border-bottom: 27px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-info-corner {
|
||||
color: $text-muted;
|
||||
cursor: pointer;
|
||||
@@ -105,11 +112,12 @@ div.flot-text {
|
||||
top: -4px;
|
||||
left: -6px;
|
||||
font-size: 75%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
&--info {
|
||||
display: block;
|
||||
background: lighten($panel-bg, 4%);
|
||||
@include panel-corner-color(lighten($panel-bg, 4%));
|
||||
.fa:before {
|
||||
content: "\f129";
|
||||
}
|
||||
@@ -117,7 +125,7 @@ div.flot-text {
|
||||
|
||||
&--links {
|
||||
display: block;
|
||||
background: lighten($panel-bg, 4%);
|
||||
@include panel-corner-color(lighten($panel-bg, 4%));
|
||||
.fa {
|
||||
left: -5px;
|
||||
}
|
||||
@@ -129,24 +137,13 @@ div.flot-text {
|
||||
&--error {
|
||||
display: block;
|
||||
color: $text-color;
|
||||
background: $errorBackground !important;
|
||||
@include panel-corner-color($errorBackground);
|
||||
.fa:before {
|
||||
content: "\f12a";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-info-corner-inner {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
border-left: 27px solid transparent;
|
||||
border-right: 0px solid transparent;
|
||||
border-bottom: 26px solid $panel-bg;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.panel-full-edit {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
@@ -231,17 +228,17 @@ div.flot-text {
|
||||
.resize-panel-handle {
|
||||
cursor: nwse-resize;
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
display: block;
|
||||
color: $text-color-faint;
|
||||
overflow: hidden;
|
||||
|
||||
&:before {
|
||||
left: initial;
|
||||
right: -5px;
|
||||
right: -1px;
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
58
public/test/specs/datasource_srv_specs.js
Normal file
58
public/test/specs/datasource_srv_specs.js
Normal file
@@ -0,0 +1,58 @@
|
||||
define([
|
||||
'app/core/config',
|
||||
'app/core/services/datasource_srv'
|
||||
], function(config) {
|
||||
'use strict';
|
||||
|
||||
describe('datasource_srv', function() {
|
||||
var _datasourceSrv;
|
||||
var metricSources;
|
||||
var templateSrv = {};
|
||||
|
||||
beforeEach(module('grafana.core'));
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('templateSrv', templateSrv);
|
||||
}));
|
||||
beforeEach(module('grafana.services'));
|
||||
beforeEach(inject(function(datasourceSrv) {
|
||||
_datasourceSrv = datasourceSrv;
|
||||
}));
|
||||
|
||||
describe('when loading metric sources', function() {
|
||||
var unsortedDatasources = {
|
||||
'mmm': {
|
||||
type: 'test-db',
|
||||
meta: { metrics: {m: 1} }
|
||||
},
|
||||
'--Mixed--': {
|
||||
type: 'test-db',
|
||||
meta: {builtIn: true, metrics: {m: 1} }
|
||||
},
|
||||
'ZZZ': {
|
||||
type: 'test-db',
|
||||
meta: {metrics: {m: 1} }
|
||||
},
|
||||
'aaa': {
|
||||
type: 'test-db',
|
||||
meta: { metrics: {m: 1} }
|
||||
},
|
||||
'BBB': {
|
||||
type: 'test-db',
|
||||
meta: { metrics: {m: 1} }
|
||||
},
|
||||
};
|
||||
beforeEach(function() {
|
||||
config.datasources = unsortedDatasources;
|
||||
metricSources = _datasourceSrv.getMetricSources({skipVariables: true});
|
||||
});
|
||||
|
||||
it('should return a list of sources sorted case insensitively with builtin sources last', function() {
|
||||
expect(metricSources[0].name).to.be('aaa');
|
||||
expect(metricSources[1].name).to.be('BBB');
|
||||
expect(metricSources[2].name).to.be('mmm');
|
||||
expect(metricSources[3].name).to.be('ZZZ');
|
||||
expect(metricSources[4].name).to.be('--Mixed--');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,22 +9,26 @@ function () {
|
||||
var ctrl;
|
||||
var tagValuesMap = {};
|
||||
var rootScope;
|
||||
var q;
|
||||
|
||||
beforeEach(module('grafana.core'));
|
||||
beforeEach(inject(function($controller, $rootScope, $q, $httpBackend) {
|
||||
rootScope = $rootScope;
|
||||
q = $q;
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('ValueSelectDropdownCtrl', {$scope: scope});
|
||||
ctrl.getValuesForTag = function(obj) {
|
||||
return $q.when(tagValuesMap[obj.tagKey]);
|
||||
};
|
||||
ctrl.onUpdated = sinon.spy();
|
||||
$httpBackend.when('GET', /\.html$/).respond('');
|
||||
}));
|
||||
|
||||
describe("Given simple variable", function() {
|
||||
beforeEach(function() {
|
||||
ctrl.variable = {current: {text: 'hej', value: 'hej' }};
|
||||
ctrl.variable = {
|
||||
current: {text: 'hej', value: 'hej' },
|
||||
getValuesForTag: function(key) {
|
||||
return q.when(tagValuesMap[key]);
|
||||
},
|
||||
};
|
||||
ctrl.init();
|
||||
});
|
||||
|
||||
@@ -43,6 +47,9 @@ function () {
|
||||
{text: 'server-3', value: 'server-3'},
|
||||
],
|
||||
tags: ["key1", "key2", "key3"],
|
||||
getValuesForTag: function(key) {
|
||||
return q.when(tagValuesMap[key]);
|
||||
},
|
||||
multi: true
|
||||
};
|
||||
tagValuesMap.key1 = ['server-1', 'server-3'];
|
||||
@@ -145,6 +152,9 @@ function () {
|
||||
{text: 'server-3', value: 'server-3'},
|
||||
],
|
||||
tags: ["key1", "key2", "key3"],
|
||||
getValuesForTag: function(key) {
|
||||
return q.when(tagValuesMap[key]);
|
||||
},
|
||||
multi: true
|
||||
};
|
||||
ctrl.init();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
|
||||
function exit_if_fail {
|
||||
command=$@
|
||||
echo "Executing '$command'"
|
||||
@@ -13,17 +10,17 @@ function exit_if_fail {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
cd /home/ubuntu/.go_workspace/src/github.com/grafana/grafana
|
||||
|
||||
rm -rf node_modules
|
||||
npm install -g npm
|
||||
npm install
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
|
||||
exit_if_fail npm test
|
||||
exit_if_fail npm run coveralls
|
||||
|
||||
test -z "$(gofmt -s -l ./pkg/... | tee /dev/stderr)"
|
||||
#test -z "$(gofmt -s -l ./pkg/... | tee /dev/stderr)"
|
||||
exit_if_fail test -z "$(gofmt -s -l ./pkg/... | tee /dev/stderr)"
|
||||
|
||||
exit_if_fail go run build.go setup
|
||||
exit_if_fail go run build.go build
|
||||
|
||||
@@ -2,9 +2,31 @@
|
||||
|
||||
_circle_token=$1
|
||||
|
||||
trigger_build_url=https://circleci.com/api/v1/project/grafana/grafana-packer/tree/master?circle-token=${_circle_token}
|
||||
trigger_build_url=https://circleci.com/api/v1/project/grafana/grafana-packer/tree/v4.1.x?circle-token=${_circle_token}
|
||||
|
||||
post_data=$(cat <<EOF
|
||||
{
|
||||
"build_parameters": {
|
||||
"BRANCH": "v4.1.x"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo ${post_data}
|
||||
|
||||
curl \
|
||||
--header "Accept: application/json" \
|
||||
--header "Content-Type: application/json" \
|
||||
--request POST ${trigger_build_url}
|
||||
--data "${post_data}" \
|
||||
--request POST ${trigger_build_url}
|
||||
|
||||
#curl \
|
||||
#--header "Accept: application/json" \
|
||||
#--header "Content-Type: application/json" \
|
||||
#-X POST -d '{ "build_parameters": { "BRANCH": "v4.1.x"} }' \
|
||||
#${trigger_build_url}
|
||||
|
||||
#--request POST ${trigger_build_url}
|
||||
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ _token=$1
|
||||
curl \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${_token}" \
|
||||
-X POST -d '{ "accountName": "Torkeldegaard", "projectSlug": "grafana","branch": "v4.0.x","environmentVariables": {}}' \
|
||||
-X POST -d '{ "accountName": "Torkeldegaard", "projectSlug": "grafana","branch": "v4.1.x","environmentVariables": {}}' \
|
||||
https://ci.appveyor.com/api/builds
|
||||
|
||||
Reference in New Issue
Block a user