Compare commits

...

68 Commits

Author SHA1 Message Date
bergquist
3b510b7581 release: 4.1.2 2017-02-13 13:13:31 +01:00
Daniel Lee
0c7d101a97 admin: adds paging to global user list
Currently there is a limit of 1000 users in the global
user list. This change introduces paging so that an
admin can see all users and not just the first 1000.

Adds a new route to the api - /api/users/search that
returns a list of users and a total count. It takes
two parameters perpage and page that enable paging.

Fixes #7469
2017-02-13 13:11:09 +01:00
Daniel Lee
4c192714e4 api: removes import alias + some unused fields 2017-02-13 13:04:59 +01:00
Daniel Lee
d01cf16dea fix(panel): case insensitive sort metric sources
Sorts the list of metric sources that is used in dropdown for Panel
Data Source on the Metrics tab so that it is case insensitive and
so that the built data sources are last in the list.
2017-02-13 13:04:51 +01:00
Daniel Lee
9f0b6f5aca fix(api): case insensitive sort for datasources
The data source list is case sensitive when sorted. This changes the
sort to be case insensitive. The test only tests the handler, not the
routing or database query.
2017-02-13 13:04:42 +01:00
bergquist
46c1f72c03 fix(table): fixes broken annotation rendering
closes #7268
2017-01-17 12:20:25 +01:00
bergquist
ec033840bc updates package cloud publish script 2017-01-12 11:05:18 +01:00
bergquist
f09c114019 updates version to 4.1.1 2017-01-12 09:48:03 +01:00
Torkel Ödegaard
225bf094b2 fix(graph): fixed table legend min-height, fixes #7221 2017-01-11 15:22:31 +01:00
Torkel Ödegaard
98b648dabe fix(graph): fix for table legend and scroll, fixes #7204, fixes #6628 2017-01-11 10:38:10 +01:00
bergquist
da7f1f29de updates packagecloud script 2017-01-10 15:09:01 +01:00
bergquist
0719a66d47 tech(build): updates circle ci trigger scripts 2017-01-10 15:06:48 +01:00
bergquist
65ac0bc68b tech(build): upgrades appveyour nodejs version 2017-01-10 14:59:06 +01:00
bergquist
8862ee1ae6 tech(build): changes version to 4.1.0 2017-01-10 13:33:04 +01:00
Mark Theisen
3205d1e754 Fix webhook username mismatch
Fixing variable name mismatch so that the Authorization HTTP header is added to webhook notifications.
2017-01-10 10:34:51 +01:00
Mitsuhiro Tanda
eac75ec303 add cloudwatch region (#7161) 2017-01-06 12:17:04 +01:00
Torkel Ödegaard
45d2e70ba7 feat(dashboard): sorting dashboard model kekeys, refactoring PR #7139 2017-01-06 10:09:11 +01:00
Torkel Ödegaard
c472054f21 Merge branch 'master' into dashboard-export 2017-01-06 07:50:16 +01:00
Carl Bergquist
e61d049623 tech(build): replace npm with yarn (#7108)
* tech(build): replace npm with yarn

* tech(build): change node version for CI
2017-01-06 07:28:43 +01:00
Torkel Ödegaard
7433e8c58f docs(changelog): updated changelog with ES PR #7154 2017-01-06 07:24:56 +01:00
Torkel Ödegaard
20b7c2f2e2 feat(ES): refactoring PR #7154 2017-01-06 07:22:37 +01:00
Torkel Ödegaard
293fd93e6c fix(elasticsearch): fixed predict default value, fixes #7145 2017-01-06 07:06:41 +01:00
Torkel Ödegaard
6960fb46c2 fix(build): remove triggering full package release on every commit, #6970 2017-01-06 06:51:17 +01:00
Torkel Ödegaard
813232315e fix(testdata): fixed test data source build issue 2017-01-06 06:50:02 +01:00
Torkel Ödegaard
63f6e86e46 feat(ES): better support for text type, closes #7151 2017-01-06 06:35:08 +01:00
Torkel Ödegaard
658d39944b feat(elasticsearch): add support for Elasticsearch scaled_type number type, closes #7155 2017-01-06 06:12:48 +01:00
Vaibhav Tandon
b97e784555 Moving average support for all models along with additional settings associated with each model 2017-01-05 23:17:24 +00:00
Dan Cech
df1e90c723 export sorted dashboard json 2017-01-04 18:13:33 -05:00
Torkel Ödegaard
36929c2a92 fix(elasticsearch): add support for text type filter in templating query, fixes #7136, fixes #7135 2017-01-04 20:14:56 +01:00
Utkarsh Bhatnagar
65057212e7 Org update should throw error if not found (#7066)
* Org update should give error if not found

* Used affected rows
2017-01-03 09:21:18 +01:00
bergquist
d8ebebc612 docs(http_api): adds docs for alerting in http api 2017-01-03 08:12:18 +01:00
bergquist
9a3e51894b tech(alerting): remove unused property 2017-01-03 08:11:17 +01:00
Torkel Ödegaard
9dc42648cf fix(templating): fixed issue with experimental feature template variable value groups tags, fixes #6752 2017-01-02 13:32:20 +01:00
bergquist
2e7d222f6e Revert "tech: remove unused code"
This reverts commit d9131be0a5.
2017-01-02 13:22:37 +01:00
bergquist
d9131be0a5 tech: remove unused code 2017-01-02 13:11:09 +01:00
Torkel Ödegaard
2be5ee0bd5 fix(graph): fixed graph legend issue with always visible scrollbar, fixes #6828 2017-01-02 12:22:56 +01:00
Torkel Ödegaard
083a42942f fix(phantomsj): fixed issue with y axis label rotation in phantomjs rendered graphs, fixes #6924 2017-01-02 12:01:48 +01:00
Torkel Ödegaard
8030b56bba fix(panel): fixed panel help text for panels that set background color, fixes #7085 2017-01-02 11:14:00 +01:00
Mitsuhiro Tanda
9855ea8c6c handle sts error (#7088) 2016-12-30 16:59:06 +01:00
bergquist
08bad530dc tech(build): fail when go fmt fails 2016-12-30 15:08:31 +01:00
Julie Lee
8ed8922525 Update _tabbed_view.scss (#7078)
repeated style statement
2016-12-28 12:28:05 +01:00
Carl Bergquist
c97131d1a0 Merge pull request #7074 from ksatirli/master
Adds London and Canada AWS Regions
2016-12-27 16:32:56 +01:00
Kerim Satirli
48b57afe84 Adds London and Canada AWS Regions 2016-12-27 14:25:12 +01:00
Kerim Satirli
797e2ea25d Adds London and Canada AWS Regions 2016-12-27 14:10:36 +01:00
Vitaliy Fuks
716d6473b7 Fixed typo. (#7064) 2016-12-27 11:35:00 +01:00
Darrian
d662961ebe Added 'linear' to fill types for InfluxDB (#7042) 2016-12-23 09:43:47 +01:00
Ryan McKinley
27d83f414e Add influx 'mode' and 'cumulative_sum' (#7045) 2016-12-23 09:43:15 +01:00
James Blackburn
f98e07e012 Fix auto_assign_org_role = Read Only Editor docs (#7057)
`auto_assign_org_role` should be `Read Only Editor` rather than `Read-Only Editor`
2016-12-23 09:41:28 +01:00
bergquist
fef511d403 docs(conf): adds docs about supporting database_url
closes #1878
2016-12-21 23:39:08 +01:00
bergquist
c18741c605 docs(ldap): adds note about special chars in password
closes #5337
2016-12-21 22:02:34 +01:00
bergquist
02bf83b37e docs: fixes broken code formating 2016-12-21 15:35:55 +01:00
bergquist
8859eeb151 docs(readme): adds link to 4.1 beta release 2016-12-21 12:35:54 +01:00
bergquist
a7b3ef06a8 docs: update image in github repo 2016-12-21 12:31:25 +01:00
Carl Bergquist
70f37651be Merge pull request #7034 from Timosha/add-ruble-sign
add Russian ruble sign
2016-12-21 11:10:01 +01:00
Timon
1ab66ac5d7 add Russian ruble sign 2016-12-21 12:59:09 +03:00
bergquist
cb21b20905 docs(changelog): adds note about closing #6997 2016-12-21 10:05:56 +01:00
bergquist
4e306590f8 Merge branch 'v4.0.x' 2016-12-21 10:02:28 +01:00
bergquist
8cef4cc74e fix(influxdb): handles time(auto) like time($interval)
closes #6997
2016-12-21 10:01:20 +01:00
bergquist
c3005397f5 docs: adds guide for whats new in 4.1 beta 2016-12-21 08:33:35 +01:00
bergquist
c18b410104 docs: updates release date for 4.1.0 2016-12-20 20:11:38 +01:00
Trent White
5440804109 add new grabber icon and tweak styles to better fit space (#7019) 2016-12-20 17:49:04 +01:00
bergquist
1c8865e702 docs: updates links to windows beta build 2016-12-20 16:08:05 +01:00
Torkel Ödegaard
1507d6c872 Merge branch 'master' of github.com:grafana/grafana 2016-12-20 15:30:10 +01:00
Torkel Ödegaard
4c5bdd9da4 fix(panel): need to check if panel has links 2016-12-20 15:24:53 +01:00
bergquist
48fbd7e134 docs: add note about closing #4079 2016-12-20 14:46:05 +01:00
bergquist
afa5e9507a docs: updates install links for 4.1.0 beta 2016-12-20 13:15:59 +01:00
Utkarsh Bhatnagar
13c4ce68ee Added name and id in response of create/update (#7016) 2016-12-20 13:02:26 +01:00
Torkel Ödegaard
fa01022494 fix(ux): fixed css issue with add row button, fixes #7017 2016-12-20 13:00:46 +01:00
87 changed files with 7421 additions and 797 deletions

View File

@@ -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

View File

@@ -4,7 +4,7 @@ deps-go:
go run build.go setup
deps-js:
npm install
yarn install
deps: deps-go deps-js

View File

@@ -10,7 +10,7 @@
Grafana is an open source, feature rich metrics dashboard and graph editor for
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
![](http://grafana.org/assets/img/start_page_bg.png)
![](http://grafana.org/assets/img/features/dashboard_ex1.png)
- [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
```

View File

@@ -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

View File

@@ -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}

View File

@@ -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)"

View 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!

View 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"
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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.

View 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)"

View File

@@ -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`

View File

@@ -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

View File

@@ -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
```

View File

@@ -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",

View 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.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
View 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

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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
View 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

View File

@@ -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) {

View File

@@ -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
View 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)
})
})
}

View File

@@ -73,7 +73,6 @@ type Alert struct {
Frequency int64
EvalData *simplejson.Json
EvalDate time.Time
NewStateDate time.Time
StateChanges int

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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
}

View 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)
})
})
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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',

View File

@@ -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', {

View File

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

View File

@@ -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'},
]
},
{

View File

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

View 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;
}

View File

@@ -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);

View File

@@ -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();
});
});

View 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();
});
}
});
}
}

View File

@@ -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>
&nbsp;&nbsp;
<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>
&nbsp;&nbsp;
<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>

View File

@@ -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 {

View File

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

View 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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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>

View File

@@ -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');

View File

@@ -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 '';

View File

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

View File

@@ -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}}

View File

@@ -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);
}
});
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -5,6 +5,7 @@ export class ConfigCtrl {
appEditCtrl: any;
/** @ngInject **/
constructor(private backendSrv) {
this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
}

View File

@@ -9,7 +9,7 @@
"name": "Grafana Project",
"url": "http://grafana.org"
},
"version": "1.0.15",
"version": "1.0.17",
"updated": "2016-09-26"
},

View File

@@ -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>

View File

@@ -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 || {};

View File

@@ -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

View File

@@ -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 = {};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 = [];

View File

@@ -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,
});

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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.

View File

@@ -59,4 +59,5 @@
<glyph unicode="&#xe908;" 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="&#xe909;" 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="&#xe90a;" 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="&#xe90b;" 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.

View File

@@ -143,6 +143,9 @@
.icon-gf-bulk_action:before {
content: "\e61c";
}
.icon-gf-grabber:before {
content: "\e90b";
}
.icon-gf-users:before {
content: "\e622";
}

View File

@@ -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 {

View File

@@ -9,7 +9,7 @@
padding: 0;
.tabbed-view-header {
padding: 0;
/* padding: 0; */
background-color: $body-bg;
padding: 1.5em 1rem 0 1rem;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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--');
});
});
});
});

View File

@@ -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();

View File

@@ -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

View File

@@ -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}

View File

@@ -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

5080
yarn.lock Normal file

File diff suppressed because it is too large Load Diff