Merge branch 'master' of github.com:grafana/grafana into react-panels

This commit is contained in:
Torkel Ödegaard 2018-07-09 15:35:16 +02:00
commit 4fd2107071
134 changed files with 2261 additions and 1640 deletions

View File

@ -8,6 +8,9 @@ aliases:
- &filter-not-release
tags:
ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- &filter-only-master
branches:
only: master
version: 2
@ -91,9 +94,6 @@ jobs:
- image: circleci/node:8
steps:
- checkout
- run:
name: install yarn
command: 'sudo npm install -g yarn --quiet'
- restore_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
- run:
@ -163,7 +163,7 @@ jobs:
steps:
- checkout
- run:
name: build and package grafana
name: build, test and package grafana enterprise
command: './scripts/build/build_enterprise.sh'
- run:
name: sign packages
@ -171,6 +171,26 @@ jobs:
- run:
name: sha-sum packages
command: 'go run build.go sha-dist'
- run:
name: move enterprise packages into their own folder
command: 'mv dist enterprise-dist'
- persist_to_workspace:
root: .
paths:
- enterprise-dist/grafana-enterprise*
deploy-enterprise-master:
docker:
- image: circleci/python:2.7-stretch
steps:
- attach_workspace:
at: .
- run:
name: install awscli
command: 'sudo pip install awscli'
- run:
name: deploy to s3
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
deploy-master:
docker:
@ -224,6 +244,8 @@ workflows:
jobs:
- build-all:
filters: *filter-not-release
- build-enterprise:
filters: *filter-only-master
- codespell:
filters: *filter-not-release
- gometalinter:
@ -248,6 +270,18 @@ workflows:
filters:
branches:
only: master
- deploy-enterprise-master:
requires:
- build-all
- test-backend
- test-frontend
- codespell
- gometalinter
- mysql-integration-test
- postgres-integration-test
- build-enterprise
filters: *filter-only-master
release:
jobs:
- build-all:

2
.gitignore vendored
View File

@ -43,6 +43,8 @@ fig.yml
docker-compose.yml
docker-compose.yaml
/conf/provisioning/**/custom.yaml
/conf/provisioning/**/dev.yaml
/conf/ldap_dev.toml
profile.cov
/grafana
/local

View File

@ -7,8 +7,32 @@
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
* **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
* **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
# 5.2.0 (unreleased)
# 5.2.2 (unreleased)
### Minor
* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
* **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
# 5.2.1 (2018-06-29)
### Minor
* **Auth Proxy**: Important security fix for whitelist of IP address feature [#12444](https://github.com/grafana/grafana/pull/12444)
* **UI**: Fix - Grafana footer overlapping page [#12430](https://github.com/grafana/grafana/issues/12430)
* **Logging**: Errors should be reported before crashing [#12438](https://github.com/grafana/grafana/issues/12438)
# 5.2.0-stable (2018-06-27)
### Minor
@ -16,6 +40,10 @@
* **Render**: Enhance error message if phantomjs executable is not found [#11868](https://github.com/grafana/grafana/issues/11868)
* **Dashboard**: Set correct text in drop down when variable is present in url [#11968](https://github.com/grafana/grafana/issues/11968)
### 5.2.0-beta3 fixes
* **LDAP**: Handle "dn" ldap attribute more gracefully [#12385](https://github.com/grafana/grafana/pull/12385), reverts [#10970](https://github.com/grafana/grafana/pull/10970)
# 5.2.0-beta3 (2018-06-21)
### Minor
@ -57,6 +85,7 @@
### New Features
* **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
* **Build**: Crosscompile and packages Grafana on arm, windows, linux and darwin [#11920](https://github.com/grafana/grafana/pull/11920), thx [@fg2it](https://github.com/fg2it)
* **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882)
* **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541)
@ -92,6 +121,10 @@
* **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525)
* **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657)
# 5.1.5 (2018-06-27)
* **Docker**: Config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170)
# 5.1.4 (2018-06-19)
* **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343)
@ -1319,7 +1352,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated
**New features**
- [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site
- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site
- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is promted with a warning if he really wants to overwrite the other's changes
- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is prompted with a warning if he really wants to overwrite the other's changes
- [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views
- [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, useful when you want to ignore last minute because it contains incomplete data
- [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift

View File

@ -465,7 +465,6 @@ func ldflags() string {
b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
b.WriteString(fmt.Sprintf(" -X main.enterprise=%t", enterprise))
return b.String()
}

9
devenv/dashboards.yaml Normal file
View File

@ -0,0 +1,9 @@
apiVersion: 1
providers:
- name: 'gdev dashboards'
folder: 'gdev dashboards'
type: file
options:
path: devenv/dev-dashboards

View File

@ -1,9 +0,0 @@
apiVersion: 1
providers:
- name: 'dev dashboards'
folder: 'dev dashboards'
type: file
options:
path: devenv/dashboards/dev-dashboards

View File

@ -1,38 +1,38 @@
apiVersion: 1
datasources:
- name: Graphite
- name: gdev-graphite
type: graphite
access: proxy
url: http://localhost:8080
jsonData:
graphiteVersion: "1.1"
- name: Prometheus
- name: gdev-prometheus
type: prometheus
access: proxy
isDefault: true
url: http://localhost:9090
- name: InfluxDB
- name: gdev-influxdb
type: influxdb
access: proxy
database: site
user: grafana
password: grafana
url: http://localhost:8086
jsonData:
jsonData:
timeInterval: "15s"
- name: OpenTsdb
- name: gdev-opentsdb
type: opentsdb
access: proxy
url: http://localhost:4242
jsonData:
jsonData:
tsdbResolution: 1
tsdbVersion: 1
- name: Elastic
- name: gdev-elasticsearch-metrics
type: elasticsearch
access: proxy
database: "[metrics-]YYYY.MM.DD"
@ -40,22 +40,22 @@ datasources:
jsonData:
interval: Daily
timeField: "@timestamp"
- name: MySQL
- name: gdev-mysql
type: mysql
url: localhost:3306
database: grafana
user: grafana
password: password
- name: MSSQL
- name: gdev-mssql
type: mssql
url: localhost:1433
database: grafana
user: grafana
password: "Password!"
- name: Postgres
- name: gdev-postgres
type: postgres
url: localhost:5432
database: grafana
@ -64,7 +64,7 @@ datasources:
jsonData:
sslmode: "disable"
- name: Cloudwatch
- name: gdev-cloudwatch
type: cloudwatch
editable: true
jsonData:

View File

@ -23,41 +23,36 @@ requiresJsonnet() {
}
defaultDashboards() {
requiresJsonnet
ln -s -f -r ./dashboards/dev-dashboards/dev-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
}
defaultDatasources() {
echo "setting up all default datasources using provisioning"
ln -s -f -r ./datasources/default/default.yaml ../conf/provisioning/datasources/custom.yaml
ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
}
usage() {
echo -e "install.sh\n\tThis script installs my basic setup for a debian laptop\n"
echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
echo "Usage:"
echo " bulk-dashboards - create and provisioning 400 dashboards"
echo " default-datasources - provisiong all core datasources"
echo " no args - provisiong core datasources and dev dashboards"
}
main() {
local cmd=$1
if [[ -z "$cmd" ]]; then
usage
exit 1
fi
if [[ $cmd == "bulk-dashboards" ]]; then
bulkDashboard
elif [[ $cmd == "default-datasources" ]]; then
defaultDatasources
elif [[ $cmd == "default-dashboards" ]]; then
defaultDashboards
else
defaultDashboards
defaultDatasources
fi
if [[ -z "$cmd" ]]; then
usage
fi
}
main "$@"

View File

@ -8,7 +8,8 @@ ENV OPENLDAP_VERSION 2.4.40
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
slapd=${OPENLDAP_VERSION}* && \
slapd=${OPENLDAP_VERSION}* \
ldap-utils && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@ -22,6 +23,7 @@ COPY modules/ /etc/ldap.dist/modules
COPY prepopulate/ /etc/ldap.dist/prepopulate
COPY entrypoint.sh /entrypoint.sh
COPY prepopulate.sh /prepopulate.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -76,13 +76,14 @@ EOF
IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES); unset IFS
for module in "${modules[@]}"; do
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
echo "Adding module ${module}"
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
done
fi
for file in `ls /etc/ldap/prepopulate/*.ldif`; do
slapadd -F /etc/ldap/slapd.d -l "$file"
done
# This needs to run in background
# Will prepopulate entries after ldap daemon has started
./prepopulate.sh &
chown -R openldap:openldap /etc/ldap/slapd.d/ /var/lib/ldap/ /var/run/slapd/
else

View File

@ -1,6 +1,6 @@
# Notes on OpenLdap Docker Block
Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database.
Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database.
The ldif files add three users, `ldapviewer`, `ldapeditor` and `ldapadmin`. Two groups, `admins` and `users`, are added that correspond with the group mappings in the default conf/ldap.toml. `ldapadmin` is a member of `admins` and `ldapeditor` is a member of `users`.
@ -22,3 +22,27 @@ enabled = true
config_file = conf/ldap.toml
; allow_sign_up = true
```
Test groups & users
admins
ldap-admin
ldap-torkel
ldap-daniel
backend
ldap-carl
ldap-torkel
ldap-leo
frontend
ldap-torkel
ldap-tobias
ldap-daniel
editors
ldap-editors
no groups
ldap-viewer

View File

@ -0,0 +1,14 @@
#!/bin/bash
echo "Pre-populating ldap entries, first waiting for ldap to start"
sleep 3
adminUserDn="cn=admin,dc=grafana,dc=org"
adminPassword="grafana"
for file in `ls /etc/ldap/prepopulate/*.ldif`; do
ldapadd -x -D $adminUserDn -w $adminPassword -f "$file"
done

View File

@ -0,0 +1,9 @@
dn: ou=groups,dc=grafana,dc=org
ou: Groups
objectclass: top
objectclass: organizationalUnit
dn: ou=users,dc=grafana,dc=org
ou: Users
objectclass: top
objectclass: organizationalUnit

View File

@ -0,0 +1,80 @@
# ldap-admin
dn: cn=ldap-admin,ou=users,dc=grafana,dc=org
mail: ldap-admin@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-admin
cn: ldap-admin
dn: cn=ldap-editor,ou=users,dc=grafana,dc=org
mail: ldap-editor@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-editor
cn: ldap-editor
dn: cn=ldap-viewer,ou=users,dc=grafana,dc=org
mail: ldap-viewer@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-viewer
cn: ldap-viewer
dn: cn=ldap-carl,ou=users,dc=grafana,dc=org
mail: ldap-carl@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-carl
cn: ldap-carl
dn: cn=ldap-daniel,ou=users,dc=grafana,dc=org
mail: ldap-daniel@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-daniel
cn: ldap-daniel
dn: cn=ldap-leo,ou=users,dc=grafana,dc=org
mail: ldap-leo@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-leo
cn: ldap-leo
dn: cn=ldap-tobias,ou=users,dc=grafana,dc=org
mail: ldap-tobias@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-tobias
cn: ldap-tobias
dn: cn=ldap-torkel,ou=users,dc=grafana,dc=org
mail: ldap-torkel@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldap-torkel
cn: ldap-torkel

View File

@ -0,0 +1,25 @@
dn: cn=admins,ou=groups,dc=grafana,dc=org
cn: admins
objectClass: groupOfNames
objectClass: top
member: cn=ldap-admin,ou=users,dc=grafana,dc=org
member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
dn: cn=editors,ou=groups,dc=grafana,dc=org
cn: editors
objectClass: groupOfNames
member: cn=ldap-editor,ou=users,dc=grafana,dc=org
dn: cn=backend,ou=groups,dc=grafana,dc=org
cn: backend
objectClass: groupOfNames
member: cn=ldap-carl,ou=users,dc=grafana,dc=org
member: cn=ldap-leo,ou=users,dc=grafana,dc=org
member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
dn: cn=frontend,ou=groups,dc=grafana,dc=org
cn: frontend
objectClass: groupOfNames
member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
member: cn=ldap-daniel,ou=users,dc=grafana,dc=org
member: cn=ldap-leo,ou=users,dc=grafana,dc=org

View File

@ -1,10 +0,0 @@
dn: cn=ldapadmin,dc=grafana,dc=org
mail: ldapadmin@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldapadmin
cn: ldapadmin
memberOf: cn=admins,dc=grafana,dc=org

View File

@ -1,5 +0,0 @@
dn: cn=admins,dc=grafana,dc=org
cn: admins
member: cn=ldapadmin,dc=grafana,dc=org
objectClass: groupOfNames
objectClass: top

View File

@ -1,10 +0,0 @@
dn: cn=ldapeditor,dc=grafana,dc=org
mail: ldapeditor@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldapeditor
cn: ldapeditor
memberOf: cn=users,dc=grafana,dc=org

View File

@ -1,5 +0,0 @@
dn: cn=users,dc=grafana,dc=org
cn: users
member: cn=ldapeditor,dc=grafana,dc=org
objectClass: groupOfNames
objectClass: top

View File

@ -1,9 +0,0 @@
dn: cn=ldapviewer,dc=grafana,dc=org
mail: ldapviewer@grafana.com
userPassword: grafana
objectClass: person
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
sn: ldapviewer
cn: ldapviewer

View File

@ -77,9 +77,9 @@ Macro example | Description
------------ | -------------
*$__time(dateColumn)* | Will be replaced by an expression to rename the column to *time*. For example, *dateColumn as time*
*$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to unix timestamp and rename it to *time*. <br/>For example, *DATEDIFF(second, '1970-01-01', dateColumn) AS time*
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. <br/>For example, *dateColumn >= DATEADD(s, 1494410783, '1970-01-01') AND dateColumn <= DATEADD(s, 1494410783, '1970-01-01')*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *DATEADD(second, 1494410783, '1970-01-01')*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *DATEADD(second, 1494410783, '1970-01-01')*
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. <br/>For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
*$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*

View File

@ -60,9 +60,9 @@ Macro example | Description
------------ | -------------
*$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
*$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494497183)*
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*

View File

@ -14,14 +14,14 @@ weight = -8
Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements.
* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms!
* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
* [Security]({{< relref "#security" >}}) make your Grafana instance more secure
* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
* [InfluxDB]({{< relref "#influxdb" >}}) with support for a new function
* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
- [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
- [Native builds for ARM]({{< relref "#native-builds-for-arm" >}}) native builds of Grafana for many more platforms!
- [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
- [Security]({{< relref "#security" >}}) make your Grafana instance more secure
- [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
- [InfluxDB]({{< relref "#influxdb" >}}) now supports the `mode` function
- [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
- [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
## Elasticsearch alerting
@ -32,16 +32,18 @@ the most requested features by our community and now it's finally here. Please t
<div class="clearfix"></div>
## Cross platform build support
## Native builds for ARM
Grafana v5.2 brings an improved build pipeline with cross platform support. This enables native builds of Grafana for ARMv7 (x32), ARM64 (x64),
MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds.
Grafana v5.2 brings an improved build pipeline with cross-platform support. This enables native builds of Grafana for ARMv7 (x32) and ARM64 (x64).
We've been longing for native ARM build support for ages. With the help from our amazing community this is now finally available.
Please try it out and let us know what you think.
We've been longing for native ARM build support for a long time. With the help from our amazing community this is now finally available.
Another great addition with the improved build pipeline is that binaries for MacOS/Darwin (x64) and Windows (x64) are now automatically built and
published for both stable and nightly builds.
## Improved Docker image
The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More
The Grafana docker image adds support for Docker secrets which enables you to supply Grafana with configuration through files. More
information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets).
## Security
@ -49,18 +51,18 @@ information in the [Installing using Docker documentation](/installation/docker/
{{< docs-imagebox img="/img/docs/v52/login_change_password.png" max-width="800px" class="docs-image--right" >}}
Starting from Grafana v5.2, when you login with the administrator account using the default password you'll be presented with a form to change the password.
By this we hope to encourage users to follow Grafana's best practices and change the default administrator password.
We hope this encourages users to follow Grafana's best practices and change the default administrator password.
<div class="clearfix"></div>
## Prometheus
The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate*
functions get consistent results, and thus avoid graphs jumping around on reload.
functions get consistent results, and thus avoids graphs jumping around on reload.
## InfluxDB
The InfluxDB datasource now includes support for the *mode* function which allows to return the most frequent value in a list of field values.
The InfluxDB datasource now includes support for the *mode* function which returns the most frequent value in a list of field values.
## Alerting
@ -72,9 +74,9 @@ By popular demand Grafana now includes support for an alert notification channel
{{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}}
Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified
time range or variable you'll need to actively select that when saving a dashboard, see screenshot.
This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit
Starting from Grafana v5.2, a modified time range or variable are no longer saved by default. To save a modified
time range or variable, you'll need to actively select that when saving a dashboard, see screenshot.
This should hopefully make it easier to have sane defaults for time and variables in dashboards and make it more explicit
when you actually want to overwrite those settings.
<div class="clearfix"></div>
@ -83,13 +85,13 @@ when you actually want to overwrite those settings.
{{< docs-imagebox img="/img/docs/v52/dashboard_import.png" max-width="800px" class="docs-image--right" >}}
Grafana v5.2 adds support for specifying an existing folder or create a new one when importing a dashboard, a long awaited feature since
Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page have also got some general improvements
Grafana v5.2 adds support for specifying an existing folder or creating a new one when importing a dashboard - a long-awaited feature since
Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page has also got some general improvements
and should now make it more clear if a possible import will overwrite an existing dashboard, or not.
This release also adds some improvements for those users only having editor or admin permissions in certain folders. Now the links to
*Create Dashboard* and *Import Dashboard* is available in side navigation, dashboard search and manage dashboards/folder page for a
user that has editor role in an organization or edit permission in at least one folder.
This release also adds some improvements for those users only having editor or admin permissions in certain folders. The links to
*Create Dashboard* and *Import Dashboard* are now available in the side navigation, in dashboard search and on the manage dashboards/folder page for a
user that has editor role in an organization or the edit permission in at least one folder.
<div class="clearfix"></div>

View File

@ -36,11 +36,10 @@ HTTP/1.1 200
Content-Type: application/json
{
"DEFAULT":
{
"app_mode":"production"},
"analytics":
{
"DEFAULT": {
"app_mode":"production"
},
"analytics": {
"google_analytics_ua_id":"",
"reporting_enabled":"false"
},
@ -195,15 +194,16 @@ HTTP/1.1 200
Content-Type: application/json
{
"user_count":2,
"org_count":1,
"dashboard_count":4,
"db_snapshot_count":2,
"db_tag_count":6,
"data_source_count":1,
"playlist_count":1,
"starred_db_count":2,
"grafana_admin_count":2
"users":2,
"orgs":1,
"dashboards":4,
"snapshots":2,
"tags":6,
"datasources":1,
"playlists":1,
"stars":2,
"alerts":2,
"activeUsers":1
}
```
@ -340,4 +340,4 @@ HTTP/1.1 200
Content-Type: application/json
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
```
```

View File

@ -44,6 +44,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
The `Authorization` header value should be `Bearer <your api key>`.
The API Token can also be passed as a Basic authorization password with the special username `api_key`:
curl example:
```bash
?curl http://api_key:eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk@localhost:3000/api/org
{"id":1,"name":"Main Org."}
```
# Auth HTTP resources / actions
## Api Keys

View File

@ -19,6 +19,10 @@ The unique identifier (uid) of a folder can be used for uniquely identify folder
The uid can have a maximum length of 40 characters.
## A note about the General folder
The General folder (id=0) is special and is not part of the Folder API which means
that you cannot use this API for retrieving information about the General folder.
## Get all folders
@ -273,14 +277,14 @@ Status Codes:
## Get folder by id
`GET /api/folders/:id`
`GET /api/folders/id/:id`
Will return the folder identified by id.
**Example Request**:
```http
GET /api/folders/1 HTTP/1.1
GET /api/folders/id/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

View File

@ -12,7 +12,13 @@ parent = "http_api"
# Organisation API
## Get current Organisation
The Organisation HTTP API is divided in two resources, `/api/org` (current organisation)
and `/api/orgs` (admin organisations). One big difference between these are that
the admin of all organisations API only works with basic authentication, see [Admin Organisations API](#admin-organisations-api) for more information.
## Current Organisation API
### Get current Organisation
`GET /api/org/`
@ -37,135 +43,7 @@ Content-Type: application/json
}
```
## Get Organisation by Id
`GET /api/orgs/:orgId`
**Example Request**:
```http
GET /api/orgs/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Note: The api will only work when you pass the admin name and password
to the request http url, like http://admin:admin@localhost:3000/api/orgs/1
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"id":1,
"name":"Main Org.",
"address":{
"address1":"",
"address2":"",
"city":"",
"zipCode":"",
"state":"",
"country":""
}
}
```
## Get Organisation by Name
`GET /api/orgs/name/:orgName`
**Example Request**:
```http
GET /api/orgs/name/Main%20Org%2E HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Note: The api will only work when you pass the admin name and password
to the request http url, like http://admin:admin@localhost:3000/api/orgs/name/Main%20Org%2E
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"id":1,
"name":"Main Org.",
"address":{
"address1":"",
"address2":"",
"city":"",
"zipCode":"",
"state":"",
"country":""
}
}
```
## Create Organisation
`POST /api/orgs`
**Example Request**:
```http
POST /api/orgs HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"New Org."
}
```
Note: The api will work in the following two ways
1) Need to set GF_USERS_ALLOW_ORG_CREATE=true
2) Set the config users.allow_org_create to true in ini file
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"orgId":"1",
"message":"Organization created"
}
```
## Update current Organisation
`PUT /api/org`
**Example Request**:
```http
PUT /api/org HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"Main Org."
}
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{"message":"Organization updated"}
```
## Get all users within the actual organisation
### Get all users within the current organisation
`GET /api/org/users`
@ -195,36 +73,7 @@ Content-Type: application/json
]
```
## Add a new user to the actual organisation
`POST /api/org/users`
Adds a global user to the actual organisation.
**Example Request**:
```http
POST /api/org/users HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"role": "Admin",
"loginOrEmail": "admin"
}
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{"message":"User added to organization"}
```
## Updates the given user
### Updates the given user
`PATCH /api/org/users/:userId`
@ -250,7 +99,7 @@ Content-Type: application/json
{"message":"Organization user updated"}
```
## Delete user in actual organisation
### Delete user in current organisation
`DELETE /api/org/users/:userId`
@ -272,19 +121,181 @@ Content-Type: application/json
{"message":"User removed from organization"}
```
# Organisations
### Update current Organisation
## Search all Organisations
`PUT /api/org`
**Example Request**:
```http
PUT /api/org HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"Main Org."
}
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{"message":"Organization updated"}
```
### Add a new user to the current organisation
`POST /api/org/users`
Adds a global user to the current organisation.
**Example Request**:
```http
POST /api/org/users HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"role": "Admin",
"loginOrEmail": "admin"
}
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{"message":"User added to organization"}
```
## Admin Organisations API
The Admin Organisations HTTP API does not currently work with an API Token. API Tokens are currently
only linked to an organization and an organization role. They cannot be given the permission of server
admin, only users can be given that permission. So in order to use these API calls you will have to
use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user
is called `admin` and has permission to use this API).
### Get Organisation by Id
`GET /api/orgs/:orgId`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
GET /api/orgs/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"id":1,
"name":"Main Org.",
"address":{
"address1":"",
"address2":"",
"city":"",
"zipCode":"",
"state":"",
"country":""
}
}
```
### Get Organisation by Name
`GET /api/orgs/name/:orgName`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
GET /api/orgs/name/Main%20Org%2E HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"id":1,
"name":"Main Org.",
"address":{
"address1":"",
"address2":"",
"city":"",
"zipCode":"",
"state":"",
"country":""
}
}
```
### Create Organisation
`POST /api/orgs`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
POST /api/orgs HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"name":"New Org."
}
```
Note: The api will work in the following two ways
1) Need to set GF_USERS_ALLOW_ORG_CREATE=true
2) Set the config users.allow_org_create to true in ini file
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"orgId":"1",
"message":"Organization created"
}
```
### Search all Organisations
`GET /api/orgs`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
GET /api/orgs HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Note: The api will only work when you pass the admin name and password
to the request http url, like http://admin:admin@localhost:3000/api/orgs
@ -303,11 +314,12 @@ Content-Type: application/json
]
```
## Update Organisation
### Update Organisation
`PUT /api/orgs/:orgId`
Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented yet.
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
@ -315,7 +327,6 @@ Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented
PUT /api/orgs/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"Main Org 2."
@ -331,16 +342,17 @@ Content-Type: application/json
{"message":"Organization updated"}
```
## Delete Organisation
### Delete Organisation
`DELETE /api/orgs/:orgId`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
DELETE /api/orgs/1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
@ -352,17 +364,18 @@ Content-Type: application/json
{"message":"Organization deleted"}
```
## Get Users in Organisation
### Get Users in Organisation
`GET /api/orgs/:orgId/users`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
GET /api/orgs/1/users HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Note: The api will only work when you pass the admin name and password
to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users
@ -384,25 +397,24 @@ Content-Type: application/json
]
```
## Add User in Organisation
### Add User in Organisation
`POST /api/orgs/:orgId/users`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
POST /api/orgs/1/users HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"loginOrEmail":"user",
"role":"Viewer"
}
```
Note: The api will only work when you pass the admin name and password
to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users
**Example Response**:
@ -413,17 +425,18 @@ Content-Type: application/json
{"message":"User added to organization"}
```
## Update Users in Organisation
### Update Users in Organisation
`PATCH /api/orgs/:orgId/users/:userId`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
PATCH /api/orgs/1/users/2 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"role":"Admin"
@ -439,17 +452,18 @@ Content-Type: application/json
{"message":"Organization user updated"}
```
## Delete User in Organisation
### Delete User in Organisation
`DELETE /api/orgs/:orgId/users/:userId`
Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
**Example Request**:
```http
DELETE /api/orgs/1/users/2 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:

View File

@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
<h4>Provisioning</h4>
<p>A guide to help you automate your Grafana setup & configuration.</p>
</a>
<a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>What's new in v5.0</h4>
<p>Article on all the new cool features and enhancements in v5.0</p>
<a href="{{< relref "guides/whats-new-in-v5-2.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>What's new in v5.2</h4>
<p>Article on all the new cool features and enhancements in v5.2</p>
</a>
<a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Screencasts</h4>

View File

@ -26,7 +26,7 @@ Otherwise Grafana will not behave correctly. See example below.
## Examples
Here are some example configurations for running Grafana behind a reverse proxy.
### Grafana configuration (ex http://foo.bar.com)
### Grafana configuration (ex http://foo.bar)
```bash
[server]
@ -47,7 +47,7 @@ server {
}
```
### Examples with **sub path** (ex http://foo.bar.com/grafana)
### Examples with **sub path** (ex http://foo.bar/grafana)
#### Grafana configuration with sub path
```bash

View File

@ -19,6 +19,8 @@ installation.
## Configure
**Important:** After you've downloaded the zip file and before extracting it, make sure to open properties for that file (right-click Properties) and check the `unblock` checkbox and `Ok`.
The zip file contains a folder with the current Grafana version. Extract
this folder to anywhere you want Grafana to run from. Go into the
`conf` directory and copy `sample.ini` to `custom.ini`. You should edit

View File

@ -21,42 +21,32 @@ If you open scripted.js you can see how it reads url parameters from ARGS variab
## Example
```javascript
var rows = 1;
var seriesName = 'argName';
if(!_.isUndefined(ARGS.rows)) {
rows = parseInt(ARGS.rows, 10);
}
if(!_.isUndefined(ARGS.name)) {
seriesName = ARGS.name;
}
for (var i = 0; i < rows; i++) {
dashboard.rows.push({
title: 'Scripted Graph ' + i,
height: '300px',
panels: [
{
title: 'Events',
type: 'graph',
span: 12,
fill: 1,
linewidth: 2,
targets: [
{
'target': "randomWalk('" + seriesName + "')"
},
{
'target': "randomWalk('random walk2')"
}
],
}
]
});
}
dashboard.panels.push({
title: 'Events',
type: 'graph',
fill: 1,
linewidth: 2,
gridPos: {
h: 10,
w: 24,
x: 0,
y: 10,
},
targets: [
{
'target': "randomWalk('" + seriesName + "')"
},
{
'target': "randomWalk('random walk2')"
}
]
});
return dashboard;
```

View File

@ -1,5 +1,6 @@
[
{ "version": "v5.1", "path": "/", "archived": false, "current": true },
{ "version": "v5.2", "path": "/", "archived": false, "current": true },
{ "version": "v5.1", "path": "/v5.1", "archived": true },
{ "version": "v5.0", "path": "/v5.0", "archived": true },
{ "version": "v4.6", "path": "/v4.6", "archived": true },
{ "version": "v4.5", "path": "/v4.5", "archived": true },

View File

@ -1,4 +1,4 @@
{
"stable": "5.1.3",
"testing": "5.1.3"
"stable": "5.2.0",
"testing": "5.2.0"
}

View File

@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "5.2.0-pre1",
"version": "5.3.0-pre1",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@ -154,12 +154,12 @@
"file-saver": "^1.3.3",
"immutable": "^3.8.2",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"lodash": "^4.17.10",
"mini-css-extract-plugin": "^0.4.0",
"mobx": "^3.4.1",
"mobx-react": "^4.3.5",
"mobx-state-tree": "^1.3.1",
"moment": "^2.18.1",
"moment": "^2.22.2",
"mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0",
"optimize-css-assets-webpack-plugin": "^4.0.2",

View File

@ -135,7 +135,7 @@ func postAlertScenario(desc string, url string, routePattern string, role m.Role
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID

View File

@ -223,7 +223,7 @@ func postAnnotationScenario(desc string, url string, routePattern string, role m
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
@ -246,7 +246,7 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
@ -269,7 +269,7 @@ func deleteAnnotationsScenario(desc string, url string, routePattern string, rol
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID

View File

@ -9,9 +9,7 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
// Register adds http routes
func (hs *HTTPServer) registerRoutes() {
macaronR := hs.macaron
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
@ -21,15 +19,12 @@ func (hs *HTTPServer) registerRoutes() {
quota := middleware.Quota
bind := binding.Bind
// automatically set HEAD for every GET
macaronR.SetAutoHead(true)
r := hs.RouteRegister
// not logged in views
r.Get("/", reqSignedIn, Index)
r.Get("/logout", Logout)
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), wrap(LoginPost))
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
r.Get("/login/:name", quota("session"), OAuthLogin)
r.Get("/login", LoginView)
r.Get("/invite/:code", Index)
@ -88,20 +83,20 @@ func (hs *HTTPServer) registerRoutes() {
// sign up
r.Get("/signup", Index)
r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), wrap(SignUp))
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), wrap(SignUpStep2))
r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
// invited
r.Get("/api/user/invite/:code", wrap(GetInviteInfoByCode))
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), wrap(CompleteInvite))
r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
// reset password
r.Get("/user/password/send-reset-email", Index)
r.Get("/user/password/reset", Index)
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail))
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail))
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword))
// dashboard snapshots
r.Get("/dashboard/snapshot/*", Index)
@ -111,8 +106,8 @@ func (hs *HTTPServer) registerRoutes() {
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
r.Get("/api/snapshots-delete/:deleteKey", wrap(DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqEditorRole, wrap(DeleteDashboardSnapshot))
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
// api renew session based on remember cookie
r.Get("/api/login/ping", quota("session"), LoginAPIPing)
@ -122,138 +117,138 @@ func (hs *HTTPServer) registerRoutes() {
// user (signed in)
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
userRoute.Get("/", wrap(GetSignedInUser))
userRoute.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
userRoute.Post("/using/:id", wrap(UserSetUsingOrg))
userRoute.Get("/orgs", wrap(GetSignedInUserOrgList))
userRoute.Get("/", Wrap(GetSignedInUser))
userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
userRoute.Post("/stars/dashboard/:id", wrap(StarDashboard))
userRoute.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
userRoute.Get("/quotas", wrap(GetUserQuotas))
userRoute.Put("/helpflags/:id", wrap(SetHelpFlag))
userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), Wrap(ChangeUserPassword))
userRoute.Get("/quotas", Wrap(GetUserQuotas))
userRoute.Put("/helpflags/:id", Wrap(SetHelpFlag))
// For dev purpose
userRoute.Get("/helpflags/clear", wrap(ClearHelpFlags))
userRoute.Get("/helpflags/clear", Wrap(ClearHelpFlags))
userRoute.Get("/preferences", wrap(GetUserPreferences))
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
userRoute.Get("/preferences", Wrap(GetUserPreferences))
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
})
// users (admin permission required)
apiRoute.Group("/users", func(usersRoute routing.RouteRegister) {
usersRoute.Get("/", wrap(SearchUsers))
usersRoute.Get("/search", wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", wrap(GetUserByID))
usersRoute.Get("/:id/orgs", wrap(GetUserOrgList))
usersRoute.Get("/", Wrap(SearchUsers))
usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", Wrap(GetUserByID))
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail))
usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), Wrap(UpdateUser))
usersRoute.Post("/:id/using/:orgId", Wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin)
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID))
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
}, reqOrgAdmin)
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
teamsRoute.Get("/search", Wrap(SearchTeams))
})
// org information available to all users.
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
orgRoute.Get("/quotas", wrap(GetOrgQuotas))
orgRoute.Get("/", Wrap(GetOrgCurrent))
orgRoute.Get("/quotas", Wrap(GetOrgQuotas))
})
// current org
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
// invites
orgRoute.Get("/invites", wrap(GetPendingOrgInvites))
orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", wrap(RevokeInvite))
orgRoute.Get("/invites", Wrap(GetPendingOrgInvites))
orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), Wrap(AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", Wrap(RevokeInvite))
// prefs
orgRoute.Get("/preferences", wrap(GetOrgPreferences))
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
orgRoute.Get("/preferences", Wrap(GetOrgPreferences))
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateOrgPreferences))
}, reqOrgAdmin)
// current org without requirement of user to be org admin
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
orgRoute.Get("/users", Wrap(GetOrgUsersForCurrentOrg))
})
// create new org
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), Wrap(CreateOrg))
// search all orgs
apiRoute.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
apiRoute.Get("/orgs", reqGrafanaAdmin, Wrap(SearchOrgs))
// orgs (admin routes)
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByID))
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
orgsRoute.Delete("/", wrap(DeleteOrgByID))
orgsRoute.Get("/users", wrap(GetOrgUsers))
orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", wrap(RemoveOrgUser))
orgsRoute.Get("/quotas", wrap(GetOrgQuotas))
orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), wrap(UpdateOrgQuota))
orgsRoute.Get("/", Wrap(GetOrgByID))
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrg))
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
orgsRoute.Delete("/", Wrap(DeleteOrgByID))
orgsRoute.Get("/users", Wrap(GetOrgUsers))
orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), Wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", Wrap(RemoveOrgUser))
orgsRoute.Get("/quotas", Wrap(GetOrgQuotas))
orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), Wrap(UpdateOrgQuota))
}, reqGrafanaAdmin)
// orgs (admin routes)
apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByName))
orgsRoute.Get("/", Wrap(GetOrgByName))
}, reqGrafanaAdmin)
// auth api keys
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
keysRoute.Get("/", wrap(GetAPIKeys))
keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey))
keysRoute.Delete("/:id", wrap(DeleteAPIKey))
keysRoute.Get("/", Wrap(GetAPIKeys))
keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), Wrap(AddAPIKey))
keysRoute.Delete("/:id", Wrap(DeleteAPIKey))
}, reqOrgAdmin)
// Preferences
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), Wrap(SetHomeDashboard))
})
// Data sources
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
datasourceRoute.Get("/", wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
datasourceRoute.Delete("/:id", wrap(DeleteDataSourceByID))
datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName))
datasourceRoute.Get("/:id", wrap(GetDataSourceByID))
datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName))
datasourceRoute.Get("/", Wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
}, reqOrgAdmin)
apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIDByName), reqSignedIn)
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
apiRoute.Get("/plugins", wrap(GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID))
apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
apiRoute.Get("/plugins", Wrap(GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown))
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
}, reqOrgAdmin)
apiRoute.Get("/frontend/settings/", GetFrontendSettings)
@ -262,106 +257,106 @@ func (hs *HTTPServer) registerRoutes() {
// Folders
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
folderRoute.Get("/", wrap(GetFolders))
folderRoute.Get("/id/:id", wrap(GetFolderByID))
folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
folderRoute.Get("/", Wrap(GetFolders))
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", wrap(GetFolderByUID))
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
folderUidRoute.Delete("/", wrap(DeleteFolder))
folderUidRoute.Get("/", Wrap(GetFolderByUID))
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), Wrap(UpdateFolder))
folderUidRoute.Delete("/", Wrap(DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions))
folderPermissionRoute.Get("/", Wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateFolderPermissions))
})
})
})
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID))
dashboardRoute.Get("/uid/:uid", Wrap(GetDashboard))
dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID))
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
dashboardRoute.Get("/db/:slug", Wrap(GetDashboard))
dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboard))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(PostDashboard))
dashboardRoute.Get("/home", Wrap(GetHomeDashboard))
dashboardRoute.Get("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateDashboardPermissions))
})
})
})
// Dashboard snapshots
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/", wrap(SearchDashboardSnapshots))
dashboardRoute.Get("/", Wrap(SearchDashboardSnapshots))
})
// Playlist
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
playlistRoute.Get("/", wrap(SearchPlaylists))
playlistRoute.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist))
playlistRoute.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards))
playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist))
playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, wrap(UpdatePlaylist))
playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), wrap(CreatePlaylist))
playlistRoute.Get("/", Wrap(SearchPlaylists))
playlistRoute.Get("/:id", ValidateOrgPlaylist, Wrap(GetPlaylist))
playlistRoute.Get("/:id/items", ValidateOrgPlaylist, Wrap(GetPlaylistItems))
playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, Wrap(GetPlaylistDashboards))
playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, Wrap(DeletePlaylist))
playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, Wrap(UpdatePlaylist))
playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), Wrap(CreatePlaylist))
})
// Search
apiRoute.Get("/search/", Search)
// metrics
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(QueryMetrics))
apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios))
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk))
apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
alertsRoute.Get("/", wrap(GetAlerts))
alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), Wrap(AlertTest))
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), Wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, Wrap(GetAlert))
alertsRoute.Get("/", Wrap(GetAlerts))
alertsRoute.Get("/states-for-dashboard", Wrap(GetAlertStatesForDashboard))
})
apiRoute.Get("/alert-notifications", wrap(GetAlertNotifications))
apiRoute.Get("/alert-notifiers", wrap(GetAlertNotifiers))
apiRoute.Get("/alert-notifications", Wrap(GetAlertNotifications))
apiRoute.Get("/alert-notifiers", Wrap(GetAlertNotifiers))
apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID))
alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification))
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
alertNotifications.Get("/:notificationId", Wrap(GetAlertNotificationByID))
alertNotifications.Delete("/:notificationId", Wrap(DeleteAlertNotification))
}, reqEditorRole)
apiRoute.Get("/annotations", wrap(GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
apiRoute.Get("/annotations", Wrap(GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), Wrap(DeleteAnnotations))
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation))
annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation))
})
// error test
r.Get("/metrics/error", wrap(GenerateError))
r.Get("/metrics/error", Wrap(GenerateError))
}, reqSignedIn)
@ -372,10 +367,10 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
adminRoute.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
adminRoute.Delete("/users/:id", AdminDeleteUser)
adminRoute.Get("/users/:id/quotas", wrap(GetUserQuotas))
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota))
adminRoute.Get("/users/:id/quotas", Wrap(GetUserQuotas))
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
adminRoute.Get("/stats", AdminGetStats)
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), wrap(PauseAllAlerts))
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
}, reqGrafanaAdmin)
// rendering
@ -393,10 +388,4 @@ func (hs *HTTPServer) registerRoutes() {
// streams
//r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream)
r.Register(macaronR)
InitAppPluginRoutes(macaronR)
macaronR.NotFound(NotFoundHandler)
}

View File

@ -18,7 +18,7 @@ import (
var pluginProxyTransport *http.Transport
func InitAppPluginRoutes(r *macaron.Macaron) {
func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS,

View File

@ -30,7 +30,7 @@ type NormalResponse struct {
err error
}
func wrap(action interface{}) macaron.Handler {
func Wrap(action interface{}) macaron.Handler {
return func(c *m.ReqContext) {
var res Response

View File

@ -23,7 +23,7 @@ func loggedInUserScenarioWithRole(desc string, method string, url string, routeP
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
@ -51,7 +51,7 @@ func anonymousUserScenario(desc string, method string, url string, routePattern
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)

View File

@ -194,7 +194,7 @@ func updateDashboardPermissionScenario(desc string, url string, routePattern str
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.OrgId = TestOrgID
sc.context.UserId = TestUserID

View File

@ -882,7 +882,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
@ -907,7 +907,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{
OrgId: TestOrgID,

View File

@ -13,6 +13,7 @@ type IndexViewData struct {
Theme string
NewGrafanaVersionExists bool
NewGrafanaVersion string
AppName string
}
type PluginCss struct {

View File

@ -226,7 +226,7 @@ func updateFolderPermissionScenario(desc string, url string, routePattern string
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.OrgId = TestOrgID
sc.context.UserId = TestUserID

View File

@ -152,7 +152,7 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
@ -181,7 +181,7 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}

View File

@ -153,6 +153,7 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
"latestVersion": plugins.GrafanaLatestVersion,
"hasUpdate": plugins.GrafanaHasUpdate,
"env": setting.Env,
"isEnterprise": setting.IsEnterprise,
},
}

View File

@ -33,7 +33,11 @@ import (
)
func init() {
registry.RegisterService(&HTTPServer{})
registry.Register(&registry.Descriptor{
Name: "HTTPServer",
Instance: &HTTPServer{},
InitPriority: registry.High,
})
}
type HTTPServer struct {
@ -54,6 +58,10 @@ func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server")
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
hs.streamManager = live.NewStreamManager()
hs.macaron = hs.newMacaron()
hs.registerRoutes()
return nil
}
@ -61,10 +69,8 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
var err error
hs.context = ctx
hs.streamManager = live.NewStreamManager()
hs.macaron = hs.newMacaron()
hs.registerRoutes()
hs.applyRoutes()
hs.streamManager.Run(ctx)
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
@ -164,6 +170,26 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
macaron.Env = setting.Env
m := macaron.New()
// automatically set HEAD for every GET
m.SetAutoHead(true)
return m
}
func (hs *HTTPServer) applyRoutes() {
// start with middlewares & static routes
hs.addMiddlewaresAndStaticRoutes()
// then add view routes & api routes
hs.RouteRegister.Register(hs.macaron)
// then custom app proxy routes
hs.initAppPluginRoutes(hs.macaron)
// lastly not found route
hs.macaron.NotFound(NotFoundHandler)
}
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.macaron
m.Use(middleware.Logger())
if setting.EnableGzip {
@ -175,7 +201,7 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
for _, route := range plugins.StaticRoutes {
pluginRoute := path.Join("/public/plugins/", route.PluginId)
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
hs.mapStatic(m, route.Directory, "", pluginRoute)
hs.mapStatic(hs.macaron, route.Directory, "", pluginRoute)
}
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
@ -204,8 +230,6 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
}
m.Use(middleware.AddDefaultResponseHeaders())
return m
}
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {

View File

@ -76,6 +76,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
BuildCommit: setting.BuildCommit,
NewGrafanaVersion: plugins.GrafanaLatestVersion,
NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
AppName: setting.ApplicationName,
}
if setting.DisableGravatar {

View File

@ -18,7 +18,7 @@ import (
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/extensions"
extensions "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
@ -35,7 +35,6 @@ import (
var version = "5.0.0"
var commit = "NA"
var buildstamp string
var enterprise string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
@ -78,7 +77,7 @@ func main() {
setting.BuildVersion = version
setting.BuildCommit = commit
setting.BuildStamp = buildstampInt64
setting.Enterprise, _ = strconv.ParseBool(enterprise)
setting.IsEnterprise = extensions.IsEnterprise
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
@ -88,10 +87,11 @@ func main() {
err := server.Run()
code := server.Exit(err)
trace.Stop()
log.Close()
server.Exit(err)
os.Exit(code)
}
func listenToSystemSignals(server *GrafanaServerImpl) {

View File

@ -175,7 +175,7 @@ func (g *GrafanaServerImpl) Shutdown(reason string) {
g.childRoutines.Wait()
}
func (g *GrafanaServerImpl) Exit(reason error) {
func (g *GrafanaServerImpl) Exit(reason error) int {
// default exit code is 1
code := 1
@ -185,7 +185,7 @@ func (g *GrafanaServerImpl) Exit(reason error) {
}
g.log.Error("Server shutdown", "reason", reason)
os.Exit(code)
return code
}
func (g *GrafanaServerImpl) writePIDFile() {

View File

@ -1,3 +1,3 @@
package extensions
import _ "github.com/pkg/errors"
var IsEnterprise bool = false

View File

@ -21,6 +21,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
Email: extUser.Email,
Login: extUser.Login,
}
err := bus.Dispatch(userQuery)
if err != m.ErrUserNotFound && err != nil {
return err
@ -66,7 +67,21 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
}
}
return syncOrgRoles(cmd.Result, extUser)
err = syncOrgRoles(cmd.Result, extUser)
if err != nil {
return err
}
err = bus.Dispatch(&m.SyncTeamsCommand{
User: cmd.Result,
ExternalUser: extUser,
})
if err == bus.ErrHandlerNotFound {
return nil
}
return err
}
func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
@ -76,6 +91,7 @@ func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
Name: extUser.Name,
SkipOrgSetup: len(extUser.OrgRoles) > 0,
}
if err := bus.Dispatch(cmd); err != nil {
return nil, err
}

View File

@ -163,6 +163,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
Login: ldapUser.Username,
Email: ldapUser.Email,
Groups: ldapUser.MemberOf,
OrgRoles: map[int64]m.RoleType{},
}
@ -194,6 +195,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
ExternalUser: extUser,
SignupAllowed: setting.LdapAllowSignup,
}
err := bus.Dispatch(userQuery)
if err != nil {
return nil, err

View File

@ -1,6 +1,7 @@
package login
import (
"context"
"crypto/tls"
"testing"
@ -14,6 +15,14 @@ func TestLdapAuther(t *testing.T) {
Convey("When translating ldap user to grafana user", t, func() {
var user1 = &m.User{}
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpsertUserCommand) error {
cmd.Result = user1
cmd.Result.Login = "torkelo"
return nil
})
Convey("Given no ldap group map match", func() {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{{}},
@ -23,8 +32,6 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldEqual, ErrInvalidCredentials)
})
var user1 = &m.User{}
ldapAutherScenario("Given wildcard group match", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
@ -96,7 +103,6 @@ func TestLdapAuther(t *testing.T) {
})
Convey("When syncing ldap groups to grafana org roles", t, func() {
ldapAutherScenario("given no current user orgs", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
@ -322,6 +328,10 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
bus.AddHandler("test", UpsertUser)
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
sc.getUserByAuthInfoQuery = cmd
sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}

View File

@ -334,6 +334,14 @@ func updateTotalStats() {
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
func getEdition() string {
if setting.IsEnterprise {
return "enterprise"
} else {
return "oss"
}
}
func sendUsageStats() {
if !setting.ReportingEnabled {
return
@ -349,6 +357,7 @@ func sendUsageStats() {
"metrics": metrics,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"edition": getEdition(),
}
statsQuery := models.GetSystemStatsQuery{}

View File

@ -9,6 +9,7 @@ import (
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type AuthOptions struct {
@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string {
return key
}
username, password, err := util.DecodeBasicAuthHeader(header)
if err == nil && username == "api_key" {
return password
}
return ""
}

View File

@ -2,6 +2,7 @@ package middleware
import (
"fmt"
"net"
"net/mail"
"reflect"
"strings"
@ -28,7 +29,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
}
// if auth proxy ip(s) defined, check if request comes from one of those
if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil {
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
ctx.Handle(407, "Proxy authentication required", err)
return true
}
@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
return nil
}
// Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy
if strings.Contains(remoteAddr, ",") {
sourceIPs := strings.Split(remoteAddr, ",")
remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1])
}
remoteAddr = strings.TrimPrefix(remoteAddr, "[")
remoteAddr = strings.TrimSuffix(remoteAddr, "]")
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
sourceIP, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return err
}
// Compare allowed IP addresses to actual address
for _, proxyIP := range proxies {
if remoteAddr == strings.TrimSpace(proxyIP) {
if sourceIP == strings.TrimSpace(proxyIP) {
return nil
}
}
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr)
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
}

View File

@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) {
setting.BasicAuthEnabled = true
authHeader := util.GetBasicAuthHeader("myUser", "myPass")
sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec()
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should init middleware context with user", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
@ -128,6 +128,28 @@ func TestMiddlewareContext(t *testing.T) {
})
})
middlewareScenario("Valid api key via Basic auth", func(sc *scenarioContext) {
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
return nil
})
authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should return 200", func() {
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("Should init middleware context", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.OrgId, ShouldEqual, 12)
So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
})
})
middlewareScenario("UserId in session", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
@ -293,61 +315,6 @@ func TestMiddlewareContext(t *testing.T) {
})
})
middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is not trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.1, 192.168.1.2")
sc.exec()
Convey("should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.1.2 is not from the authentication proxy")
})
})
middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.2, 192.168.1.1")
sc.exec()
Convey("Should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 33)
So(sc.context.OrgId, ShouldEqual, 4)
})
})
middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
return sc
}
func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext {
func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
sc.authHeader = authHeader
return sc
}

View File

@ -42,6 +42,7 @@ type RemoveTeamMemberCommand struct {
type GetTeamMembersQuery struct {
OrgId int64
TeamId int64
UserId int64
Result []*TeamMemberDTO
}

View File

@ -19,6 +19,7 @@ type ExternalUserInfo struct {
Email string
Login string
Name string
Groups []string
OrgRoles map[int64]RoleType
}
@ -70,3 +71,8 @@ type GetAuthInfoQuery struct {
Result *UserAuth
}
type SyncTeamsCommand struct {
ExternalUser *ExternalUserInfo
User *User
}

View File

@ -4,6 +4,8 @@ import (
"context"
"reflect"
"sort"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
type Descriptor struct {
@ -34,23 +36,45 @@ func GetServices() []*Descriptor {
return services
}
// Service interface is the lowest common shape that services
// are expected to forfill to be started within Grafana.
type Service interface {
// Init is called by Grafana main process which gives the service
// the possibility do some initial work before its started. Things
// like adding routes, bus handlers should be done in the Init function
Init() error
}
// Useful for alerting service
// CanBeDisabled allows the services to decide if it should
// be started or not by itself. This is useful for services
// that might not always be started, ex alerting.
// This will be called after `Init()`.
type CanBeDisabled interface {
// IsDisabled should return a bool saying if it can be started or not.
IsDisabled() bool
}
// BackgroundService should be implemented for services that have
// long running tasks in the background.
type BackgroundService interface {
// Run starts the background process of the service after `Init` have been called
// on all services. The `context.Context` passed into the function should be used
// to subscribe to ctx.Done() so the service can be notified when Grafana shuts down.
Run(ctx context.Context) error
}
type HasInitPriority interface {
GetInitPriority() Priority
// DatabaseMigrator allows the caller to add migrations to
// the migrator passed as argument
type DatabaseMigrator interface {
// AddMigrations allows the service to add migrations to
// the database migrator.
AddMigration(mg *migrator.Migrator)
}
// IsDisabled takes an service and return true if its disabled
func IsDisabled(srv Service) bool {
canBeDisabled, ok := srv.(CanBeDisabled)
return ok && canBeDisabled.IsDisabled()

View File

@ -108,9 +108,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
break
}
}
// get other points
// get the oldest point
points = points[0:i]
for i := len(points) - 1; i >= 0; i-- {
for i := 0; i < len(points); i++ {
if points[i][0].Valid {
allNull = false
value = first - points[i][0].Float64
@ -131,9 +131,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
break
}
}
// get other points
// get the oldest point
points = points[0:i]
for i := len(points) - 1; i >= 0; i-- {
for i := 0; i < len(points); i++ {
if points[i][0].Valid {
allNull = false
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100

View File

@ -110,16 +110,35 @@ func TestSimpleReducer(t *testing.T) {
So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
})
Convey("diff", func() {
Convey("diff one point", func() {
result := testReducer("diff", 30)
So(result, ShouldEqual, float64(0))
})
Convey("diff two points", func() {
result := testReducer("diff", 30, 40)
So(result, ShouldEqual, float64(10))
})
Convey("percent_diff", func() {
Convey("diff three points", func() {
result := testReducer("diff", 30, 40, 40)
So(result, ShouldEqual, float64(10))
})
Convey("percent_diff one point", func() {
result := testReducer("percent_diff", 40)
So(result, ShouldEqual, float64(0))
})
Convey("percent_diff two points", func() {
result := testReducer("percent_diff", 30, 40)
So(result, ShouldEqual, float64(33.33333333333333))
})
Convey("percent_diff three points", func() {
result := testReducer("percent_diff", 30, 40, 40)
So(result, ShouldEqual, float64(33.33333333333333))
})
})
}

View File

@ -50,7 +50,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(err, ShouldBeNil)
Convey("Extractor should not modify the original json", func() {
dashJson, err := simplejson.NewJson([]byte(json))
dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
@ -79,7 +79,7 @@ func TestAlertRuleExtraction(t *testing.T) {
Convey("Parsing and validating dashboard containing graphite alerts", func() {
dashJson, err := simplejson.NewJson([]byte(json))
dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
@ -143,7 +143,7 @@ func TestAlertRuleExtraction(t *testing.T) {
panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
dashJson, err := simplejson.NewJson(panelWithoutId)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
@ -159,7 +159,7 @@ func TestAlertRuleExtraction(t *testing.T) {
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson([]byte(panelWithIdZero))
dashJson, err := simplejson.NewJson(panelWithIdZero)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)

View File

@ -104,7 +104,10 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return err
}
n.log.Info("uploaded", "url", context.ImagePublicUrl)
if context.ImagePublicUrl != "" {
n.log.Info("uploaded screenshot of alert to external image store", "url", context.ImagePublicUrl)
}
return nil
}

View File

@ -50,4 +50,5 @@ func addTeamMigrations(mg *Migrator) {
mg.AddMigration("Add column email to team table", NewAddColumnMigration(teamV1, &Column{
Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190,
}))
}

View File

@ -132,6 +132,13 @@ func (ss *SqlStore) Init() error {
migrator := migrator.NewMigrator(x)
migrations.AddMigrations(migrator)
for _, descriptor := range registry.GetServices() {
sc, ok := descriptor.Instance.(registry.DatabaseMigrator)
if ok {
sc.AddMigration(migrator)
}
}
if err := migrator.Start(); err != nil {
return fmt.Errorf("Migration failed err: %v", err)
}

View File

@ -268,7 +268,15 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
query.Result = make([]*m.TeamMemberDTO, 0)
sess := x.Table("team_member")
sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("team_member.org_id=? and team_member.team_id=?", query.OrgId, query.TeamId)
if query.OrgId != 0 {
sess.Where("team_member.org_id=?", query.OrgId)
}
if query.TeamId != 0 {
sess.Where("team_member.team_id=?", query.TeamId)
}
if query.UserId != 0 {
sess.Where("team_member.user_id=?", query.UserId)
}
sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
sess.Asc("user.login", "user.email")

View File

@ -18,9 +18,10 @@ import (
"github.com/go-macaron/session"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"time"
)
type Scheme string
@ -49,7 +50,7 @@ var (
BuildVersion string
BuildCommit string
BuildStamp int64
Enterprise bool
IsEnterprise bool
ApplicationName string
// Paths
@ -517,7 +518,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
Raw = cfg.Raw
ApplicationName = "Grafana"
if Enterprise {
if IsEnterprise {
ApplicationName += " Enterprise"
}

View File

@ -213,6 +213,7 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
userInfo := &BasicUserInfo{
Name: data.Login,
Login: data.Login,
Id: fmt.Sprintf("%d", data.Id),
Email: data.Email,
}

View File

@ -32,7 +32,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
So(json.Get("size").MustInt(500), ShouldEqual, 0)
So(json.Get("sort").Interface(), ShouldBeNil)
@ -81,7 +81,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
So(json.Get("size").MustInt(0), ShouldEqual, 200)
@ -124,7 +124,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
scriptFields, err := json.Get("script_fields").Map()
@ -163,7 +163,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
So(json.Get("aggs").MustMap(), ShouldHaveLength, 2)
@ -200,7 +200,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
So(json.Get("aggs").MustMap(), ShouldHaveLength, 1)
@ -251,7 +251,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
topAggOne := json.GetPath("aggs", "1")
@ -300,7 +300,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
topAgg := json.GetPath("aggs", "1")
@ -364,7 +364,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
termsAgg := json.GetPath("aggs", "1")
@ -419,7 +419,7 @@ func TestSearchRequest(t *testing.T) {
Convey("When marshal to JSON should generate correct json", func() {
body, err := json.Marshal(sr)
So(err, ShouldBeNil)
json, err := simplejson.NewJson([]byte(body))
json, err := simplejson.NewJson(body)
So(err, ShouldBeNil)
scriptFields, err := json.Get("script_fields").Map()

View File

@ -82,11 +82,12 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", m.TimeRange.GetFromAsSecondsEpoch()), nil
return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", m.TimeRange.GetToAsSecondsEpoch()), nil
return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
@ -108,7 +109,7 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
m.Query.Model.Set("fillValue", floatVal)
}
}
return fmt.Sprintf("CAST(ROUND(DATEDIFF(second, '1970-01-01', %s)/%.1f, 0) as bigint)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@ -49,21 +49,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= DATEADD(s, %d, '1970-01-01') AND time_column <= DATEADD(s, %d, '1970-01-01')", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeGroup function", func() {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
})
Convey("interpolate __timeGroup function with spaces around arguments", func() {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
})
Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
@ -96,14 +96,14 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -137,21 +137,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= DATEADD(s, %d, '1970-01-01') AND time_column <= DATEADD(s, %d, '1970-01-01')", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -185,21 +185,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= DATEADD(s, %d, '1970-01-01') AND time_column <= DATEADD(s, %d, '1970-01-01')", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {

View File

@ -210,11 +210,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
points := queryResult.Series[0].Points
So(len(points), ShouldEqual, 6)
// without fill this should result in 4 buckets
So(len(points), ShouldEqual, 4)
dt := fromStart
for i := 0; i < 3; i++ {
for i := 0; i < 2; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 15)
@ -222,9 +223,9 @@ func TestMSSQL(t *testing.T) {
dt = dt.Add(5 * time.Minute)
}
// adjust for 5 minute gap
dt = dt.Add(5 * time.Minute)
for i := 3; i < 6; i++ {
// adjust for 10 minute gap between first and second set of points
dt = dt.Add(10 * time.Minute)
for i := 2; i < 4; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 20)
@ -260,7 +261,7 @@ func TestMSSQL(t *testing.T) {
dt := fromStart
for i := 0; i < 3; i++ {
for i := 0; i < 2; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 15)
@ -268,17 +269,22 @@ func TestMSSQL(t *testing.T) {
dt = dt.Add(5 * time.Minute)
}
// check for NULL values inserted by fill
So(points[2][0].Valid, ShouldBeFalse)
So(points[3][0].Valid, ShouldBeFalse)
// adjust for 5 minute gap
dt = dt.Add(5 * time.Minute)
for i := 4; i < 7; i++ {
// adjust for 10 minute gap between first and second set of points
dt = dt.Add(10 * time.Minute)
for i := 4; i < 6; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 20)
So(aTime, ShouldEqual, dt)
dt = dt.Add(5 * time.Minute)
}
So(points[6][0].Valid, ShouldBeFalse)
})
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
@ -525,7 +531,7 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 1)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float32(tInitial.Unix()))*1e3)
})
Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() {
@ -547,7 +553,7 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 1)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float32(tInitial.Unix()))*1e3)
})
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
@ -924,7 +930,7 @@ func TestMSSQL(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
})
Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() {
@ -954,7 +960,7 @@ func TestMSSQL(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
})
Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {

View File

@ -77,11 +77,12 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
return fmt.Sprintf("%s >= FROM_UNIXTIME(%d) AND %s <= FROM_UNIXTIME(%d)", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
return fmt.Sprintf("FROM_UNIXTIME(%d)", m.TimeRange.GetFromAsSecondsEpoch()), nil
return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
return fmt.Sprintf("FROM_UNIXTIME(%d)", m.TimeRange.GetToAsSecondsEpoch()), nil
return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
@ -103,7 +104,7 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
m.Query.Model.Set("fillValue", floatVal)
}
}
return fmt.Sprintf("cast(cast(UNIX_TIMESTAMP(%s)/(%.0f) as signed)*%.0f as signed)", args[0], interval.Seconds(), interval.Seconds()), nil
return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@ -39,7 +39,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
})
Convey("interpolate __timeGroup function with spaces around arguments", func() {
@ -47,28 +47,28 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
})
Convey("interpolate __timeFilter function", func() {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= FROM_UNIXTIME(%d) AND time_column <= FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -102,21 +102,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= FROM_UNIXTIME(%d) AND time_column <= FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -150,21 +150,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= FROM_UNIXTIME(%d) AND time_column <= FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {

View File

@ -132,8 +132,8 @@ func TestMySQL(t *testing.T) {
So(column[7].(float64), ShouldEqual, 1.11)
So(column[8].(float64), ShouldEqual, 2.22)
So(*column[9].(*float32), ShouldEqual, 3.33)
So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now())
So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now())
So(column[10].(time.Time), ShouldHappenWithin, 10*time.Second, time.Now())
So(column[11].(time.Time), ShouldHappenWithin, 10*time.Second, time.Now())
So(column[12].(string), ShouldEqual, "11:11:11")
So(column[13].(int64), ShouldEqual, 2018)
So(*column[14].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
@ -209,11 +209,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
points := queryResult.Series[0].Points
So(len(points), ShouldEqual, 6)
// without fill this should result in 4 buckets
So(len(points), ShouldEqual, 4)
dt := fromStart
for i := 0; i < 3; i++ {
for i := 0; i < 2; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 15)
@ -221,9 +222,9 @@ func TestMySQL(t *testing.T) {
dt = dt.Add(5 * time.Minute)
}
// adjust for 5 minute gap
dt = dt.Add(5 * time.Minute)
for i := 3; i < 6; i++ {
// adjust for 10 minute gap between first and second set of points
dt = dt.Add(10 * time.Minute)
for i := 2; i < 4; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 20)
@ -259,7 +260,7 @@ func TestMySQL(t *testing.T) {
dt := fromStart
for i := 0; i < 3; i++ {
for i := 0; i < 2; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 15)
@ -267,17 +268,23 @@ func TestMySQL(t *testing.T) {
dt = dt.Add(5 * time.Minute)
}
// check for NULL values inserted by fill
So(points[2][0].Valid, ShouldBeFalse)
So(points[3][0].Valid, ShouldBeFalse)
// adjust for 5 minute gap
dt = dt.Add(5 * time.Minute)
for i := 4; i < 7; i++ {
// adjust for 10 minute gap between first and second set of points
dt = dt.Add(10 * time.Minute)
for i := 4; i < 6; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 20)
So(aTime, ShouldEqual, dt)
dt = dt.Add(5 * time.Minute)
}
// check for NULL values inserted by fill
So(points[6][0].Valid, ShouldBeFalse)
})
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
@ -571,7 +578,7 @@ func TestMySQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 1)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float32(tInitial.Unix()))*1e3)
})
Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() {
@ -593,7 +600,7 @@ func TestMySQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 1)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float32(tInitial.Unix()))*1e3)
})
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
@ -810,7 +817,7 @@ func TestMySQL(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
})
Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {

View File

@ -109,7 +109,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
m.Query.Model.Set("fillValue", floatVal)
}
}
return fmt.Sprintf("(extract(epoch from %s)/%v)::bigint*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@ -53,7 +53,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
})
Convey("interpolate __timeGroup function with spaces between args", func() {
@ -61,7 +61,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
})
Convey("interpolate __timeTo function", func() {

View File

@ -189,21 +189,23 @@ func TestPostgres(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
points := queryResult.Series[0].Points
So(len(points), ShouldEqual, 6)
// without fill this should result in 4 buckets
So(len(points), ShouldEqual, 4)
dt := fromStart
for i := 0; i < 3; i++ {
for i := 0; i < 2; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 15)
So(aTime, ShouldEqual, dt)
So(aTime.Unix()%300, ShouldEqual, 0)
dt = dt.Add(5 * time.Minute)
}
// adjust for 5 minute gap
dt = dt.Add(5 * time.Minute)
for i := 3; i < 6; i++ {
// adjust for 10 minute gap between first and second set of points
dt = dt.Add(10 * time.Minute)
for i := 2; i < 4; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 20)
@ -239,7 +241,7 @@ func TestPostgres(t *testing.T) {
dt := fromStart
for i := 0; i < 3; i++ {
for i := 0; i < 2; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 15)
@ -247,17 +249,23 @@ func TestPostgres(t *testing.T) {
dt = dt.Add(5 * time.Minute)
}
// check for NULL values inserted by fill
So(points[2][0].Valid, ShouldBeFalse)
So(points[3][0].Valid, ShouldBeFalse)
// adjust for 5 minute gap
dt = dt.Add(5 * time.Minute)
for i := 4; i < 7; i++ {
// adjust for 10 minute gap between first and second set of points
dt = dt.Add(10 * time.Minute)
for i := 4; i < 6; i++ {
aValue := points[i][0].Float64
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
So(aValue, ShouldEqual, 20)
So(aTime, ShouldEqual, dt)
dt = dt.Add(5 * time.Minute)
}
// check for NULL values inserted by fill
So(points[6][0].Valid, ShouldBeFalse)
})
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
@ -504,7 +512,7 @@ func TestPostgres(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 1)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float32(tInitial.Unix()))*1e3)
})
Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() {
@ -526,7 +534,7 @@ func TestPostgres(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 1)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float32(tInitial.Unix()))*1e3)
})
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
@ -713,7 +721,7 @@ func TestPostgres(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
})
Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() {
@ -743,7 +751,7 @@ func TestPostgres(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
})
Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {

View File

@ -9,7 +9,7 @@ import { getNextCharacter, getPreviousCousin } from './utils/dom';
import BracesPlugin from './slate-plugins/braces';
import ClearPlugin from './slate-plugins/clear';
import NewlinePlugin from './slate-plugins/newline';
import PluginPrism, { configurePrismMetricsTokens } from './slate-plugins/prism/index';
import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
import RunnerPlugin from './slate-plugins/runner';
import debounce from './utils/debounce';
import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
@ -17,13 +17,13 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
import Typeahead from './Typeahead';
const EMPTY_METRIC = '';
const TYPEAHEAD_DEBOUNCE = 300;
export const TYPEAHEAD_DEBOUNCE = 300;
function flattenSuggestions(s) {
return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
}
const getInitialValue = query =>
export const getInitialValue = query =>
Value.fromJSON({
document: {
nodes: [
@ -45,12 +45,14 @@ const getInitialValue = query =>
},
});
class Portal extends React.Component {
class Portal extends React.Component<any, any> {
node: any;
constructor(props) {
super(props);
const { index = 0, prefix = 'query' } = props;
this.node = document.createElement('div');
this.node.classList.add('explore-typeahead', `explore-typeahead-${props.index}`);
this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
document.body.appendChild(this.node);
}
@ -71,12 +73,14 @@ class QueryField extends React.Component<any, any> {
constructor(props, context) {
super(props, context);
const { prismDefinition = {}, prismLanguage = 'promql' } = props;
this.plugins = [
BracesPlugin(),
ClearPlugin(),
RunnerPlugin({ handler: props.onPressEnter }),
NewlinePlugin(),
PluginPrism(),
PluginPrism({ definition: prismDefinition, language: prismLanguage }),
];
this.state = {
@ -131,7 +135,8 @@ class QueryField extends React.Component<any, any> {
if (!this.state.metrics) {
return;
}
configurePrismMetricsTokens(this.state.metrics);
setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
// Trigger re-render
window.requestAnimationFrame(() => {
// Bogus edit to trigger highlighting
@ -162,7 +167,7 @@ class QueryField extends React.Component<any, any> {
const selection = window.getSelection();
if (selection.anchorNode) {
const wrapperNode = selection.anchorNode.parentElement;
const editorNode = wrapperNode.closest('.query-field');
const editorNode = wrapperNode.closest('.slate-query-field');
if (!editorNode || this.state.value.isBlurred) {
// Not inside this editor
return;
@ -330,20 +335,30 @@ class QueryField extends React.Component<any, any> {
}
onKeyDown = (event, change) => {
if (this.menuEl) {
const { typeaheadIndex, suggestions } = this.state;
const { typeaheadIndex, suggestions } = this.state;
switch (event.key) {
case 'Escape': {
if (this.menuEl) {
event.preventDefault();
this.resetTypeahead();
return true;
}
break;
switch (event.key) {
case 'Escape': {
if (this.menuEl) {
event.preventDefault();
event.stopPropagation();
this.resetTypeahead();
return true;
}
break;
}
case 'Tab': {
case ' ': {
if (event.ctrlKey) {
event.preventDefault();
this.handleTypeahead();
return true;
}
break;
}
case 'Tab': {
if (this.menuEl) {
// Dont blur input
event.preventDefault();
if (!suggestions || suggestions.length === 0) {
@ -359,25 +374,30 @@ class QueryField extends React.Component<any, any> {
this.applyTypeahead(change, suggestion);
return true;
}
break;
}
case 'ArrowDown': {
case 'ArrowDown': {
if (this.menuEl) {
// Select next suggestion
event.preventDefault();
this.setState({ typeaheadIndex: typeaheadIndex + 1 });
break;
}
break;
}
case 'ArrowUp': {
case 'ArrowUp': {
if (this.menuEl) {
// Select previous suggestion
event.preventDefault();
this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
break;
}
break;
}
default: {
// console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
break;
}
default: {
// console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
break;
}
}
return undefined;
@ -502,10 +522,17 @@ class QueryField extends React.Component<any, any> {
// Align menu overlay to editor node
if (node) {
// Read from DOM
const rect = node.parentElement.getBoundingClientRect();
menu.style.opacity = 1;
menu.style.top = `${rect.top + window.scrollY + rect.height + 4}px`;
menu.style.left = `${rect.left + window.scrollX - 2}px`;
const scrollX = window.scrollX;
const scrollY = window.scrollY;
// Write DOM
requestAnimationFrame(() => {
menu.style.opacity = 1;
menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
menu.style.left = `${rect.left + scrollX - 2}px`;
});
}
};
@ -514,6 +541,7 @@ class QueryField extends React.Component<any, any> {
};
renderMenu = () => {
const { portalPrefix } = this.props;
const { suggestions } = this.state;
const hasSuggesstions = suggestions && suggestions.length > 0;
if (!hasSuggesstions) {
@ -524,11 +552,13 @@ class QueryField extends React.Component<any, any> {
let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
const flattenedSuggestions = flattenSuggestions(suggestions);
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
const selectedKeys = flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : [];
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
i => (typeof i === 'object' ? i.text : i)
);
// Create typeahead in DOM root so we can later position it absolutely
return (
<Portal>
<Portal prefix={portalPrefix}>
<Typeahead
menuRef={this.menuRef}
selectedItems={selectedKeys}
@ -541,7 +571,7 @@ class QueryField extends React.Component<any, any> {
render() {
return (
<div className="query-field">
<div className="slate-query-field">
{this.renderMenu()}
<Editor
autoCorrect={false}

View File

@ -1,5 +1,6 @@
import React, { PureComponent } from 'react';
import promql from './slate-plugins/prism/promql';
import QueryField from './QueryField';
class QueryRow extends PureComponent<any, any> {
@ -55,12 +56,15 @@ class QueryRow extends PureComponent<any, any> {
<i className="fa fa-minus" />
</button>
</div>
<div className="query-field-wrapper">
<div className="slate-query-field-wrapper">
<QueryField
initialQuery={edited ? null : query}
portalPrefix="explore"
onPressEnter={this.handlePressEnter}
onQueryChange={this.handleChangeQuery}
placeholder="Enter a PromQL query"
prismLanguage="promql"
prismDefinition={promql}
request={request}
/>
</div>

View File

@ -23,12 +23,13 @@ class TypeaheadItem extends React.PureComponent<any, any> {
};
render() {
const { isSelected, label, onClickItem } = this.props;
const { hint, isSelected, label, onClickItem } = this.props;
const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
const onClick = () => onClickItem(label);
return (
<li ref={this.getRef} className={className} onClick={onClick}>
{label}
{hint && isSelected ? <div className="typeahead-item-hint">{hint}</div> : null}
</li>
);
}
@ -41,9 +42,19 @@ class TypeaheadGroup extends React.PureComponent<any, any> {
<li className="typeahead-group">
<div className="typeahead-group__title">{label}</div>
<ul className="typeahead-group__list">
{items.map(item => (
<TypeaheadItem key={item} onClickItem={onClickItem} isSelected={selected.indexOf(item) > -1} label={item} />
))}
{items.map(item => {
const text = typeof item === 'object' ? item.text : item;
const label = typeof item === 'object' ? item.display || item.text : item;
return (
<TypeaheadItem
key={text}
onClickItem={onClickItem}
isSelected={selected.indexOf(text) > -1}
hint={item.hint}
label={label}
/>
);
})}
</ul>
</li>
);

View File

@ -1,16 +1,12 @@
import React from 'react';
import Prism from 'prismjs';
import Promql from './promql';
Prism.languages.promql = Promql;
const TOKEN_MARK = 'prism-token';
export function configurePrismMetricsTokens(metrics) {
Prism.languages.promql.metric = {
alias: 'variable',
pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
export function setPrismTokens(language, field, values, alias = 'variable') {
Prism.languages[language][field] = {
alias,
pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`),
};
}
@ -21,7 +17,12 @@ export function configurePrismMetricsTokens(metrics) {
* (Adapted to handle nested grammar definitions.)
*/
export default function PrismPlugin() {
export default function PrismPlugin({ definition, language }) {
if (definition) {
// Don't override exising modified definitions
Prism.languages[language] = Prism.languages[language] || definition;
}
return {
/**
* Render a Slate mark with appropiate CSS class names
@ -54,7 +55,7 @@ export default function PrismPlugin() {
const texts = node.getTexts().toArray();
const tstring = texts.map(t => t.text).join('\n');
const grammar = Prism.languages.promql;
const grammar = Prism.languages[language];
const tokens = Prism.tokenize(tstring, grammar);
const decorations = [];
let startText = texts.shift();

View File

@ -1,11 +1,18 @@
import _ from 'lodash';
class Settings {
export interface BuildInfo {
version: string;
commit: string;
isEnterprise: boolean;
env: string;
}
export class Settings {
datasources: any;
panels: any;
appSubUrl: string;
window_title_prefix: string;
buildInfo: any;
buildInfo: BuildInfo;
new_panel_title: string;
bootData: any;
externalUserMngLinkUrl: string;
@ -32,7 +39,14 @@ class Settings {
playlist_timespan: '1m',
unsaved_changes_warning: true,
appSubUrl: '',
buildInfo: {
version: 'v1.0',
commit: '1',
env: 'production',
isEnterprise: false,
},
};
_.extend(this, defaults, options);
}
}

View File

@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl {
tagValuesPromise = this.$q.when(tag.values);
}
tagValuesPromise.then(values => {
return tagValuesPromise.then(values => {
tag.values = values;
tag.valuesText = values.join(' + ');
_.each(this.options, option => {
@ -132,7 +132,7 @@ export class ValueSelectDropdownCtrl {
this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
}
selectValue(option, event, commitChange, excludeOthers) {
selectValue(option, event, commitChange?, excludeOthers?) {
if (!option) {
return;
}

View File

@ -34,14 +34,10 @@ export class ContextSrv {
constructor() {
this.sidemenu = store.getBool('grafana.sidemenu', true);
if (!config.buildInfo) {
config.buildInfo = {};
}
if (!config.bootData) {
config.bootData = { user: {}, settings: {} };
}
this.version = config.buildInfo.version;
this.user = new User();
this.isSignedIn = this.user.isSignedIn;
this.isGrafanaAdmin = this.user.isGrafanaAdmin;

View File

@ -44,3 +44,38 @@ describe('when sorting table asc', () => {
expect(table.rows[2][1]).toBe(15);
});
});
describe('when sorting with nulls', () => {
var table;
var values;
beforeEach(() => {
table = new TableModel();
table.columns = [{}, {}];
table.rows = [[42, ''], [19, 'a'], [null, 'b'], [0, 'd'], [null, null], [2, 'c'], [0, null], [-8, '']];
});
it('numbers with nulls at end with asc sort', () => {
table.sort({ col: 0, desc: false });
values = table.rows.map(row => row[0]);
expect(values).toEqual([-8, 0, 0, 2, 19, 42, null, null]);
});
it('numbers with nulls at start with desc sort', () => {
table.sort({ col: 0, desc: true });
values = table.rows.map(row => row[0]);
expect(values).toEqual([null, null, 42, 19, 2, 0, 0, -8]);
});
it('strings with nulls at end with asc sort', () => {
table.sort({ col: 1, desc: false });
values = table.rows.map(row => row[1]);
expect(values).toEqual(['', '', 'a', 'b', 'c', 'd', null, null]);
});
it('strings with nulls at start with desc sort', () => {
table.sort({ col: 1, desc: true });
values = table.rows.map(row => row[1]);
expect(values).toEqual([null, null, 'd', 'c', 'b', 'a', '', '']);
});
});

View File

@ -119,6 +119,20 @@ describe('TimeSeries', function() {
series.getFlotPairs('null');
expect(series.stats.avg).toBe(null);
});
it('calculates timeStep', function() {
series = new TimeSeries({
datapoints: [[null, 1], [null, 2], [null, 3]],
});
series.getFlotPairs('null');
expect(series.stats.timeStep).toBe(1);
series = new TimeSeries({
datapoints: [[0, 1530529290], [0, 1530529305], [0, 1530529320]],
});
series.getFlotPairs('null');
expect(series.stats.timeStep).toBe(15);
});
});
describe('When checking if ms resolution is needed', function() {

View File

@ -0,0 +1,159 @@
import 'app/core/directives/value_select_dropdown';
import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown';
import q from 'q';
describe('SelectDropdownCtrl', () => {
let tagValuesMap: any = {};
ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn();
let ctrl;
describe('Given simple variable', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl(q);
ctrl.variable = {
current: { text: 'hej', value: 'hej' },
getValuesForTag: key => {
return Promise.resolve(tagValuesMap[key]);
},
};
ctrl.init();
});
it('Should init labelText and linkText', () => {
expect(ctrl.linkText).toBe('hej');
});
});
describe('Given variable with tags and dropdown is opened', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl(q);
ctrl.variable = {
current: { text: 'server-1', value: 'server-1' },
options: [
{ text: 'server-1', value: 'server-1', selected: true },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: key => {
return Promise.resolve(tagValuesMap[key]);
},
multi: true,
};
tagValuesMap.key1 = ['server-1', 'server-3'];
tagValuesMap.key2 = ['server-2', 'server-3'];
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
ctrl.init();
ctrl.show();
});
it('should init tags model', () => {
expect(ctrl.tags.length).toBe(3);
expect(ctrl.tags[0].text).toBe('key1');
});
it('should init options model', () => {
expect(ctrl.options.length).toBe(3);
});
it('should init selected values array', () => {
expect(ctrl.selectedValues.length).toBe(1);
});
it('should set linkText', () => {
expect(ctrl.linkText).toBe('server-1');
});
describe('after adititional value is selected', () => {
beforeEach(() => {
ctrl.selectValue(ctrl.options[2], {});
ctrl.commitChanges();
});
it('should update link text', () => {
expect(ctrl.linkText).toBe('server-1 + server-3');
});
});
describe('When tag is selected', () => {
beforeEach(async () => {
await ctrl.selectTag(ctrl.tags[0]);
ctrl.commitChanges();
});
it('should select tag', () => {
expect(ctrl.selectedTags.length).toBe(1);
});
it('should select values', () => {
expect(ctrl.options[0].selected).toBe(true);
expect(ctrl.options[2].selected).toBe(true);
});
it('link text should not include tag values', () => {
expect(ctrl.linkText).toBe('');
});
describe('and then dropdown is opened and closed without changes', () => {
beforeEach(() => {
ctrl.show();
ctrl.commitChanges();
});
it('should still have selected tag', () => {
expect(ctrl.selectedTags.length).toBe(1);
});
});
describe('and then unselected', () => {
beforeEach(async () => {
await ctrl.selectTag(ctrl.tags[0]);
});
it('should deselect tag', () => {
expect(ctrl.selectedTags.length).toBe(0);
});
});
describe('and then value is unselected', () => {
beforeEach(() => {
ctrl.selectValue(ctrl.options[0], {});
});
it('should deselect tag', () => {
expect(ctrl.selectedTags.length).toBe(0);
});
});
});
});
describe('Given variable with selected tags', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl(q);
ctrl.variable = {
current: {
text: 'server-1',
value: 'server-1',
tags: [{ text: 'key1', selected: true }],
},
options: [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: key => {
return Promise.resolve(tagValuesMap[key]);
},
multi: true,
};
ctrl.init();
ctrl.show();
});
it('should set tag as selected', () => {
expect(ctrl.tags[0].selected).toBe(true);
});
});
});

View File

@ -1,171 +0,0 @@
import { describe, beforeEach, it, expect, angularMocks, sinon } from 'test/lib/common';
import 'app/core/directives/value_select_dropdown';
describe('SelectDropdownCtrl', function() {
var scope;
var ctrl;
var tagValuesMap: any = {};
var rootScope;
var q;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(
angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) {
rootScope = $rootScope;
q = $q;
scope = $rootScope.$new();
ctrl = $controller('ValueSelectDropdownCtrl', { $scope: scope });
ctrl.onUpdated = sinon.spy();
$httpBackend.when('GET', /\.html$/).respond('');
})
);
describe('Given simple variable', function() {
beforeEach(function() {
ctrl.variable = {
current: { text: 'hej', value: 'hej' },
getValuesForTag: function(key) {
return q.when(tagValuesMap[key]);
},
};
ctrl.init();
});
it('Should init labelText and linkText', function() {
expect(ctrl.linkText).to.be('hej');
});
});
describe('Given variable with tags and dropdown is opened', function() {
beforeEach(function() {
ctrl.variable = {
current: { text: 'server-1', value: 'server-1' },
options: [
{ text: 'server-1', value: 'server-1', selected: true },
{ text: 'server-2', value: 'server-2' },
{ 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'];
tagValuesMap.key2 = ['server-2', 'server-3'];
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
ctrl.init();
ctrl.show();
});
it('should init tags model', function() {
expect(ctrl.tags.length).to.be(3);
expect(ctrl.tags[0].text).to.be('key1');
});
it('should init options model', function() {
expect(ctrl.options.length).to.be(3);
});
it('should init selected values array', function() {
expect(ctrl.selectedValues.length).to.be(1);
});
it('should set linkText', function() {
expect(ctrl.linkText).to.be('server-1');
});
describe('after adititional value is selected', function() {
beforeEach(function() {
ctrl.selectValue(ctrl.options[2], {});
ctrl.commitChanges();
});
it('should update link text', function() {
expect(ctrl.linkText).to.be('server-1 + server-3');
});
});
describe('When tag is selected', function() {
beforeEach(function() {
ctrl.selectTag(ctrl.tags[0]);
rootScope.$digest();
ctrl.commitChanges();
});
it('should select tag', function() {
expect(ctrl.selectedTags.length).to.be(1);
});
it('should select values', function() {
expect(ctrl.options[0].selected).to.be(true);
expect(ctrl.options[2].selected).to.be(true);
});
it('link text should not include tag values', function() {
expect(ctrl.linkText).to.be('');
});
describe('and then dropdown is opened and closed without changes', function() {
beforeEach(function() {
ctrl.show();
ctrl.commitChanges();
rootScope.$digest();
});
it('should still have selected tag', function() {
expect(ctrl.selectedTags.length).to.be(1);
});
});
describe('and then unselected', function() {
beforeEach(function() {
ctrl.selectTag(ctrl.tags[0]);
rootScope.$digest();
});
it('should deselect tag', function() {
expect(ctrl.selectedTags.length).to.be(0);
});
});
describe('and then value is unselected', function() {
beforeEach(function() {
ctrl.selectValue(ctrl.options[0], {});
});
it('should deselect tag', function() {
expect(ctrl.selectedTags.length).to.be(0);
});
});
});
});
describe('Given variable with selected tags', function() {
beforeEach(function() {
ctrl.variable = {
current: {
text: 'server-1',
value: 'server-1',
tags: [{ text: 'key1', selected: true }],
},
options: [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: function(key) {
return q.when(tagValuesMap[key]);
},
multi: true,
};
ctrl.init();
ctrl.show();
});
it('should set tag as selected', function() {
expect(ctrl.tags[0].selected).to.be(true);
});
});
});

View File

@ -19,23 +19,16 @@ export default class TableModel {
this.rows.sort(function(a, b) {
a = a[options.col];
b = b[options.col];
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
// Sort null or undefined seperately from comparable values
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
});
this.columns[options.col].sort = true;
if (options.desc) {
this.rows.reverse();
this.columns[options.col].desc = true;
} else {
this.columns[options.col].desc = false;
}
this.columns[options.col].sort = true;
this.columns[options.col].desc = options.desc;
}
addColumn(col) {

View File

@ -1,17 +1,17 @@
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import '../annotations_srv';
import helpers from 'test/specs/helpers';
import 'app/features/dashboard/time_srv';
import { AnnotationsSrv } from '../annotations_srv';
describe('AnnotationsSrv', function() {
var ctx = new helpers.ServiceTestContext();
let $rootScope = {
onAppEvent: jest.fn(),
};
let $q;
let datasourceSrv;
let backendSrv;
let timeSrv;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.createService('timeSrv'));
beforeEach(() => {
ctx.createService('annotationsSrv');
});
let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv);
describe('When translating the query result', () => {
const annotationSource = {
@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() {
let translatedAnnotations;
beforeEach(() => {
translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations);
translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations);
});
it('should set defaults', () => {
expect(translatedAnnotations[0].source).to.eql(annotationSource);
expect(translatedAnnotations[0].source).toEqual(annotationSource);
});
});
});

View File

@ -86,9 +86,7 @@ describe('given dashboard with repeated panels', () => {
],
};
config.buildInfo = {
version: '3.0.2',
};
config.buildInfo.version = '3.0.2';
//Stubs test function calls
var datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };

View File

@ -0,0 +1,67 @@
//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import 'app/features/dashboard/view_state_srv';
import config from 'app/core/config';
import { DashboardViewState } from '../view_state_srv';
describe('when updating view state', () => {
let location = {
replace: jest.fn(),
search: jest.fn(),
};
let $scope = {
onAppEvent: jest.fn(() => {}),
dashboard: {
meta: {},
panels: [],
},
};
let $rootScope = {};
let viewState;
beforeEach(() => {
config.bootData = {
user: {
orgId: 1,
},
};
});
describe('to fullscreen true and edit true', () => {
beforeEach(() => {
location.search = jest.fn(() => {
return { fullscreen: true, edit: true, panelId: 1 };
});
viewState = new DashboardViewState($scope, location, {}, $rootScope);
});
it('should update querystring and view state', () => {
var updateState = { fullscreen: true, edit: true, panelId: 1 };
viewState.update(updateState);
expect(location.search).toHaveBeenCalledWith({
edit: true,
editview: null,
fullscreen: true,
orgId: 1,
panelId: 1,
});
expect(viewState.dashboard.meta.fullscreen).toBe(true);
expect(viewState.state.fullscreen).toBe(true);
});
});
describe('to fullscreen false', () => {
beforeEach(() => {
viewState = new DashboardViewState($scope, location, {}, $rootScope);
});
it('should remove params from query string', () => {
viewState.update({ fullscreen: true, panelId: 1, edit: true });
viewState.update({ fullscreen: false });
expect(viewState.dashboard.meta.fullscreen).toBe(false);
expect(viewState.state.fullscreen).toBe(null);
});
});
});

View File

@ -1,65 +0,0 @@
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import 'app/features/dashboard/view_state_srv';
import config from 'app/core/config';
describe('when updating view state', function() {
var viewState, location;
var timeSrv = {};
var templateSrv = {};
var contextSrv = {
user: {
orgId: 19,
},
};
beforeEach(function() {
config.bootData = {
user: {
orgId: 1,
},
};
});
beforeEach(angularMocks.module('grafana.services'));
beforeEach(
angularMocks.module(function($provide) {
$provide.value('timeSrv', timeSrv);
$provide.value('templateSrv', templateSrv);
$provide.value('contextSrv', contextSrv);
})
);
beforeEach(
angularMocks.inject(function(dashboardViewStateSrv, $location, $rootScope) {
$rootScope.onAppEvent = function() {};
$rootScope.dashboard = {
meta: {},
panels: [],
};
viewState = dashboardViewStateSrv.create($rootScope);
location = $location;
})
);
describe('to fullscreen true and edit true', function() {
it('should update querystring and view state', function() {
var updateState = { fullscreen: true, edit: true, panelId: 1 };
viewState.update(updateState);
expect(location.search()).to.eql({
fullscreen: true,
edit: true,
panelId: 1,
orgId: 1,
});
expect(viewState.dashboard.meta.fullscreen).to.be(true);
expect(viewState.state.fullscreen).to.be(true);
});
});
describe('to fullscreen false', function() {
it('should remove params from query string', function() {
viewState.update({ fullscreen: true, panelId: 1, edit: true });
viewState.update({ fullscreen: false });
expect(viewState.dashboard.meta.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(null);
});
});
});

View File

@ -41,20 +41,20 @@ function dashLink($compile, $sanitize, linkSrv) {
elem.html(template);
$compile(elem.contents())(scope);
var anchor = elem.find('a');
var icon = elem.find('i');
var span = elem.find('span');
function update() {
var linkInfo = linkSrv.getAnchorInfo(link);
const anchor = elem.find('a');
const span = elem.find('span');
span.text(linkInfo.title);
if (!link.asDropdown) {
anchor.attr('href', linkInfo.href);
sanitizeAnchor();
}
elem.find('a').attr('data-placement', 'bottom');
anchor.attr('data-placement', 'bottom');
// tooltip
elem.find('a').tooltip({
anchor.tooltip({
title: $sanitize(scope.link.tooltip),
html: true,
container: 'body',
@ -62,12 +62,13 @@ function dashLink($compile, $sanitize, linkSrv) {
}
function sanitizeAnchor() {
const anchor = elem.find('a');
const anchorSanitized = $sanitize(anchor.parent().html());
anchor.parent().html(anchorSanitized);
}
icon.attr('class', 'fa fa-fw ' + scope.link.icon);
anchor.attr('target', scope.link.target);
elem.find('i').attr('class', 'fa fa-fw ' + scope.link.icon);
elem.find('a').attr('target', scope.link.target);
// fix for menus on the far right
if (link.asDropdown && scope.$last) {

Some files were not shown because too many files have changed in this diff Show More