Merge branch 'master' into v52_merge_master

This commit is contained in:
Marcus Efraimsson
2018-06-18 09:44:39 +02:00
81 changed files with 10251 additions and 4860 deletions
+10 -5
View File
@@ -183,16 +183,21 @@ jobs:
command: 'sudo pip install awscli' command: 'sudo pip install awscli'
- run: - run:
name: deploy to s3 name: deploy to s3
command: 'aws s3 sync ./dist s3://$BUCKET_NAME/master' command: |
# Also
cp dist/grafana-latest.linux-x64.tar.gz dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
aws s3 sync ./dist s3://$BUCKET_NAME/master
- run: - run:
name: Trigger Windows build name: Trigger Windows build
command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master' command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
- run: - run:
name: Trigger Docker build name: Trigger Docker build
command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}' command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)'
- run: - run:
name: Publish to Grafana.com name: Publish to Grafana.com
command: './scripts/publish -apiKey ${GRAFANA_COM_API_KEY}' command: |
rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
deploy-release: deploy-release:
docker: docker:
@@ -241,8 +246,8 @@ workflows:
- mysql-integration-test - mysql-integration-test
- postgres-integration-test - postgres-integration-test
filters: filters:
branches: branches:
only: master only: master
release: release:
jobs: jobs:
- build-all: - build-all:
-3
View File
@@ -11,8 +11,5 @@ dump.rdb
node_modules node_modules
/local /local
/tmp /tmp
/vendor
*.yml *.yml
*.md *.md
/vendor
/tmp
+18
View File
@@ -2,6 +2,24 @@
### New Features ### New Features
* **Dashboard**: Import dashboard to folder [#10796](https://github.com/grafana/grafana/issues/10796)
### Minor
* **Dashboard**: Fix so panel titles doesn't wrap [#11074](https://github.com/grafana/grafana/issues/11074)
* **Dashboard**: Prevent double-click when saving dashboard [#11963](https://github.com/grafana/grafana/issues/11963)
* **Dashboard**: AutoFocus the add-panel search filter [#12189](https://github.com/grafana/grafana/pull/12189) thx [@ryantxu](https://github.com/ryantxu)
* **Units**: W/m2 (energy), l/h (flow) and kPa (pressure) [#11233](https://github.com/grafana/grafana/pull/11233), thx [@flopp999](https://github.com/flopp999)
* **Units**: Litre/min (flow) and milliLitre/min (flow) [#12282](https://github.com/grafana/grafana/pull/12282), thx [@flopp999](https://github.com/flopp999)
* **Alerting**: Fix mobile notifications for Microsoft Teams alert notifier [#11484](https://github.com/grafana/grafana/pull/11484), thx [@manacker](https://github.com/manacker)
* **Influxdb**: Add support for mode function [#12286](https://github.com/grafana/grafana/issues/12286)
* **Cloudwatch**: Fixes panic caused by bad timerange settings [#12199](https://github.com/grafana/grafana/issues/12199)
* **Auth Proxy**: Whitelist proxy IP address instead of client IP address [#10707](https://github.com/grafana/grafana/issues/10707)
# 5.2.0-beta1 (2018-06-05)
### New Features
* **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95) * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
* **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882) * **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) * **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)
Generated
+3 -3
View File
@@ -331,8 +331,8 @@
[[projects]] [[projects]]
name = "github.com/mattn/go-sqlite3" name = "github.com/mattn/go-sqlite3"
packages = ["."] packages = ["."]
revision = "6c771bb9887719704b210e87e934f08be014bdb1" revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
version = "v1.6.0" version = "v1.7.0"
[[projects]] [[projects]]
name = "github.com/matttproud/golang_protobuf_extensions" name = "github.com/matttproud/golang_protobuf_extensions"
@@ -670,6 +670,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "08b97771990365d506af4788acb33cdf283ce89856669262ecb84860ad45bfcb" inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1
+1 -1
View File
@@ -129,7 +129,7 @@ ignored = [
[[constraint]] [[constraint]]
name = "github.com/mattn/go-sqlite3" name = "github.com/mattn/go-sqlite3"
version = "1.6.0" version = "1.7.0"
[[constraint]] [[constraint]]
name = "github.com/opentracing/opentracing-go" name = "github.com/opentracing/opentracing-go"
+1 -1
View File
@@ -76,7 +76,7 @@ Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://gith
> This feature is available from v5.0 > This feature is available from v5.0
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list. It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `deleteDatasources`. Grafana will delete datasources listed in `deleteDatasources` before inserting/updating those in the `datasource` list.
### Running Multiple Grafana Instances ### Running Multiple Grafana Instances
@@ -20,7 +20,7 @@ queries through the use of query references.
## Adding the data source ## Adding the data source
1. Open the side menu by clicking the Grafana icon in the top header. 1. Open the side menu by clicking the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. 2. In the side menu under the `Configuration` link you should find a link named `Data Sources`.
3. Click the `+ Add data source` button in the top header. 3. Click the `+ Add data source` button in the top header.
4. Select `Graphite` from the *Type* dropdown. 4. Select `Graphite` from the *Type* dropdown.
+1 -1
View File
@@ -115,7 +115,7 @@ Grafana v5.1 brings an improved workflow for provisioned dashboards:
Available options in the dialog will let you `Copy JSON to Clipboard` and/or `Save JSON to file` which can help you synchronize your dashboard changes back to the provisioning source. Available options in the dialog will let you `Copy JSON to Clipboard` and/or `Save JSON to file` which can help you synchronize your dashboard changes back to the provisioning source.
More information in the [Provisioning documentation](/features/datasources/prometheus/). More information in the [Provisioning documentation](/administration/provisioning/).
<div class="clearfix"></div> <div class="clearfix"></div>
+21
View File
@@ -331,6 +331,27 @@ Content-Type: application/json
{"message":"Organization updated"} {"message":"Organization updated"}
``` ```
## Delete Organisation
`DELETE /api/orgs/:orgId`
**Example Request**:
```http
DELETE /api/orgs/1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{"message":"Organization deleted"}
```
## Get Users in Organisation ## Get Users in Organisation
`GET /api/orgs/:orgId/users` `GET /api/orgs/:orgId/users`
+7
View File
@@ -49,6 +49,11 @@ $ docker run \
grafana/grafana:5.1.0 grafana/grafana:5.1.0
``` ```
## Running of the master branch
For every successful commit we publish a Grafana container to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/). In `grafana/grafana` container we will always overwrite the `master` tag with the latest version. In `grafana/grafana-dev` we will include
the git commit in the tag. If you run Grafana master in production we **strongly** recommend that you use the later since different machines might run different version of grafana if they pull the master tag at different times.
## Installing Plugins for Grafana ## Installing Plugins for Grafana
Pass the plugins you want installed to docker with the `GF_INSTALL_PLUGINS` environment variable as a comma separated list. This will pass each plugin name to `grafana-cli plugins install ${plugin}` and install them when Grafana starts. Pass the plugins you want installed to docker with the `GF_INSTALL_PLUGINS` environment variable as a comma separated list. This will pass each plugin name to `grafana-cli plugins install ${plugin}` and install them when Grafana starts.
@@ -132,6 +137,8 @@ docker run -d --user $ID --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 graf
## Reading secrets from files (support for Docker Secrets) ## Reading secrets from files (support for Docker Secrets)
> Available in v5.2.0 and later
It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/<name of secret>` of the container. It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/<name of secret>` of the container.
You can do this with any of the configuration options in conf/grafana.ini by setting `GF_<SectionName>_<KeyName>_FILE` to the path of the file holding the secret. You can do this with any of the configuration options in conf/grafana.ini by setting `GF_<SectionName>_<KeyName>_FILE` to the path of the file holding the secret.
@@ -25,7 +25,6 @@ To interact with the rest of grafana the plugins module file can export 5 differ
- Datasource (Required) - Datasource (Required)
- QueryCtrl (Required) - QueryCtrl (Required)
- ConfigCtrl (Required) - ConfigCtrl (Required)
- QueryOptionsCtrl
- AnnotationsQueryCtrl - AnnotationsQueryCtrl
## Plugin json ## Plugin json
@@ -182,12 +181,6 @@ A JavaScript class that will be instantiated and treated as an Angular controlle
Requires a static template or templateUrl variable which will be rendered as the view for this controller. Requires a static template or templateUrl variable which will be rendered as the view for this controller.
## QueryOptionsCtrl
A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource, such as interval, rate and aggregations if needed.
Requires a static template or templateUrl variable which will be rendered as the view for this controller.
## AnnotationsQueryCtrl ## AnnotationsQueryCtrl
A JavaScript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard. A JavaScript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard.
+4 -4
View File
@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
## Dependencies ## Dependencies
- [Go 1.9.2](https://golang.org/dl/) - [Go 1.10](https://golang.org/dl/)
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [NodeJS LTS](https://nodejs.org/download/) - [NodeJS LTS](https://nodejs.org/download/)
- node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details. - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
@@ -66,13 +66,13 @@ You can run a local instance of Grafana by running:
```bash ```bash
./bin/grafana-server ./bin/grafana-server
``` ```
If you built the binary with `go run build.go build`, run `./bin/grafana-server` Or, if you built the binary with `go run build.go build`, run `./bin/<os>-<architecture>/grafana-server`
If you built it with `go build .`, run `./grafana` If you built it with `go build .`, run `./grafana`
Open grafana in your browser (default [http://localhost:3000](http://localhost:3000)) and login with admin user (default user/pass = admin/admin). Open grafana in your browser (default [http://localhost:3000](http://localhost:3000)) and login with admin user (default user/pass = admin/admin).
## Developing Grafana # Developing Grafana
To add features, customize your config, etc, you'll need to rebuild the backend when you change the source code. We use a tool named `bra` that To add features, customize your config, etc, you'll need to rebuild the backend when you change the source code. We use a tool named `bra` that
does this. does this.
@@ -124,7 +124,7 @@ Learn more about Grafana config options in the [Configuration section](/installa
## Create a pull requests ## Create a pull requests
Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome. Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome.
## Troubleshooting # Troubleshooting
**Problem**: PhantomJS or node-sass errors when running grunt **Problem**: PhantomJS or node-sass errors when running grunt
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"stable": "5.0.4", "stable": "5.1.3",
"testing": "5.0.4" "testing": "5.1.3"
} }
+1
View File
@@ -57,4 +57,5 @@ type ImportDashboardCommand struct {
Overwrite bool `json:"overwrite"` Overwrite bool `json:"overwrite"`
Dashboard *simplejson.Json `json:"dashboard"` Dashboard *simplejson.Json `json:"dashboard"`
Inputs []plugins.ImportDashboardInput `json:"inputs"` Inputs []plugins.ImportDashboardInput `json:"inputs"`
FolderId int64 `json:"folderId"`
} }
+1
View File
@@ -140,6 +140,7 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
"authProxyEnabled": setting.AuthProxyEnabled, "authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled, "ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled, "alertingEnabled": setting.AlertingEnabled,
"exploreEnabled": setting.ExploreEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId, "googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm, "disableLoginForm": setting.DisableLoginForm,
"externalUserMngInfo": setting.ExternalUserMngInfo, "externalUserMngInfo": setting.ExternalUserMngInfo,
+2 -1
View File
@@ -99,9 +99,10 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
children = append(children, &dtos.NavLink{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"}) children = append(children, &dtos.NavLink{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"})
children = append(children, &dtos.NavLink{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"})
} }
children = append(children, &dtos.NavLink{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"})
data.NavTree = append(data.NavTree, &dtos.NavLink{ data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Create", Text: "Create",
Id: "create", Id: "create",
+20 -8
View File
@@ -25,12 +25,9 @@ import (
) )
var ( var (
logger = log.New("data-proxy-log") logger = log.New("data-proxy-log")
client = &http.Client{ tokenCache = map[string]*jwtToken{}
Timeout: time.Second * 30, client = newHTTPClient()
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
tokenCache = map[int64]*jwtToken{}
) )
type jwtToken struct { type jwtToken struct {
@@ -48,6 +45,10 @@ type DataSourceProxy struct {
plugin *plugins.DataSourcePlugin plugin *plugins.DataSourcePlugin
} }
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy { func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
targetURL, _ := url.Parse(ds.Url) targetURL, _ := url.Parse(ds.Url)
@@ -60,6 +61,13 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
} }
} }
func newHTTPClient() httpClient {
return &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
}
func (proxy *DataSourceProxy) HandleRequest() { func (proxy *DataSourceProxy) HandleRequest() {
if err := proxy.validateRequest(); err != nil { if err := proxy.validateRequest(); err != nil {
proxy.ctx.JsonApiErr(403, err.Error(), nil) proxy.ctx.JsonApiErr(403, err.Error(), nil)
@@ -311,7 +319,7 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
} }
func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) { func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
if cachedToken, found := tokenCache[proxy.ds.Id]; found { if cachedToken, found := tokenCache[proxy.getAccessTokenCacheKey()]; found {
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) { if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
logger.Info("Using token from cache") logger.Info("Using token from cache")
return cachedToken.AccessToken, nil return cachedToken.AccessToken, nil
@@ -350,12 +358,16 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64) expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
token.ExpiresOn = time.Unix(expiresOnEpoch, 0) token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
tokenCache[proxy.ds.Id] = &token tokenCache[proxy.getAccessTokenCacheKey()] = &token
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn) logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
return token.AccessToken, nil return token.AccessToken, nil
} }
func (proxy *DataSourceProxy) getAccessTokenCacheKey() string {
return fmt.Sprintf("%v_%v_%v", proxy.ds.Id, proxy.route.Path, proxy.route.Method)
}
func interpolateString(text string, data templateData) (string, error) { func interpolateString(text string, data templateData) (string, error) {
t, err := template.New("content").Parse(text) t, err := template.New("content").Parse(text)
if err != nil { if err != nil {
+134
View File
@@ -1,9 +1,13 @@
package pluginproxy package pluginproxy
import ( import (
"bytes"
"fmt"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"time"
macaron "gopkg.in/macaron.v1" macaron "gopkg.in/macaron.v1"
@@ -100,6 +104,112 @@ func TestDSRouteRule(t *testing.T) {
}) })
}) })
Convey("Plugin with multiple routes for token auth", func() {
plugin := &plugins.DataSourcePlugin{
Routes: []*plugins.AppPluginRoute{
{
Path: "pathwithtoken1",
Url: "https://api.nr1.io/some/path",
TokenAuth: &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Params: map[string]string{
"grant_type": "client_credentials",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://api.nr1.io",
},
},
},
{
Path: "pathwithtoken2",
Url: "https://api.nr2.io/some/path",
TokenAuth: &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Params: map[string]string{
"grant_type": "client_credentials",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://api.nr2.io",
},
},
},
},
}
setting.SecretKey = "password"
key, _ := util.Encrypt([]byte("123"), "password")
ds := &m.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"clientId": "asd",
"tenantId": "mytenantId",
}),
SecureJsonData: map[string][]byte{
"clientSecret": key,
},
}
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
ctx := &m.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{Request: req},
},
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
}
Convey("When creating and caching access tokens", func() {
var authorizationHeaderCall1 string
var authorizationHeaderCall2 string
Convey("first call should add authorization header with access token", func() {
json, err := ioutil.ReadFile("./test-data/access-token-1.json")
So(err, ShouldBeNil)
client = newFakeHTTPClient(json)
proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
proxy1.route = plugin.Routes[0]
proxy1.applyRoute(req)
authorizationHeaderCall1 = req.Header.Get("Authorization")
So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
Convey("second call to another route should add a different access token", func() {
json2, err := ioutil.ReadFile("./test-data/access-token-2.json")
So(err, ShouldBeNil)
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
client = newFakeHTTPClient(json2)
proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2")
proxy2.route = plugin.Routes[1]
proxy2.applyRoute(req)
authorizationHeaderCall2 = req.Header.Get("Authorization")
So(req.URL.String(), ShouldEqual, "https://api.nr2.io/some/path")
So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall2, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall2, ShouldNotEqual, authorizationHeaderCall1)
Convey("third call to first route should add cached access token", func() {
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
client = newFakeHTTPClient([]byte{})
proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
proxy3.route = plugin.Routes[0]
proxy3.applyRoute(req)
authorizationHeaderCall3 := req.Header.Get("Authorization")
So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall3, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall3, ShouldEqual, authorizationHeaderCall1)
})
})
})
})
})
Convey("When proxying graphite", func() { Convey("When proxying graphite", func() {
plugin := &plugins.DataSourcePlugin{} plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
@@ -214,3 +324,27 @@ func TestDSRouteRule(t *testing.T) {
}) })
} }
type httpClientStub struct {
fakeBody []byte
}
func (c *httpClientStub) Do(req *http.Request) (*http.Response, error) {
bodyJSON, _ := simplejson.NewJson(c.fakeBody)
_, passedTokenCacheTest := bodyJSON.CheckGet("expires_on")
So(passedTokenCacheTest, ShouldBeTrue)
bodyJSON.Set("expires_on", fmt.Sprint(time.Now().Add(time.Second*60).Unix()))
body, _ := bodyJSON.MarshalJSON()
resp := &http.Response{
Body: ioutil.NopCloser(bytes.NewReader(body)),
}
return resp, nil
}
func newFakeHTTPClient(fakeBody []byte) httpClient {
return &httpClientStub{
fakeBody: fakeBody,
}
}
@@ -0,0 +1,9 @@
{
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "0",
"expires_on": "1528740417",
"not_before": "1528736517",
"resource": "https://api.nr1.io",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJodHRwczovL2FwaS5sb2dhbmFseXRpY3MuaW8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9lN2YzZjY2MS1hOTMzLTRiM2YtODE3Ni01MWM0Zjk4MmVjNDgvIiwiaWF0IjoxNTI4NzM2NTE3LCJuYmYiOjE1Mjg3MzY1MTcsImV4cCI6MTUyODc0MDQxNywiYWlvIjoiWTJkZ1lBaStzaWRsT3NmQ2JicGhLMSsremttN0NBQT0iLCJhcHBpZCI6IjdmMzJkYjdjLTZmNmYtNGU4OC05M2Q5LTlhZTEyNmMwYTU1ZiIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2U3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OC8iLCJvaWQiOiI1NDQ5ZmJjOS1mYWJhLTRkNjItODE2Yy05ZmMwMzZkMWViN2UiLCJzdWIiOiI1NDQ5ZmJjOS1mYWJhLTRkNjItODE2Yy05ZmMwMzZkMWViN2UiLCJ0aWQiOiJlN2YzZjY2MS1hOTMzLTRiM2YtODE3Ni01MWM0Zjk4MmVjNDgiLCJ1dGkiOiJZQTlQa2lxUy1VV1hMQjhIRnU0U0FBIiwidmVyIjoiMS4wIn0.ga5qudt4LDMKTStAxUmzjyZH8UFBAaFirJqpTdmYny4NtkH6JT2EILvjTjYxlKeTQisvwx9gof0PyicZIab9d6wlMa2xiLzr2nmaOonYClY8fqBaRTgc1xVjrKFw5SCgpx3FnEyJhIWvVPIfaWaogSHcQbIpe4kdk4tz-ccmrx0D1jsziSI4BZcJcX04aJuHZGz9k4mQZ_AA5sQSeQaNuojIng6rYoIifAXFYBZPTbeeeqmiGq8v0IOLeNKbC0POeQCJC_KKBG6Z_MV2KgPxFEzQuX2ZFmRD_wGPteV5TUBxh1kARdqexA3e0zAKSawR9kmrAiZ21lPr4tX2Br_HDg"
}
@@ -0,0 +1,9 @@
{
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "0",
"expires_on": "1528662059",
"not_before": "1528658159",
"resource": "https://api.nr2.io",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2U3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OC8iLCJpYXQiOjE1Mjg2NTgxNTksIm5iZiI6MTUyODY1ODE1OSwiZXhwIjoxNTI4NjYyMDU5LCJhaW8iOiJZMmRnWUFpK3NpZGxPc2ZDYmJwaEsxKyt6a203Q0FBPSIsImFwcGlkIjoiODg5YjdlZDgtMWFlZC00ODZlLTk3ODktODE5NzcwYmJiNjFhIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZTdmM2Y2NjEtYTkzMy00YjNmLTgxNzYtNTFjNGY5ODJlYzQ4LyIsIm9pZCI6IjY0YzQxNjMyLTliOWUtNDczNy05MTYwLTBlNjAzZTg3NjljYyIsInN1YiI6IjY0YzQxNjMyLTliOWUtNDczNy05MTYwLTBlNjAzZTg3NjljYyIsInRpZCI6ImU3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OCIsInV0aSI6IkQ1ODZHSGUySDBPd0ptOU0xeVlKQUEiLCJ2ZXIiOiIxLjAifQ.Pw8c8gpoZptw3lGreQoHQaMVOozSaTE5D38Vm2aCHRB3DvD3N-Qcm1x0ZCakUEV2sJd7jvx4XtPFuW7063T0V1deExL4rzzvIo0ZfMmURf9tCTiKFKYibqf8_PtfPSz0t9eNDEUGmWDh1Wgssb4W_H-wPqgl9VPMT7T6ynkfIm0-ODPZTBzgSHiY8C_L1-DkhsK7XiqbUlSDgx9FpfChZS3ah8QhA8geqnb_HVuSktg7WhpxmogSpK5QdrwSE3jsbItpzOfLJ4iBd2ExzS2C0y8H_Coluk3Y1YA07tAxJ6Y7oBv-XwGqNfZhveOCQOzX-U3dFod3fXXysjB0UB89WQ"
}
+1
View File
@@ -174,6 +174,7 @@ func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Respon
Path: apiCmd.Path, Path: apiCmd.Path,
Inputs: apiCmd.Inputs, Inputs: apiCmd.Inputs,
Overwrite: apiCmd.Overwrite, Overwrite: apiCmd.Overwrite,
FolderId: apiCmd.FolderId,
Dashboard: apiCmd.Dashboard, Dashboard: apiCmd.Dashboard,
} }
+12 -8
View File
@@ -2,7 +2,6 @@ package middleware
import ( import (
"fmt" "fmt"
"net"
"net/mail" "net/mail"
"reflect" "reflect"
"strings" "strings"
@@ -29,7 +28,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
} }
// if auth proxy ip(s) defined, check if request comes from one of those // if auth proxy ip(s) defined, check if request comes from one of those
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil { if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil {
ctx.Handle(407, "Proxy authentication required", err) ctx.Handle(407, "Proxy authentication required", err)
return true return true
} }
@@ -197,18 +196,23 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
return nil return nil
} }
proxies := strings.Split(setting.AuthProxyWhitelist, ",") // Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy
sourceIP, _, err := net.SplitHostPort(remoteAddr) if strings.Contains(remoteAddr, ",") {
if err != nil { sourceIPs := strings.Split(remoteAddr, ",")
return err remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1])
} }
remoteAddr = strings.TrimPrefix(remoteAddr, "[")
remoteAddr = strings.TrimSuffix(remoteAddr, "]")
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
// Compare allowed IP addresses to actual address // Compare allowed IP addresses to actual address
for _, proxyIP := range proxies { for _, proxyIP := range proxies {
if sourceIP == strings.TrimSpace(proxyIP) { if remoteAddr == strings.TrimSpace(proxyIP) {
return nil return nil
} }
} }
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP) return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr)
} }
+55
View File
@@ -293,6 +293,61 @@ 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) { middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER" setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
+3 -1
View File
@@ -16,6 +16,7 @@ type ImportDashboardCommand struct {
Path string Path string
Inputs []ImportDashboardInput Inputs []ImportDashboardInput
Overwrite bool Overwrite bool
FolderId int64
OrgId int64 OrgId int64
User *m.SignedInUser User *m.SignedInUser
@@ -70,7 +71,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
UserId: cmd.User.UserId, UserId: cmd.User.UserId,
Overwrite: cmd.Overwrite, Overwrite: cmd.Overwrite,
PluginId: cmd.PluginId, PluginId: cmd.PluginId,
FolderId: dashboard.FolderId, FolderId: cmd.FolderId,
} }
dto := &dashboards.SaveDashboardDTO{ dto := &dashboards.SaveDashboardDTO{
@@ -91,6 +92,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
Title: savedDash.Title, Title: savedDash.Title,
Path: cmd.Path, Path: cmd.Path,
Revision: savedDash.Data.Get("revision").MustInt64(1), Revision: savedDash.Data.Get("revision").MustInt64(1),
FolderId: savedDash.FolderId,
ImportedUri: "db/" + savedDash.Slug, ImportedUri: "db/" + savedDash.Slug,
ImportedUrl: savedDash.GetUrl(), ImportedUrl: savedDash.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1), ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
+1
View File
@@ -17,6 +17,7 @@ type PluginDashboardInfoDTO struct {
ImportedUrl string `json:"importedUrl"` ImportedUrl string `json:"importedUrl"`
Slug string `json:"slug"` Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
FolderId int64 `json:"folderId"`
ImportedRevision int64 `json:"importedRevision"` ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"` Revision int64 `json:"revision"`
Description string `json:"description"` Description string `json:"description"`
+10 -12
View File
@@ -41,10 +41,8 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
type TeamsNotifier struct { type TeamsNotifier struct {
NotifierBase NotifierBase
Url string Url string
Recipient string log log.Logger
Mention string
log log.Logger
} }
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
@@ -75,17 +73,17 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
}) })
} }
message := this.Mention message := ""
if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok. if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message += " " + evalContext.Rule.Message message = evalContext.Rule.Message
} else {
message += " " // summary must not be empty
} }
body := map[string]interface{}{ body := map[string]interface{}{
"@type": "MessageCard", "@type": "MessageCard",
"@context": "http://schema.org/extensions", "@context": "http://schema.org/extensions",
"summary": message, // summary MUST not be empty or the webhook request fails
// summary SHOULD contain some meaningful information, since it is used for mobile notifications
"summary": evalContext.GetNotificationTitle(),
"title": evalContext.GetNotificationTitle(), "title": evalContext.GetNotificationTitle(),
"themeColor": evalContext.GetStateModel().Color, "themeColor": evalContext.GetStateModel().Color,
"sections": []map[string]interface{}{ "sections": []map[string]interface{}{
+1 -1
View File
@@ -89,7 +89,7 @@ func (ss *SqlStore) ensureAdminUser() error {
systemUserCountQuery := m.GetSystemUserCountStatsQuery{} systemUserCountQuery := m.GetSystemUserCountStatsQuery{}
if err := bus.Dispatch(&systemUserCountQuery); err != nil { if err := bus.Dispatch(&systemUserCountQuery); err != nil {
fmt.Errorf("Could not determine if admin user exists: %v", err) return fmt.Errorf("Could not determine if admin user exists: %v", err)
} }
if systemUserCountQuery.Result.Count > 0 { if systemUserCountQuery.Result.Count > 0 {
+5
View File
@@ -3,6 +3,7 @@ package cloudwatch
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
@@ -144,6 +145,10 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl
return nil, err return nil, err
} }
if endTime.Before(startTime) {
return nil, fmt.Errorf("Invalid time range: End time can't be before start time")
}
params := &cloudwatch.GetMetricStatisticsInput{ params := &cloudwatch.GetMetricStatisticsInput{
Namespace: aws.String(query.Namespace), Namespace: aws.String(query.Namespace),
MetricName: aws.String(query.MetricName), MetricName: aws.String(query.MetricName),
+2 -2
View File
@@ -43,12 +43,12 @@ type Client interface {
var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb.TimeRange) (Client, error) { var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb.TimeRange) (Client, error) {
version, err := ds.JsonData.Get("esVersion").Int() version, err := ds.JsonData.Get("esVersion").Int()
if err != nil { if err != nil {
return nil, fmt.Errorf("eleasticsearch version is required, err=%v", err) return nil, fmt.Errorf("elasticsearch version is required, err=%v", err)
} }
timeField, err := ds.JsonData.Get("timeField").String() timeField, err := ds.JsonData.Get("timeField").String()
if err != nil { if err != nil {
return nil, fmt.Errorf("eleasticsearch time field name is required, err=%v", err) return nil, fmt.Errorf("elasticsearch time field name is required, err=%v", err)
} }
indexInterval := ds.JsonData.Get("interval").MustString() indexInterval := ds.JsonData.Get("interval").MustString()
+1
View File
@@ -31,6 +31,7 @@ func init() {
renders["mean"] = QueryDefinition{Renderer: functionRenderer} renders["mean"] = QueryDefinition{Renderer: functionRenderer}
renders["median"] = QueryDefinition{Renderer: functionRenderer} renders["median"] = QueryDefinition{Renderer: functionRenderer}
renders["sum"] = QueryDefinition{Renderer: functionRenderer} renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{ renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer, Renderer: functionRenderer,
+29 -75
View File
@@ -4,85 +4,39 @@ import (
"testing" "testing"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
) )
func TestInfluxdbQueryPart(t *testing.T) { func TestInfluxdbQueryPart(t *testing.T) {
Convey("Influxdb query parts", t, func() { tcs := []struct {
mode string
input string
params []string
expected string
}{
{mode: "field", params: []string{"value"}, input: "value", expected: `"value"`},
{mode: "derivative", params: []string{"10s"}, input: "mean(value)", expected: `derivative(mean(value), 10s)`},
{mode: "bottom", params: []string{"3"}, input: "value", expected: `bottom(value, 3)`},
{mode: "time", params: []string{"$interval"}, input: "", expected: `time($interval)`},
{mode: "time", params: []string{"auto"}, input: "", expected: `time($__interval)`},
{mode: "spread", params: []string{}, input: "value", expected: `spread(value)`},
{mode: "math", params: []string{"/ 100"}, input: "mean(value)", expected: `mean(value) / 100`},
{mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`},
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
}
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
query := &Query{} query := &Query{}
Convey("render field ", func() { for _, tc := range tcs {
part, err := NewQueryPart("field", []string{"value"}) part, err := NewQueryPart(tc.mode, tc.params)
So(err, ShouldBeNil) if err != nil {
t.Errorf("Expected NewQueryPart to not return an error. error: %v", err)
}
res := part.Render(query, queryContext, "value") res := part.Render(query, queryContext, tc.input)
So(res, ShouldEqual, `"value"`) if res != tc.expected {
}) t.Errorf("expected %v to render into %s", tc, tc.expected)
}
Convey("render nested part", func() { }
part, err := NewQueryPart("derivative", []string{"10s"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "mean(value)")
So(res, ShouldEqual, "derivative(mean(value), 10s)")
})
Convey("render bottom", func() {
part, err := NewQueryPart("bottom", []string{"3"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "value")
So(res, ShouldEqual, "bottom(value, 3)")
})
Convey("render time with $interval", func() {
part, err := NewQueryPart("time", []string{"$interval"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "")
So(res, ShouldEqual, "time($interval)")
})
Convey("render time with auto", func() {
part, err := NewQueryPart("time", []string{"auto"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "")
So(res, ShouldEqual, "time($__interval)")
})
Convey("render spread", func() {
part, err := NewQueryPart("spread", []string{})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "value")
So(res, ShouldEqual, `spread(value)`)
})
Convey("render suffix", func() {
part, err := NewQueryPart("math", []string{"/ 100"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "mean(value)")
So(res, ShouldEqual, "mean(value) / 100")
})
Convey("render alias", func() {
part, err := NewQueryPart("alias", []string{"test"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "mean(value)")
So(res, ShouldEqual, `mean(value) AS "test"`)
})
Convey("render count distinct", func() {
part, err := NewQueryPart("count", []string{})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "distinct(value)")
So(res, ShouldEqual, `count(distinct(value))`)
})
})
} }
+1 -1
View File
@@ -199,7 +199,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
body.mousemove(userActivityDetected); body.mousemove(userActivityDetected);
body.keydown(userActivityDetected); body.keydown(userActivityDetected);
// set useCapture = true to catch event here // set useCapture = true to catch event here
document.addEventListener('wheel', userActivityDetected, true); document.addEventListener('wheel', userActivityDetected, { capture: true, passive: true });
// treat tab change as activity // treat tab change as activity
document.addEventListener('visibilitychange', userActivityDetected); document.addEventListener('visibilitychange', userActivityDetected);
@@ -13,6 +13,10 @@
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
Folder Folder
</a> </a>
<a class="btn btn-success" href="{{ctrl.importDashboardUrl()}}" ng-if="ctrl.hasEditPermissionInFolders || ctrl.canSave">
<i class="fa fa-plus"></i>
Import
</a>
</div> </div>
<div class="page-action-bar page-action-bar--narrow" ng-show="ctrl.hasFilters"> <div class="page-action-bar page-action-bar--narrow" ng-show="ctrl.hasFilters">
@@ -294,6 +294,16 @@ export class ManageDashboardsCtrl {
return url; return url;
} }
importDashboardUrl() {
let url = 'dashboard/import';
if (this.folderId) {
url += `?folderId=${this.folderId}`;
}
return url;
}
} }
export function manageDashboardsDirective() { export function manageDashboardsDirective() {
@@ -52,11 +52,11 @@
<a href="dashboards/folder/new" class="search-filter-box-link" ng-if="ctrl.isEditor"> <a href="dashboards/folder/new" class="search-filter-box-link" ng-if="ctrl.isEditor">
<i class="gicon gicon-folder-new"></i> New folder <i class="gicon gicon-folder-new"></i> New folder
</a> </a>
<a href="dashboard/import" class="search-filter-box-link" ng-if="ctrl.isEditor"> <a href="dashboard/import" class="search-filter-box-link" ng-if="ctrl.isEditor || ctrl.hasEditPermissionInFolders">
<i class="gicon gicon-dashboard-import"></i> Import dashboard <i class="gicon gicon-dashboard-import"></i> Import dashboard
</a> </a>
<a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search"> <a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
<img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find dashboards on Grafana.com <img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find dashboards on Grafana.com
</a> </a>
</div> </div>
</div> </div>
+1
View File
@@ -16,6 +16,7 @@ class Settings {
defaultDatasource: string; defaultDatasource: string;
alertingEnabled: boolean; alertingEnabled: boolean;
authProxyEnabled: boolean; authProxyEnabled: boolean;
exploreEnabled: boolean;
ldapEnabled: boolean; ldapEnabled: boolean;
oauth: any; oauth: any;
disableUserSignUp: boolean; disableUserSignUp: boolean;
+2 -1
View File
@@ -1,6 +1,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'lodash'; import _ from 'lodash';
import config from 'app/core/config';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { encodePathComponent } from 'app/core/utils/location_util'; import { encodePathComponent } from 'app/core/utils/location_util';
@@ -178,7 +179,7 @@ export class KeybindingSrv {
}); });
// jump to explore if permissions allow // jump to explore if permissions allow
if (this.contextSrv.isEditor) { if (this.contextSrv.isEditor && config.exploreEnabled) {
this.bind('x', async () => { this.bind('x', async () => {
if (dashboard.meta.focusPanelId) { if (dashboard.meta.focusPanelId) {
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
+4
View File
@@ -44,4 +44,8 @@ export default class TableModel {
this.columnMap[col.text] = col; this.columnMap[col.text] = col;
} }
} }
addRow(row) {
this.rows.push(row);
}
} }
+10
View File
@@ -499,6 +499,7 @@ kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1); kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1);
kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1); kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1);
kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1); kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1);
kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2');
kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
@@ -528,6 +529,7 @@ kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar');
kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1);
kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa');
kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa');
kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg');
kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']);
@@ -579,6 +581,9 @@ kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm');
kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h');
kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('L');
kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('L', -1);
// Angle // Angle
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
@@ -1014,6 +1019,7 @@ kbn.getUnitFormats = function() {
{ text: 'Watt (W)', value: 'watt' }, { text: 'Watt (W)', value: 'watt' },
{ text: 'Kilowatt (kW)', value: 'kwatt' }, { text: 'Kilowatt (kW)', value: 'kwatt' },
{ text: 'Milliwatt (mW)', value: 'mwatt' }, { text: 'Milliwatt (mW)', value: 'mwatt' },
{ text: 'Watt per square metre (W/m2)', value: 'Wm2' },
{ text: 'Volt-ampere (VA)', value: 'voltamp' }, { text: 'Volt-ampere (VA)', value: 'voltamp' },
{ text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
{ text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, { text: 'Volt-ampere reactive (var)', value: 'voltampreact' },
@@ -1049,6 +1055,7 @@ kbn.getUnitFormats = function() {
{ text: 'Bars', value: 'pressurebar' }, { text: 'Bars', value: 'pressurebar' },
{ text: 'Kilobars', value: 'pressurekbar' }, { text: 'Kilobars', value: 'pressurekbar' },
{ text: 'Hectopascals', value: 'pressurehpa' }, { text: 'Hectopascals', value: 'pressurehpa' },
{ text: 'Kilopascals', value: 'pressurekpa' },
{ text: 'Inches of mercury', value: 'pressurehg' }, { text: 'Inches of mercury', value: 'pressurehg' },
{ text: 'PSI', value: 'pressurepsi' }, { text: 'PSI', value: 'pressurepsi' },
], ],
@@ -1069,6 +1076,9 @@ kbn.getUnitFormats = function() {
{ text: 'Cubic meters/sec (cms)', value: 'flowcms' }, { text: 'Cubic meters/sec (cms)', value: 'flowcms' },
{ text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' },
{ text: 'Cubic feet/min (cfm)', value: 'flowcfm' }, { text: 'Cubic feet/min (cfm)', value: 'flowcfm' },
{ text: 'Litre/hour', value: 'litreh' },
{ text: 'Litre/min (l/min)', value: 'flowlpm' },
{ text: 'milliLitre/min (mL/min)', value: 'flowmlpm' },
], ],
}, },
{ {
@@ -18,9 +18,9 @@ describe('ThresholdMapper', () => {
}; };
var updated = ThresholdMapper.alertToGraphThresholds(panel); var updated = ThresholdMapper.alertToGraphThresholds(panel);
expect(updated).to.be(true); expect(updated).toBe(true);
expect(panel.thresholds[0].op).to.be('gt'); expect(panel.thresholds[0].op).toBe('gt');
expect(panel.thresholds[0].value).to.be(100); expect(panel.thresholds[0].value).toBe(100);
}); });
}); });
@@ -39,12 +39,12 @@ describe('ThresholdMapper', () => {
}; };
var updated = ThresholdMapper.alertToGraphThresholds(panel); var updated = ThresholdMapper.alertToGraphThresholds(panel);
expect(updated).to.be(true); expect(updated).toBe(true);
expect(panel.thresholds[0].op).to.be('lt'); expect(panel.thresholds[0].op).toBe('lt');
expect(panel.thresholds[0].value).to.be(100); expect(panel.thresholds[0].value).toBe(100);
expect(panel.thresholds[1].op).to.be('gt'); expect(panel.thresholds[1].op).toBe('gt');
expect(panel.thresholds[1].value).to.be(200); expect(panel.thresholds[1].value).toBe(200);
}); });
}); });
@@ -63,12 +63,12 @@ describe('ThresholdMapper', () => {
}; };
var updated = ThresholdMapper.alertToGraphThresholds(panel); var updated = ThresholdMapper.alertToGraphThresholds(panel);
expect(updated).to.be(true); expect(updated).toBe(true);
expect(panel.thresholds[0].op).to.be('gt'); expect(panel.thresholds[0].op).toBe('gt');
expect(panel.thresholds[0].value).to.be(100); expect(panel.thresholds[0].value).toBe(100);
expect(panel.thresholds[1].op).to.be('lt'); expect(panel.thresholds[1].op).toBe('lt');
expect(panel.thresholds[1].value).to.be(200); expect(panel.thresholds[1].value).toBe(200);
}); });
}); });
}); });
@@ -21,6 +21,9 @@ export class DashboardImportCtrl {
uidValidationError: any; uidValidationError: any;
autoGenerateUid: boolean; autoGenerateUid: boolean;
autoGenerateUidValue: string; autoGenerateUidValue: string;
folderId: number;
initialFolderTitle: string;
isValidFolderSelection: boolean;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) { constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
@@ -31,6 +34,8 @@ export class DashboardImportCtrl {
this.uidExists = false; this.uidExists = false;
this.autoGenerateUid = true; this.autoGenerateUid = true;
this.autoGenerateUidValue = 'auto-generated'; this.autoGenerateUidValue = 'auto-generated';
this.folderId = $routeParams.folderId ? Number($routeParams.folderId) || 0 : null;
this.initialFolderTitle = 'Select a folder';
// check gnetId in url // check gnetId in url
if ($routeParams.gnetId) { if ($routeParams.gnetId) {
@@ -102,8 +107,9 @@ export class DashboardImportCtrl {
this.nameExists = false; this.nameExists = false;
this.validationSrv this.validationSrv
.validateNewDashboardName(0, this.dash.title) .validateNewDashboardName(this.folderId, this.dash.title)
.then(() => { .then(() => {
this.nameExists = false;
this.hasNameValidationError = false; this.hasNameValidationError = false;
}) })
.catch(err => { .catch(err => {
@@ -138,6 +144,23 @@ export class DashboardImportCtrl {
}); });
} }
onFolderChange(folder) {
this.folderId = folder.id;
this.titleChanged();
}
onEnterFolderCreation() {
this.inputsValid = false;
}
onExitFolderCreation() {
this.inputValueChanged();
}
isValid() {
return this.inputsValid && this.folderId !== null;
}
saveDashboard() { saveDashboard() {
var inputs = this.inputs.map(input => { var inputs = this.inputs.map(input => {
return { return {
@@ -153,6 +176,7 @@ export class DashboardImportCtrl {
dashboard: this.dash, dashboard: this.dash,
overwrite: true, overwrite: true,
inputs: inputs, inputs: inputs,
folderId: this.folderId,
}) })
.then(res => { .then(res => {
this.$location.url(res.importedUrl); this.$location.url(res.importedUrl);
@@ -154,6 +154,15 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
}); });
} }
filterKeyPress(evt) {
if (evt.key === 'Enter') {
let panel = _.head(this.state.panelPlugins);
if (panel) {
this.onAddPanel(panel);
}
}
}
filterPanels(panels, filter) { filterPanels(panels, filter) {
let regex = new RegExp(filter, 'i'); let regex = new RegExp(filter, 'i');
return panels.filter(panel => { return panels.filter(panel => {
@@ -229,10 +238,12 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
<label className="gf-form gf-form--grow gf-form--has-input-icon"> <label className="gf-form gf-form--grow gf-form--has-input-icon">
<input <input
type="text" type="text"
className="gf-form-input max-width-20" autoFocus
className="gf-form-input gf-form--grow"
placeholder="Panel Search Filter" placeholder="Panel Search Filter"
value={this.state.filter} value={this.state.filter}
onChange={this.filterChange.bind(this)} onChange={this.filterChange.bind(this)}
onKeyPress={this.filterKeyPress.bind(this)}
/> />
<i className="gf-form-input-icon fa fa-search" /> <i className="gf-form-input-icon fa fa-search" />
</label> </label>
@@ -84,15 +84,18 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
'fa-chevron-right': this.state.collapsed, 'fa-chevron-right': this.state.collapsed,
}); });
let title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars); const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
const hiddenPanels = this.props.panel.panels ? this.props.panel.panels.length : 0; const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
const panels = count === 1 ? 'panel' : 'panels';
return ( return (
<div className={classes}> <div className={classes}>
<a className="dashboard-row__title pointer" onClick={this.toggle}> <a className="dashboard-row__title pointer" onClick={this.toggle}>
<i className={chevronClass} /> <i className={chevronClass} />
{title} {title}
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span> <span className="dashboard-row__panel_count">
({count} {panels})
</span>
</a> </a>
{this.dashboard.meta.canEdit === true && ( {this.dashboard.meta.canEdit === true && (
<div className="dashboard-row__actions"> <div className="dashboard-row__actions">
@@ -104,6 +107,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
</a> </a>
</div> </div>
)} )}
{this.state.collapsed === true && (
<div className="dashboard-row__toggle-target" onClick={this.toggle}>
&nbsp;
</div>
)}
<div className="dashboard-row__drag grid-drag-handle" /> <div className="dashboard-row__drag grid-drag-handle" />
</div> </div>
); );
@@ -132,23 +132,26 @@ export class FolderPickerCtrl {
} }
private loadInitialValue() { private loadInitialValue() {
if (this.initialFolderId && this.initialFolderId > 0) { const resetFolder = { text: this.initialTitle, value: null };
this.getOptions('').then(result => { const rootFolder = { text: this.rootName, value: 0 };
this.folder = _.find(result, { value: this.initialFolderId }); this.getOptions('').then(result => {
if (!this.folder) { let folder;
this.folder = { text: this.initialTitle, value: this.initialFolderId }; if (this.initialFolderId) {
} folder = _.find(result, { value: this.initialFolderId });
this.onFolderLoad(); } else if (this.enableReset && this.initialTitle && this.initialFolderId === null) {
}); folder = resetFolder;
} else {
if (this.initialTitle && this.initialFolderId === null) {
this.folder = { text: this.initialTitle, value: null };
} else {
this.folder = { text: this.rootName, value: 0 };
} }
if (!folder) {
if (this.isEditor) {
folder = rootFolder;
} else {
folder = result.length > 0 ? result[0] : resetFolder;
}
}
this.folder = folder;
this.onFolderLoad(); this.onFolderLoad();
} });
} }
private onFolderLoad() { private onFolderLoad() {
@@ -80,6 +80,20 @@
</div> </div>
</div> </div>
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<folder-picker label-class="width-15"
initial-folder-id="ctrl.folderId"
initial-title="ctrl.initialFolderTitle"
on-change="ctrl.onFolderChange($folder)"
on-load="ctrl.onFolderChange($folder)"
enter-folder-creation="ctrl.onEnterFolderCreation()"
exit-folder-creation="ctrl.onExitFolderCreation()"
enable-create-new="true">
</folder-picker>
</div>
</div>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<span class="gf-form-label width-15"> <span class="gf-form-label width-15">
@@ -132,10 +146,10 @@
</div> </div>
<div class="gf-form-button-row"> <div class="gf-form-button-row">
<button type="button" class="btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.inputsValid"> <button type="button" class="btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.isValid()">
<i class="fa fa-save"></i> Import <i class="fa fa-save"></i> Import
</button> </button>
<button type="button" class="btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.inputsValid"> <button type="button" class="btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.isValid()">
<i class="fa fa-save"></i> Import (Overwrite) <i class="fa fa-save"></i> Import (Overwrite)
</button> </button>
<a class="btn btn-link" ng-click="ctrl.back()">Cancel</a> <a class="btn btn-link" ng-click="ctrl.back()">Cancel</a>
+15 -2
View File
@@ -50,8 +50,17 @@ const template = `
</div> </div>
<div class="gf-form-button-row text-center"> <div class="gf-form-button-row text-center">
<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Save</button> <button
<a class="btn btn-link" ng-click="ctrl.dismiss();">Cancel</a> id="saveBtn"
type="submit"
class="btn btn-success"
ng-class="{'btn-success--processing': ctrl.isSaving}"
ng-disabled="ctrl.saveForm.$invalid || ctrl.isSaving"
>
<span ng-if="!ctrl.isSaving">Save</span>
<span ng-if="ctrl.isSaving === true">Saving...</span>
</button>
<button class="btn btn-inverse" ng-click="ctrl.dismiss();">Cancel</button>
</div> </div>
</form> </form>
</div> </div>
@@ -68,6 +77,7 @@ export class SaveDashboardModalCtrl {
originalCurrent = []; originalCurrent = [];
max: number; max: number;
saveForm: any; saveForm: any;
isSaving: boolean;
dismiss: () => void; dismiss: () => void;
timeChange = false; timeChange = false;
variableValueChange = false; variableValueChange = false;
@@ -76,6 +86,7 @@ export class SaveDashboardModalCtrl {
constructor(private dashboardSrv) { constructor(private dashboardSrv) {
this.message = ''; this.message = '';
this.max = 64; this.max = 64;
this.isSaving = false;
this.templating = dashboardSrv.dash.templating.list; this.templating = dashboardSrv.dash.templating.list;
this.compareTemplating(); this.compareTemplating();
@@ -126,6 +137,8 @@ export class SaveDashboardModalCtrl {
var dashboard = this.dashboardSrv.getCurrent(); var dashboard = this.dashboardSrv.getCurrent();
var saveModel = dashboard.getSaveModelClone(options); var saveModel = dashboard.getSaveModelClone(options);
this.isSaving = true;
return this.dashboardSrv.save(saveModel, options).then(this.dismiss); return this.dashboardSrv.save(saveModel, options).then(this.dismiss);
} }
} }
@@ -0,0 +1,194 @@
jest.mock('app/core/store', () => {
return {
getBool: jest.fn(),
};
});
import _ from 'lodash';
import config from 'app/core/config';
import { DashboardExporter } from '../export/exporter';
import { DashboardModel } from '../dashboard_model';
describe('given dashboard with repeated panels', () => {
var dash, exported;
beforeEach(done => {
dash = {
templating: {
list: [
{
name: 'apps',
type: 'query',
datasource: 'gfdb',
current: { value: 'Asd', text: 'Asd' },
options: [{ value: 'Asd', text: 'Asd' }],
},
{
name: 'prefix',
type: 'constant',
current: { value: 'collectd', text: 'collectd' },
options: [],
},
{
name: 'ds',
type: 'datasource',
query: 'testdb',
current: { value: 'prod', text: 'prod' },
options: [],
},
],
},
annotations: {
list: [
{
name: 'logs',
datasource: 'gfdb',
},
],
},
panels: [
{ id: 6, datasource: 'gfdb', type: 'graph' },
{ id: 7 },
{
id: 8,
datasource: '-- Mixed --',
targets: [{ datasource: 'other' }],
},
{ id: 9, datasource: '$ds' },
{
id: 2,
repeat: 'apps',
datasource: 'gfdb',
type: 'graph',
},
{ id: 3, repeat: null, repeatPanelId: 2 },
],
};
config.buildInfo = {
version: '3.0.2',
};
//Stubs test function calls
var datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };
config.panels['graph'] = {
id: 'graph',
name: 'Graph',
info: { version: '1.1.0' },
};
dash = new DashboardModel(dash, {});
var exporter = new DashboardExporter(datasourceSrvStub);
exporter.makeExportable(dash).then(clean => {
exported = clean;
done();
});
});
it('should replace datasource refs', () => {
var panel = exported.panels[0];
expect(panel.datasource).toBe('${DS_GFDB}');
});
it('should replace datasource in variable query', () => {
expect(exported.templating.list[0].datasource).toBe('${DS_GFDB}');
expect(exported.templating.list[0].options.length).toBe(0);
expect(exported.templating.list[0].current.value).toBe(undefined);
expect(exported.templating.list[0].current.text).toBe(undefined);
});
it('should replace datasource in annotation query', () => {
expect(exported.annotations.list[1].datasource).toBe('${DS_GFDB}');
});
it('should add datasource as input', () => {
expect(exported.__inputs[0].name).toBe('DS_GFDB');
expect(exported.__inputs[0].pluginId).toBe('testdb');
expect(exported.__inputs[0].type).toBe('datasource');
});
it('should add datasource to required', () => {
var require = _.find(exported.__requires, { name: 'TestDB' });
expect(require.name).toBe('TestDB');
expect(require.id).toBe('testdb');
expect(require.type).toBe('datasource');
expect(require.version).toBe('1.2.1');
});
it('should not add built in datasources to required', () => {
var require = _.find(exported.__requires, { name: 'Mixed' });
expect(require).toBe(undefined);
});
it('should add datasources used in mixed mode', () => {
var require = _.find(exported.__requires, { name: 'OtherDB' });
expect(require).not.toBe(undefined);
});
it('should add panel to required', () => {
var require = _.find(exported.__requires, { name: 'Graph' });
expect(require.name).toBe('Graph');
expect(require.id).toBe('graph');
expect(require.version).toBe('1.1.0');
});
it('should add grafana version', () => {
var require = _.find(exported.__requires, { name: 'Grafana' });
expect(require.type).toBe('grafana');
expect(require.id).toBe('grafana');
expect(require.version).toBe('3.0.2');
});
it('should add constant template variables as inputs', () => {
var input = _.find(exported.__inputs, { name: 'VAR_PREFIX' });
expect(input.type).toBe('constant');
expect(input.label).toBe('prefix');
expect(input.value).toBe('collectd');
});
it('should templatize constant variables', () => {
var variable = _.find(exported.templating.list, { name: 'prefix' });
expect(variable.query).toBe('${VAR_PREFIX}');
expect(variable.current.text).toBe('${VAR_PREFIX}');
expect(variable.current.value).toBe('${VAR_PREFIX}');
expect(variable.options[0].text).toBe('${VAR_PREFIX}');
expect(variable.options[0].value).toBe('${VAR_PREFIX}');
});
});
// Stub responses
var stubs = [];
stubs['gfdb'] = {
name: 'gfdb',
meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
};
stubs['other'] = {
name: 'other',
meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' },
};
stubs['-- Mixed --'] = {
name: 'mixed',
meta: {
id: 'mixed',
info: { version: '1.2.1' },
name: 'Mixed',
builtIn: true,
},
};
stubs['-- Grafana --'] = {
name: '-- Grafana --',
meta: {
id: 'grafana',
info: { version: '1.2.1' },
name: 'grafana',
builtIn: true,
},
};
function getStub(arg) {
return Promise.resolve(stubs[arg]);
}
@@ -1,187 +0,0 @@
import { describe, beforeEach, it, sinon, expect } from 'test/lib/common';
import _ from 'lodash';
import config from 'app/core/config';
import { DashboardExporter } from '../export/exporter';
import { DashboardModel } from '../dashboard_model';
describe('given dashboard with repeated panels', function() {
var dash, exported;
beforeEach(done => {
dash = {
templating: { list: [] },
annotations: { list: [] },
};
config.buildInfo = {
version: '3.0.2',
};
dash.templating.list.push({
name: 'apps',
type: 'query',
datasource: 'gfdb',
current: { value: 'Asd', text: 'Asd' },
options: [{ value: 'Asd', text: 'Asd' }],
});
dash.templating.list.push({
name: 'prefix',
type: 'constant',
current: { value: 'collectd', text: 'collectd' },
options: [],
});
dash.templating.list.push({
name: 'ds',
type: 'datasource',
query: 'testdb',
current: { value: 'prod', text: 'prod' },
options: [],
});
dash.annotations.list.push({
name: 'logs',
datasource: 'gfdb',
});
dash.panels = [
{ id: 6, datasource: 'gfdb', type: 'graph' },
{ id: 7 },
{
id: 8,
datasource: '-- Mixed --',
targets: [{ datasource: 'other' }],
},
{ id: 9, datasource: '$ds' },
];
dash.panels.push({
id: 2,
repeat: 'apps',
datasource: 'gfdb',
type: 'graph',
});
dash.panels.push({ id: 3, repeat: null, repeatPanelId: 2 });
var datasourceSrvStub = { get: sinon.stub() };
datasourceSrvStub.get.withArgs('gfdb').returns(
Promise.resolve({
name: 'gfdb',
meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
})
);
datasourceSrvStub.get.withArgs('other').returns(
Promise.resolve({
name: 'other',
meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' },
})
);
datasourceSrvStub.get.withArgs('-- Mixed --').returns(
Promise.resolve({
name: 'mixed',
meta: {
id: 'mixed',
info: { version: '1.2.1' },
name: 'Mixed',
builtIn: true,
},
})
);
datasourceSrvStub.get.withArgs('-- Grafana --').returns(
Promise.resolve({
name: '-- Grafana --',
meta: {
id: 'grafana',
info: { version: '1.2.1' },
name: 'grafana',
builtIn: true,
},
})
);
config.panels['graph'] = {
id: 'graph',
name: 'Graph',
info: { version: '1.1.0' },
};
dash = new DashboardModel(dash, {});
var exporter = new DashboardExporter(datasourceSrvStub);
exporter.makeExportable(dash).then(clean => {
exported = clean;
done();
});
});
it('should replace datasource refs', function() {
var panel = exported.panels[0];
expect(panel.datasource).to.be('${DS_GFDB}');
});
it('should replace datasource in variable query', function() {
expect(exported.templating.list[0].datasource).to.be('${DS_GFDB}');
expect(exported.templating.list[0].options.length).to.be(0);
expect(exported.templating.list[0].current.value).to.be(undefined);
expect(exported.templating.list[0].current.text).to.be(undefined);
});
it('should replace datasource in annotation query', function() {
expect(exported.annotations.list[1].datasource).to.be('${DS_GFDB}');
});
it('should add datasource as input', function() {
expect(exported.__inputs[0].name).to.be('DS_GFDB');
expect(exported.__inputs[0].pluginId).to.be('testdb');
expect(exported.__inputs[0].type).to.be('datasource');
});
it('should add datasource to required', function() {
var require = _.find(exported.__requires, { name: 'TestDB' });
expect(require.name).to.be('TestDB');
expect(require.id).to.be('testdb');
expect(require.type).to.be('datasource');
expect(require.version).to.be('1.2.1');
});
it('should not add built in datasources to required', function() {
var require = _.find(exported.__requires, { name: 'Mixed' });
expect(require).to.be(undefined);
});
it('should add datasources used in mixed mode', function() {
var require = _.find(exported.__requires, { name: 'OtherDB' });
expect(require).to.not.be(undefined);
});
it('should add panel to required', function() {
var require = _.find(exported.__requires, { name: 'Graph' });
expect(require.name).to.be('Graph');
expect(require.id).to.be('graph');
expect(require.version).to.be('1.1.0');
});
it('should add grafana version', function() {
var require = _.find(exported.__requires, { name: 'Grafana' });
expect(require.type).to.be('grafana');
expect(require.id).to.be('grafana');
expect(require.version).to.be('3.0.2');
});
it('should add constant template variables as inputs', function() {
var input = _.find(exported.__inputs, { name: 'VAR_PREFIX' });
expect(input.type).to.be('constant');
expect(input.label).to.be('prefix');
expect(input.value).to.be('collectd');
});
it('should templatize constant variables', function() {
var variable = _.find(exported.templating.list, { name: 'prefix' });
expect(variable.query).to.be('${VAR_PREFIX}');
expect(variable.current.text).to.be('${VAR_PREFIX}');
expect(variable.current.value).to.be('${VAR_PREFIX}');
expect(variable.options[0].text).to.be('${VAR_PREFIX}');
expect(variable.options[0].value).to.be('${VAR_PREFIX}');
});
});
@@ -314,7 +314,7 @@ class MetricsPanelCtrl extends PanelCtrl {
getAdditionalMenuItems() { getAdditionalMenuItems() {
const items = []; const items = [];
if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) { if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
items.push({ items.push({
text: 'Explore', text: 'Explore',
click: 'ctrl.explore();', click: 'ctrl.explore();',
+1 -1
View File
@@ -25,7 +25,7 @@ var template = `
<li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li> <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li>
</ul> </ul>
</span> </span>
<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span> <span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
</span>`; </span>`;
function renderMenuItem(item, ctrl) { function renderMenuItem(item, ctrl) {
@@ -1,8 +1,19 @@
jest.mock('app/core/core', () => ({})); jest.mock('app/core/core', () => ({}));
jest.mock('app/core/config', () => {
return {
exploreEnabled: true,
panels: {
test: {
id: 'test',
name: 'test',
},
},
};
});
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
import q from 'q'; import q from 'q';
import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelModel } from 'app/features/dashboard/panel_model';
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
describe('MetricsPanelCtrl', () => { describe('MetricsPanelCtrl', () => {
let ctrl; let ctrl;
@@ -1,5 +1,4 @@
import '../playlist_edit_ctrl'; import '../playlist_edit_ctrl';
import { describe, beforeEach, it, expect } from 'test/lib/common';
import { PlaylistEditCtrl } from '../playlist_edit_ctrl'; import { PlaylistEditCtrl } from '../playlist_edit_ctrl';
describe('PlaylistEditCtrl', () => { describe('PlaylistEditCtrl', () => {
@@ -20,13 +19,13 @@ describe('PlaylistEditCtrl', () => {
describe('searchresult returns 2 dashboards, ', () => { describe('searchresult returns 2 dashboards, ', () => {
it('found dashboard should be 2', () => { it('found dashboard should be 2', () => {
expect(ctx.dashboardresult.length).to.be(2); expect(ctx.dashboardresult.length).toBe(2);
}); });
it('filtred result should be 2', () => { it('filtred result should be 2', () => {
ctx.filterFoundPlaylistItems(); ctx.filterFoundPlaylistItems();
expect(ctx.filteredDashboards.length).to.be(2); expect(ctx.filteredDashboards.length).toBe(2);
expect(ctx.filteredTags.length).to.be(2); expect(ctx.filteredTags.length).toBe(2);
}); });
describe('adds one dashboard to playlist, ', () => { describe('adds one dashboard to playlist, ', () => {
@@ -37,16 +36,16 @@ describe('PlaylistEditCtrl', () => {
}); });
it('playlistitems should be increased by one', () => { it('playlistitems should be increased by one', () => {
expect(ctx.playlistItems.length).to.be(2); expect(ctx.playlistItems.length).toBe(2);
}); });
it('filtred playlistitems should be reduced by one', () => { it('filtred playlistitems should be reduced by one', () => {
expect(ctx.filteredDashboards.length).to.be(1); expect(ctx.filteredDashboards.length).toBe(1);
expect(ctx.filteredTags.length).to.be(1); expect(ctx.filteredTags.length).toBe(1);
}); });
it('found dashboard should be 2', () => { it('found dashboard should be 2', () => {
expect(ctx.dashboardresult.length).to.be(2); expect(ctx.dashboardresult.length).toBe(2);
}); });
describe('removes one dashboard from playlist, ', () => { describe('removes one dashboard from playlist, ', () => {
@@ -57,14 +56,14 @@ describe('PlaylistEditCtrl', () => {
}); });
it('playlistitems should be increased by one', () => { it('playlistitems should be increased by one', () => {
expect(ctx.playlistItems.length).to.be(0); expect(ctx.playlistItems.length).toBe(0);
}); });
it('found dashboard should be 2', () => { it('found dashboard should be 2', () => {
expect(ctx.dashboardresult.length).to.be(2); expect(ctx.dashboardresult.length).toBe(2);
expect(ctx.filteredDashboards.length).to.be(2); expect(ctx.filteredDashboards.length).toBe(2);
expect(ctx.filteredTags.length).to.be(2); expect(ctx.filteredTags.length).toBe(2);
expect(ctx.tagresult.length).to.be(2); expect(ctx.tagresult.length).toBe(2);
}); });
}); });
}); });
@@ -6,7 +6,6 @@ import coreModule from 'app/core/core_module';
import { importPluginModule } from './plugin_loader'; import { importPluginModule } from './plugin_loader';
import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module'; import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
import { DashboardRowCtrl } from './row_ctrl';
/** @ngInject **/ /** @ngInject **/
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) { function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
@@ -59,15 +58,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
} }
function loadPanelComponentInfo(scope, attrs) { function loadPanelComponentInfo(scope, attrs) {
if (scope.panel.type === 'row') {
return $q.when({
name: 'dashboard-row',
bindings: { dashboard: '=', panel: '=' },
attrs: { dashboard: 'ctrl.dashboard', panel: 'panel' },
Component: DashboardRowCtrl,
});
}
var componentInfo: any = { var componentInfo: any = {
name: 'panel-plugin-' + scope.panel.type, name: 'panel-plugin-' + scope.panel.type,
bindings: { dashboard: '=', panel: '=', row: '=' }, bindings: { dashboard: '=', panel: '=', row: '=' },
@@ -136,24 +126,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
}); });
}); });
} }
// QueryOptionsCtrl
case 'query-options-ctrl': {
return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
return importPluginModule(ds.meta.module).then((dsModule): any => {
if (!dsModule.QueryOptionsCtrl) {
return { notFound: true };
}
return {
baseUrl: ds.meta.baseUrl,
name: 'query-options-ctrl-' + ds.meta.id,
bindings: { panelCtrl: '=' },
attrs: { 'panel-ctrl': 'ctrl.panelCtrl' },
Component: dsModule.QueryOptionsCtrl,
};
});
});
}
// Annotations // Annotations
case 'annotations-query-ctrl': { case 'annotations-query-ctrl': {
return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) { return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
@@ -5,6 +5,15 @@ import kbn from 'app/core/utils/kbn';
import moment from 'moment'; import moment from 'moment';
import angular from 'angular'; import angular from 'angular';
import jquery from 'jquery'; import jquery from 'jquery';
// Experimental module exports
import prismjs from 'prismjs';
import slate from 'slate';
import slateReact from 'slate-react';
import slatePlain from 'slate-plain-serializer';
import react from 'react';
import reactDom from 'react-dom';
import config from 'app/core/config'; import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
@@ -69,6 +78,14 @@ exposeToPlugin('d3', d3);
exposeToPlugin('rxjs/Subject', Subject); exposeToPlugin('rxjs/Subject', Subject);
exposeToPlugin('rxjs/Observable', Observable); exposeToPlugin('rxjs/Observable', Observable);
// Experimental modules
exposeToPlugin('prismjs', prismjs);
exposeToPlugin('slate', slate);
exposeToPlugin('slate-react', slateReact);
exposeToPlugin('slate-plain-serializer', slatePlain);
exposeToPlugin('react', react);
exposeToPlugin('react-dom', reactDom);
// backward compatible path // backward compatible path
exposeToPlugin('vendor/npm/rxjs/Rx', { exposeToPlugin('vendor/npm/rxjs/Rx', {
Subject: Subject, Subject: Subject,
-100
View File
@@ -1,100 +0,0 @@
import _ from 'lodash';
export class DashboardRowCtrl {
static template = `
<div class="dashboard-row__center">
<div class="dashboard-row__actions-left">
<i class="fa fa-chevron-down" ng-hide="ctrl.panel.collapse"></i>
<i class="fa fa-chevron-right" ng-show="ctrl.panel.collapse"></i>
</div>
<a class="dashboard-row__title pointer" ng-click="ctrl.toggle()">
<span class="dashboard-row__title-text">
{{ctrl.panel.title | interpolateTemplateVars:this}}
</span>
</a>
<div class="dashboard-row__actions-right">
<a class="pointer" ng-click="ctrl.openSettings()"><span class="fa fa-cog"></i></a>
</div>
</div>
<div class="dashboard-row__panel_count">
({{ctrl.panel.hiddenPanels.length}} hidden panels)
</div>
<div class="dashboard-row__drag grid-drag-handle">
</div>
`;
dashboard: any;
panel: any;
constructor() {
this.panel.hiddenPanels = this.panel.hiddenPanels || [];
}
toggle() {
if (this.panel.collapse) {
let panelIndex = _.indexOf(this.dashboard.panels, this.panel);
for (let child of this.panel.hiddenPanels) {
this.dashboard.panels.splice(panelIndex + 1, 0, child);
child.y = this.panel.y + 1;
console.log('restoring child', child);
}
this.panel.hiddenPanels = [];
this.panel.collapse = false;
return;
}
this.panel.collapse = true;
let foundRow = false;
for (let i = 0; i < this.dashboard.panels.length; i++) {
let panel = this.dashboard.panels[i];
if (panel === this.panel) {
console.log('found row');
foundRow = true;
continue;
}
if (!foundRow) {
continue;
}
if (panel.type === 'row') {
break;
}
this.panel.hiddenPanels.push(panel);
console.log('hiding child', panel.id);
}
for (let hiddenPanel of this.panel.hiddenPanels) {
this.dashboard.removePanel(hiddenPanel, false);
}
}
moveUp() {
// let panelIndex = _.indexOf(this.dashboard.panels, this.panel);
// let rowAbove = null;
// for (let index = panelIndex-1; index > 0; index--) {
// panel = this.dashboard.panels[index];
// if (panel.type === 'row') {
// rowAbove = panel;
// }
// }
//
// if (rowAbove) {
// this.panel.y = rowAbove.y;
// }
}
link(scope, elem) {
elem.addClass('dashboard-row');
scope.$watch('ctrl.panel.collapse', () => {
elem.toggleClass('dashboard-row--collapse', this.panel.collapse === true);
});
}
}
@@ -2,10 +2,6 @@ import { ElasticDatasource } from './datasource';
import { ElasticQueryCtrl } from './query_ctrl'; import { ElasticQueryCtrl } from './query_ctrl';
import { ElasticConfigCtrl } from './config_ctrl'; import { ElasticConfigCtrl } from './config_ctrl';
class ElasticQueryOptionsCtrl {
static templateUrl = 'partials/query.options.html';
}
class ElasticAnnotationsQueryCtrl { class ElasticAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html'; static templateUrl = 'partials/annotations.editor.html';
} }
@@ -14,6 +10,5 @@ export {
ElasticDatasource as Datasource, ElasticDatasource as Datasource,
ElasticQueryCtrl as QueryCtrl, ElasticQueryCtrl as QueryCtrl,
ElasticConfigCtrl as ConfigCtrl, ElasticConfigCtrl as ConfigCtrl,
ElasticQueryOptionsCtrl as QueryOptionsCtrl,
ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl, ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
}; };
@@ -1,13 +1,12 @@
import { describe, beforeEach, it, expect } from 'test/lib/common';
import { ElasticResponse } from '../elastic_response'; import { ElasticResponse } from '../elastic_response';
describe('ElasticResponse', function() { describe('ElasticResponse', () => {
var targets; var targets;
var response; var response;
var result; var result;
describe('simple query and count', function() { describe('simple query and count', () => {
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -39,19 +38,19 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 1 series', function() { it('should return 1 series', () => {
expect(result.data.length).to.be(1); expect(result.data.length).toBe(1);
expect(result.data[0].target).to.be('Count'); expect(result.data[0].target).toBe('Count');
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].datapoints[0][0]).to.be(10); expect(result.data[0].datapoints[0][0]).toBe(10);
expect(result.data[0].datapoints[0][1]).to.be(1000); expect(result.data[0].datapoints[0][1]).toBe(1000);
}); });
}); });
describe('simple query count & avg aggregation', function() { describe('simple query count & avg aggregation', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -85,22 +84,22 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 2 series', function() { it('should return 2 series', () => {
expect(result.data.length).to.be(2); expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].datapoints[0][0]).to.be(10); expect(result.data[0].datapoints[0][0]).toBe(10);
expect(result.data[0].datapoints[0][1]).to.be(1000); expect(result.data[0].datapoints[0][1]).toBe(1000);
expect(result.data[1].target).to.be('Average value'); expect(result.data[1].target).toBe('Average value');
expect(result.data[1].datapoints[0][0]).to.be(88); expect(result.data[1].datapoints[0][0]).toBe(88);
expect(result.data[1].datapoints[1][0]).to.be(99); expect(result.data[1].datapoints[1][0]).toBe(99);
}); });
}); });
describe('single group by query one metric', function() { describe('single group by query one metric', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -141,18 +140,18 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 2 series', function() { it('should return 2 series', () => {
expect(result.data.length).to.be(2); expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).to.be('server1'); expect(result.data[0].target).toBe('server1');
expect(result.data[1].target).to.be('server2'); expect(result.data[1].target).toBe('server2');
}); });
}); });
describe('single group by query two metrics', function() { describe('single group by query two metrics', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -199,20 +198,20 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 2 series', function() { it('should return 2 series', () => {
expect(result.data.length).to.be(4); expect(result.data.length).toBe(4);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).to.be('server1 Count'); expect(result.data[0].target).toBe('server1 Count');
expect(result.data[1].target).to.be('server1 Average @value'); expect(result.data[1].target).toBe('server1 Average @value');
expect(result.data[2].target).to.be('server2 Count'); expect(result.data[2].target).toBe('server2 Count');
expect(result.data[3].target).to.be('server2 Average @value'); expect(result.data[3].target).toBe('server2 Average @value');
}); });
}); });
describe('with percentiles ', function() { describe('with percentiles ', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -246,21 +245,21 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 2 series', function() { it('should return 2 series', () => {
expect(result.data.length).to.be(2); expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).to.be('p75'); expect(result.data[0].target).toBe('p75');
expect(result.data[1].target).to.be('p90'); expect(result.data[1].target).toBe('p90');
expect(result.data[0].datapoints[0][0]).to.be(3.3); expect(result.data[0].datapoints[0][0]).toBe(3.3);
expect(result.data[0].datapoints[0][1]).to.be(1000); expect(result.data[0].datapoints[0][1]).toBe(1000);
expect(result.data[1].datapoints[1][0]).to.be(4.5); expect(result.data[1].datapoints[1][0]).toBe(4.5);
}); });
}); });
describe('with extended_stats', function() { describe('with extended_stats', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -322,21 +321,21 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 4 series', function() { it('should return 4 series', () => {
expect(result.data.length).to.be(4); expect(result.data.length).toBe(4);
expect(result.data[0].datapoints.length).to.be(1); expect(result.data[0].datapoints.length).toBe(1);
expect(result.data[0].target).to.be('server1 Max'); expect(result.data[0].target).toBe('server1 Max');
expect(result.data[1].target).to.be('server1 Std Dev Upper'); expect(result.data[1].target).toBe('server1 Std Dev Upper');
expect(result.data[0].datapoints[0][0]).to.be(10.2); expect(result.data[0].datapoints[0][0]).toBe(10.2);
expect(result.data[1].datapoints[0][0]).to.be(3); expect(result.data[1].datapoints[0][0]).toBe(3);
}); });
}); });
describe('single group by with alias pattern', function() { describe('single group by with alias pattern', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -385,19 +384,19 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 2 series', function() { it('should return 2 series', () => {
expect(result.data.length).to.be(3); expect(result.data.length).toBe(3);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).to.be('server1 Count and {{not_exist}} server1'); expect(result.data[0].target).toBe('server1 Count and {{not_exist}} server1');
expect(result.data[1].target).to.be('server2 Count and {{not_exist}} server2'); expect(result.data[1].target).toBe('server2 Count and {{not_exist}} server2');
expect(result.data[2].target).to.be('0 Count and {{not_exist}} 0'); expect(result.data[2].target).toBe('0 Count and {{not_exist}} 0');
}); });
}); });
describe('histogram response', function() { describe('histogram response', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -420,16 +419,16 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return table with byte and count', function() { it('should return table with byte and count', () => {
expect(result.data[0].rows.length).to.be(3); expect(result.data[0].rows.length).toBe(3);
expect(result.data[0].columns).to.eql([{ text: 'bytes', filterable: true }, { text: 'Count' }]); expect(result.data[0].columns).toEqual([{ text: 'bytes', filterable: true }, { text: 'Count' }]);
}); });
}); });
describe('with two filters agg', function() { describe('with two filters agg', () => {
var result; var result;
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -472,16 +471,16 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return 2 series', function() { it('should return 2 series', () => {
expect(result.data.length).to.be(2); expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).to.be('@metric:cpu'); expect(result.data[0].target).toBe('@metric:cpu');
expect(result.data[1].target).to.be('@metric:logins.count'); expect(result.data[1].target).toBe('@metric:logins.count');
}); });
}); });
describe('with dropfirst and last aggregation', function() { describe('with dropfirst and last aggregation', () => {
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -528,14 +527,14 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should remove first and last value', function() { it('should remove first and last value', () => {
expect(result.data.length).to.be(2); expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).to.be(1); expect(result.data[0].datapoints.length).toBe(1);
}); });
}); });
describe('No group by time', function() { describe('No group by time', () => {
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -570,21 +569,21 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return table', function() { it('should return table', () => {
expect(result.data.length).to.be(1); expect(result.data.length).toBe(1);
expect(result.data[0].type).to.be('table'); expect(result.data[0].type).toBe('table');
expect(result.data[0].rows.length).to.be(2); expect(result.data[0].rows.length).toBe(2);
expect(result.data[0].rows[0][0]).to.be('server-1'); expect(result.data[0].rows[0][0]).toBe('server-1');
expect(result.data[0].rows[0][1]).to.be(1000); expect(result.data[0].rows[0][1]).toBe(1000);
expect(result.data[0].rows[0][2]).to.be(369); expect(result.data[0].rows[0][2]).toBe(369);
expect(result.data[0].rows[1][0]).to.be('server-2'); expect(result.data[0].rows[1][0]).toBe('server-2');
expect(result.data[0].rows[1][1]).to.be(2000); expect(result.data[0].rows[1][1]).toBe(2000);
}); });
}); });
describe('Multiple metrics of same type', function() { describe('Multiple metrics of same type', () => {
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -615,15 +614,15 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should include field in metric name', function() { it('should include field in metric name', () => {
expect(result.data[0].type).to.be('table'); expect(result.data[0].type).toBe('table');
expect(result.data[0].rows[0][1]).to.be(1000); expect(result.data[0].rows[0][1]).toBe(1000);
expect(result.data[0].rows[0][2]).to.be(3000); expect(result.data[0].rows[0][2]).toBe(3000);
}); });
}); });
describe('Raw documents query', function() { describe('Raw documents query', () => {
beforeEach(function() { beforeEach(() => {
targets = [ targets = [
{ {
refId: 'A', refId: 'A',
@@ -657,13 +656,13 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries(); result = new ElasticResponse(targets, response).getTimeSeries();
}); });
it('should return docs', function() { it('should return docs', () => {
expect(result.data.length).to.be(1); expect(result.data.length).toBe(1);
expect(result.data[0].type).to.be('docs'); expect(result.data[0].type).toBe('docs');
expect(result.data[0].total).to.be(100); expect(result.data[0].total).toBe(100);
expect(result.data[0].datapoints.length).to.be(2); expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].datapoints[0].sourceProp).to.be('asd'); expect(result.data[0].datapoints[0].sourceProp).toBe('asd');
expect(result.data[0].datapoints[0].fieldProp).to.be('field'); expect(result.data[0].datapoints[0].fieldProp).toBe('field');
}); });
}); });
}); });
@@ -1,38 +1,37 @@
///<amd-dependency path="test/specs/helpers" name="helpers" /> ///<amd-dependency path="test/specs/helpers" name="helpers" />
import { describe, it, expect } from 'test/lib/common';
import moment from 'moment'; import moment from 'moment';
import { IndexPattern } from '../index_pattern'; import { IndexPattern } from '../index_pattern';
describe('IndexPattern', function() { describe('IndexPattern', () => {
describe('when getting index for today', function() { describe('when getting index for today', () => {
it('should return correct index name', function() { test('should return correct index name', () => {
var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
var expected = 'asd-' + moment.utc().format('YYYY.MM.DD'); var expected = 'asd-' + moment.utc().format('YYYY.MM.DD');
expect(pattern.getIndexForToday()).to.be(expected); expect(pattern.getIndexForToday()).toBe(expected);
}); });
}); });
describe('when getting index list for time range', function() { describe('when getting index list for time range', () => {
describe('no interval', function() { describe('no interval', () => {
it('should return correct index', function() { test('should return correct index', () => {
var pattern = new IndexPattern('my-metrics', null); var pattern = new IndexPattern('my-metrics', null);
var from = new Date(2015, 4, 30, 1, 2, 3); var from = new Date(2015, 4, 30, 1, 2, 3);
var to = new Date(2015, 5, 1, 12, 5, 6); var to = new Date(2015, 5, 1, 12, 5, 6);
expect(pattern.getIndexList(from, to)).to.eql('my-metrics'); expect(pattern.getIndexList(from, to)).toEqual('my-metrics');
}); });
}); });
describe('daily', function() { describe('daily', () => {
it('should return correct index list', function() { test('should return correct index list', () => {
var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
var from = new Date(1432940523000); var from = new Date(1432940523000);
var to = new Date(1433153106000); var to = new Date(1433153106000);
var expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']; var expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01'];
expect(pattern.getIndexList(from, to)).to.eql(expected); expect(pattern.getIndexList(from, to)).toEqual(expected);
}); });
}); });
}); });
@@ -1,25 +1,24 @@
import { describe, beforeEach, it, expect } from 'test/lib/common';
import { ElasticQueryBuilder } from '../query_builder'; import { ElasticQueryBuilder } from '../query_builder';
describe('ElasticQueryBuilder', function() { describe('ElasticQueryBuilder', () => {
var builder; var builder;
beforeEach(function() { beforeEach(() => {
builder = new ElasticQueryBuilder({ timeField: '@timestamp' }); builder = new ElasticQueryBuilder({ timeField: '@timestamp' });
}); });
it('with defaults', function() { it('with defaults', () => {
var query = builder.build({ var query = builder.build({
metrics: [{ type: 'Count', id: '0' }], metrics: [{ type: 'Count', id: '0' }],
timeField: '@timestamp', timeField: '@timestamp',
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
}); });
expect(query.query.bool.filter[0].range['@timestamp'].gte).to.be('$timeFrom'); expect(query.query.bool.filter[0].range['@timestamp'].gte).toBe('$timeFrom');
expect(query.aggs['1'].date_histogram.extended_bounds.min).to.be('$timeFrom'); expect(query.aggs['1'].date_histogram.extended_bounds.min).toBe('$timeFrom');
}); });
it('with defaults on es5.x', function() { it('with defaults on es5.x', () => {
var builder_5x = new ElasticQueryBuilder({ var builder_5x = new ElasticQueryBuilder({
timeField: '@timestamp', timeField: '@timestamp',
esVersion: 5, esVersion: 5,
@@ -31,11 +30,11 @@ describe('ElasticQueryBuilder', function() {
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
}); });
expect(query.query.bool.filter[0].range['@timestamp'].gte).to.be('$timeFrom'); expect(query.query.bool.filter[0].range['@timestamp'].gte).toBe('$timeFrom');
expect(query.aggs['1'].date_histogram.extended_bounds.min).to.be('$timeFrom'); expect(query.aggs['1'].date_histogram.extended_bounds.min).toBe('$timeFrom');
}); });
it('with multiple bucket aggs', function() { it('with multiple bucket aggs', () => {
var query = builder.build({ var query = builder.build({
metrics: [{ type: 'count', id: '1' }], metrics: [{ type: 'count', id: '1' }],
timeField: '@timestamp', timeField: '@timestamp',
@@ -45,11 +44,11 @@ describe('ElasticQueryBuilder', function() {
], ],
}); });
expect(query.aggs['2'].terms.field).to.be('@host'); expect(query.aggs['2'].terms.field).toBe('@host');
expect(query.aggs['2'].aggs['3'].date_histogram.field).to.be('@timestamp'); expect(query.aggs['2'].aggs['3'].date_histogram.field).toBe('@timestamp');
}); });
it('with select field', function() { it('with select field', () => {
var query = builder.build( var query = builder.build(
{ {
metrics: [{ type: 'avg', field: '@value', id: '1' }], metrics: [{ type: 'avg', field: '@value', id: '1' }],
@@ -60,10 +59,10 @@ describe('ElasticQueryBuilder', function() {
); );
var aggs = query.aggs['2'].aggs; var aggs = query.aggs['2'].aggs;
expect(aggs['1'].avg.field).to.be('@value'); expect(aggs['1'].avg.field).toBe('@value');
}); });
it('with term agg and order by metric agg', function() { it('with term agg and order by metric agg', () => {
var query = builder.build( var query = builder.build(
{ {
metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }], metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
@@ -84,11 +83,11 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['2']; var firstLevel = query.aggs['2'];
var secondLevel = firstLevel.aggs['3']; var secondLevel = firstLevel.aggs['3'];
expect(firstLevel.aggs['5'].avg.field).to.be('@value'); expect(firstLevel.aggs['5'].avg.field).toBe('@value');
expect(secondLevel.aggs['5'].avg.field).to.be('@value'); expect(secondLevel.aggs['5'].avg.field).toBe('@value');
}); });
it('with metric percentiles', function() { it('with metric percentiles', () => {
var query = builder.build( var query = builder.build(
{ {
metrics: [ metrics: [
@@ -109,11 +108,11 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3']; var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['1'].percentiles.field).to.be('@load_time'); expect(firstLevel.aggs['1'].percentiles.field).toBe('@load_time');
expect(firstLevel.aggs['1'].percentiles.percents).to.eql([1, 2, 3, 4]); expect(firstLevel.aggs['1'].percentiles.percents).toEqual([1, 2, 3, 4]);
}); });
it('with filters aggs', function() { it('with filters aggs', () => {
var query = builder.build({ var query = builder.build({
metrics: [{ type: 'count', id: '1' }], metrics: [{ type: 'count', id: '1' }],
timeField: '@timestamp', timeField: '@timestamp',
@@ -129,12 +128,12 @@ describe('ElasticQueryBuilder', function() {
], ],
}); });
expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).to.be('@metric:cpu'); expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).toBe('@metric:cpu');
expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).to.be('@metric:logins.count'); expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).toBe('@metric:logins.count');
expect(query.aggs['2'].aggs['4'].date_histogram.field).to.be('@timestamp'); expect(query.aggs['2'].aggs['4'].date_histogram.field).toBe('@timestamp');
}); });
it('with filters aggs on es5.x', function() { it('with filters aggs on es5.x', () => {
var builder_5x = new ElasticQueryBuilder({ var builder_5x = new ElasticQueryBuilder({
timeField: '@timestamp', timeField: '@timestamp',
esVersion: 5, esVersion: 5,
@@ -154,31 +153,31 @@ describe('ElasticQueryBuilder', function() {
], ],
}); });
expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).to.be('@metric:cpu'); expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).toBe('@metric:cpu');
expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).to.be('@metric:logins.count'); expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).toBe('@metric:logins.count');
expect(query.aggs['2'].aggs['4'].date_histogram.field).to.be('@timestamp'); expect(query.aggs['2'].aggs['4'].date_histogram.field).toBe('@timestamp');
}); });
it('with raw_document metric', function() { it('with raw_document metric', () => {
var query = builder.build({ var query = builder.build({
metrics: [{ type: 'raw_document', id: '1', settings: {} }], metrics: [{ type: 'raw_document', id: '1', settings: {} }],
timeField: '@timestamp', timeField: '@timestamp',
bucketAggs: [], bucketAggs: [],
}); });
expect(query.size).to.be(500); expect(query.size).toBe(500);
}); });
it('with raw_document metric size set', function() { it('with raw_document metric size set', () => {
var query = builder.build({ var query = builder.build({
metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }], metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }],
timeField: '@timestamp', timeField: '@timestamp',
bucketAggs: [], bucketAggs: [],
}); });
expect(query.size).to.be(1337); expect(query.size).toBe(1337);
}); });
it('with moving average', function() { it('with moving average', () => {
var query = builder.build({ var query = builder.build({
metrics: [ metrics: [
{ {
@@ -198,12 +197,12 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3']; var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['2']).not.to.be(undefined); expect(firstLevel.aggs['2']).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg).not.to.be(undefined); expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg.buckets_path).to.be('3'); expect(firstLevel.aggs['2'].moving_avg.buckets_path).toBe('3');
}); });
it('with broken moving average', function() { it('with broken moving average', () => {
var query = builder.build({ var query = builder.build({
metrics: [ metrics: [
{ {
@@ -227,13 +226,13 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3']; var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['2']).not.to.be(undefined); expect(firstLevel.aggs['2']).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg).not.to.be(undefined); expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg.buckets_path).to.be('3'); expect(firstLevel.aggs['2'].moving_avg.buckets_path).toBe('3');
expect(firstLevel.aggs['4']).to.be(undefined); expect(firstLevel.aggs['4']).toBe(undefined);
}); });
it('with derivative', function() { it('with derivative', () => {
var query = builder.build({ var query = builder.build({
metrics: [ metrics: [
{ {
@@ -252,12 +251,12 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3']; var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['2']).not.to.be(undefined); expect(firstLevel.aggs['2']).not.toBe(undefined);
expect(firstLevel.aggs['2'].derivative).not.to.be(undefined); expect(firstLevel.aggs['2'].derivative).not.toBe(undefined);
expect(firstLevel.aggs['2'].derivative.buckets_path).to.be('3'); expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('3');
}); });
it('with histogram', function() { it('with histogram', () => {
var query = builder.build({ var query = builder.build({
metrics: [{ id: '1', type: 'count' }], metrics: [{ id: '1', type: 'count' }],
bucketAggs: [ bucketAggs: [
@@ -271,13 +270,13 @@ describe('ElasticQueryBuilder', function() {
}); });
var firstLevel = query.aggs['3']; var firstLevel = query.aggs['3'];
expect(firstLevel.histogram.field).to.be('bytes'); expect(firstLevel.histogram.field).toBe('bytes');
expect(firstLevel.histogram.interval).to.be(10); expect(firstLevel.histogram.interval).toBe(10);
expect(firstLevel.histogram.min_doc_count).to.be(2); expect(firstLevel.histogram.min_doc_count).toBe(2);
expect(firstLevel.histogram.missing).to.be(5); expect(firstLevel.histogram.missing).toBe(5);
}); });
it('with adhoc filters', function() { it('with adhoc filters', () => {
var query = builder.build( var query = builder.build(
{ {
metrics: [{ type: 'Count', id: '0' }], metrics: [{ type: 'Count', id: '0' }],
@@ -295,12 +294,12 @@ describe('ElasticQueryBuilder', function() {
] ]
); );
expect(query.query.bool.must[0].match_phrase['key1'].query).to.be('value1'); expect(query.query.bool.must[0].match_phrase['key1'].query).toBe('value1');
expect(query.query.bool.must[1].match_phrase['key2'].query).to.be('value2'); expect(query.query.bool.must[1].match_phrase['key2'].query).toBe('value2');
expect(query.query.bool.must_not[0].match_phrase['key2'].query).to.be('value2'); expect(query.query.bool.must_not[0].match_phrase['key2'].query).toBe('value2');
expect(query.query.bool.filter[2].range['key3'].lt).to.be('value3'); expect(query.query.bool.filter[2].range['key3'].lt).toBe('value3');
expect(query.query.bool.filter[3].range['key4'].gt).to.be('value4'); expect(query.query.bool.filter[3].range['key4'].gt).toBe('value4');
expect(query.query.bool.filter[4].regexp['key5']).to.be('value5'); expect(query.query.bool.filter[4].regexp['key5']).toBe('value5');
expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).to.be('value6'); expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).toBe('value6');
}); });
}); });
@@ -0,0 +1,93 @@
import * as queryDef from '../query_def';
describe('ElasticQueryDef', () => {
describe('getPipelineAggOptions', () => {
describe('with zero targets', () => {
var response = queryDef.getPipelineAggOptions([]);
test('should return zero', () => {
expect(response.length).toBe(0);
});
});
describe('with count and sum targets', () => {
var targets = {
metrics: [{ type: 'count', field: '@value' }, { type: 'sum', field: '@value' }],
};
var response = queryDef.getPipelineAggOptions(targets);
test('should return zero', () => {
expect(response.length).toBe(2);
});
});
describe('with count and moving average targets', () => {
var targets = {
metrics: [{ type: 'count', field: '@value' }, { type: 'moving_avg', field: '@value' }],
};
var response = queryDef.getPipelineAggOptions(targets);
test('should return one', () => {
expect(response.length).toBe(1);
});
});
describe('with derivatives targets', () => {
var targets = {
metrics: [{ type: 'derivative', field: '@value' }],
};
var response = queryDef.getPipelineAggOptions(targets);
test('should return zero', () => {
expect(response.length).toBe(0);
});
});
});
describe('isPipelineMetric', () => {
describe('moving_avg', () => {
var result = queryDef.isPipelineAgg('moving_avg');
test('is pipe line metric', () => {
expect(result).toBe(true);
});
});
describe('count', () => {
var result = queryDef.isPipelineAgg('count');
test('is not pipe line metric', () => {
expect(result).toBe(false);
});
});
});
describe('pipeline aggs depending on esverison', () => {
describe('using esversion undefined', () => {
test('should not get pipeline aggs', () => {
expect(queryDef.getMetricAggTypes(undefined).length).toBe(9);
});
});
describe('using esversion 1', () => {
test('should not get pipeline aggs', () => {
expect(queryDef.getMetricAggTypes(1).length).toBe(9);
});
});
describe('using esversion 2', () => {
test('should get pipeline aggs', () => {
expect(queryDef.getMetricAggTypes(2).length).toBe(11);
});
});
describe('using esversion 5', () => {
test('should get pipeline aggs', () => {
expect(queryDef.getMetricAggTypes(5).length).toBe(11);
});
});
});
});
@@ -1,95 +0,0 @@
import { describe, it, expect } from 'test/lib/common';
import * as queryDef from '../query_def';
describe('ElasticQueryDef', function() {
describe('getPipelineAggOptions', function() {
describe('with zero targets', function() {
var response = queryDef.getPipelineAggOptions([]);
it('should return zero', function() {
expect(response.length).to.be(0);
});
});
describe('with count and sum targets', function() {
var targets = {
metrics: [{ type: 'count', field: '@value' }, { type: 'sum', field: '@value' }],
};
var response = queryDef.getPipelineAggOptions(targets);
it('should return zero', function() {
expect(response.length).to.be(2);
});
});
describe('with count and moving average targets', function() {
var targets = {
metrics: [{ type: 'count', field: '@value' }, { type: 'moving_avg', field: '@value' }],
};
var response = queryDef.getPipelineAggOptions(targets);
it('should return one', function() {
expect(response.length).to.be(1);
});
});
describe('with derivatives targets', function() {
var targets = {
metrics: [{ type: 'derivative', field: '@value' }],
};
var response = queryDef.getPipelineAggOptions(targets);
it('should return zero', function() {
expect(response.length).to.be(0);
});
});
});
describe('isPipelineMetric', function() {
describe('moving_avg', function() {
var result = queryDef.isPipelineAgg('moving_avg');
it('is pipe line metric', function() {
expect(result).to.be(true);
});
});
describe('count', function() {
var result = queryDef.isPipelineAgg('count');
it('is not pipe line metric', function() {
expect(result).to.be(false);
});
});
});
describe('pipeline aggs depending on esverison', function() {
describe('using esversion undefined', function() {
it('should not get pipeline aggs', function() {
expect(queryDef.getMetricAggTypes(undefined).length).to.be(9);
});
});
describe('using esversion 1', function() {
it('should not get pipeline aggs', function() {
expect(queryDef.getMetricAggTypes(1).length).to.be(9);
});
});
describe('using esversion 2', function() {
it('should get pipeline aggs', function() {
expect(queryDef.getMetricAggTypes(2).length).to.be(11);
});
});
describe('using esversion 5', function() {
it('should get pipeline aggs', function() {
expect(queryDef.getMetricAggTypes(5).length).to.be(11);
});
});
});
});
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'test/lib/common';
import { InfluxQueryBuilder } from '../query_builder'; import { InfluxQueryBuilder } from '../query_builder';
describe('InfluxQueryBuilder', function() { describe('InfluxQueryBuilder', function() {
@@ -1,11 +1,11 @@
import { describe, beforeEach, it, sinon, expect } from '../../../../../test/lib/common'; jest.mock('app/core/core', () => ({}));
import $ from 'jquery'; import $ from 'jquery';
import GraphTooltip from '../graph_tooltip'; import GraphTooltip from '../graph_tooltip';
var scope = { var scope = {
appEvent: sinon.spy(), appEvent: jest.fn(),
onAppEvent: sinon.spy(), onAppEvent: jest.fn(),
ctrl: {}, ctrl: {},
}; };
@@ -47,22 +47,22 @@ describe('findHoverIndexFromData', function() {
it('should return 0 if posX out of lower bounds', function() { it('should return 0 if posX out of lower bounds', function() {
var posX = 99; var posX = 99;
expect(tooltip.findHoverIndexFromData(posX, series)).to.be(0); expect(tooltip.findHoverIndexFromData(posX, series)).toBe(0);
}); });
it('should return n - 1 if posX out of upper bounds', function() { it('should return n - 1 if posX out of upper bounds', function() {
var posX = 108; var posX = 108;
expect(tooltip.findHoverIndexFromData(posX, series)).to.be(series.data.length - 1); expect(tooltip.findHoverIndexFromData(posX, series)).toBe(series.data.length - 1);
}); });
it('should return i if posX in series', function() { it('should return i if posX in series', function() {
var posX = 104; var posX = 104;
expect(tooltip.findHoverIndexFromData(posX, series)).to.be(4); expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4);
}); });
it('should return i if posX not in series and i + 1 > posX', function() { it('should return i if posX not in series and i + 1 > posX', function() {
var posX = 104.9; var posX = 104.9;
expect(tooltip.findHoverIndexFromData(posX, series)).to.be(4); expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4);
}); });
}); });
@@ -73,17 +73,17 @@ describeSharedTooltip('steppedLine false, stack false', function(ctx) {
}); });
it('should return 2 series', function() { it('should return 2 series', function() {
expect(ctx.results.length).to.be(2); expect(ctx.results.length).toBe(2);
}); });
it('should add time to results array', function() { it('should add time to results array', function() {
expect(ctx.results.time).to.be(10); expect(ctx.results.time).toBe(10);
}); });
it('should set value and hoverIndex', function() { it('should set value and hoverIndex', function() {
expect(ctx.results[0].value).to.be(15); expect(ctx.results[0].value).toBe(15);
expect(ctx.results[1].value).to.be(2); expect(ctx.results[1].value).toBe(2);
expect(ctx.results[0].hoverIndex).to.be(0); expect(ctx.results[0].hoverIndex).toBe(0);
}); });
}); });
@@ -121,7 +121,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio
}); });
it('should show stacked value', function() { it('should show stacked value', function() {
expect(ctx.results[1].value).to.be(17); expect(ctx.results[1].value).toBe(17);
}); });
}); });
@@ -152,7 +152,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s
}); });
it('should not show stacked value', function() { it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2); expect(ctx.results[1].value).toBe(2);
}); });
}); });
@@ -184,6 +184,6 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function
}); });
it('should not show stacked value', function() { it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2); expect(ctx.results[1].value).toBe(2);
}); });
}); });
@@ -1,5 +1,3 @@
import { describe, it, expect } from '../../../../../test/lib/common';
import angular from 'angular'; import angular from 'angular';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import { ThresholdManager } from '../threshold_manager'; import { ThresholdManager } from '../threshold_manager';
@@ -38,16 +36,16 @@ describe('ThresholdManager', function() {
it('should add fill for threshold with fill: true', function() { it('should add fill for threshold with fill: true', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(300); expect(markings[0].yaxis.from).toBe(300);
expect(markings[0].yaxis.to).to.be(Infinity); expect(markings[0].yaxis.to).toBe(Infinity);
expect(markings[0].color).to.be('rgba(234, 112, 112, 0.12)'); expect(markings[0].color).toBe('rgba(234, 112, 112, 0.12)');
}); });
it('should add line', function() { it('should add line', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(300); expect(markings[1].yaxis.from).toBe(300);
expect(markings[1].yaxis.to).to.be(300); expect(markings[1].yaxis.to).toBe(300);
expect(markings[1].color).to.be('rgba(237, 46, 24, 0.60)'); expect(markings[1].color).toBe('rgba(237, 46, 24, 0.60)');
}); });
}); });
@@ -59,14 +57,14 @@ describe('ThresholdManager', function() {
it('should add fill for first thresholds to next threshold', function() { it('should add fill for first thresholds to next threshold', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(200); expect(markings[0].yaxis.from).toBe(200);
expect(markings[0].yaxis.to).to.be(300); expect(markings[0].yaxis.to).toBe(300);
}); });
it('should add fill for last thresholds to infinity', function() { it('should add fill for last thresholds to infinity', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(300); expect(markings[1].yaxis.from).toBe(300);
expect(markings[1].yaxis.to).to.be(Infinity); expect(markings[1].yaxis.to).toBe(Infinity);
}); });
}); });
@@ -78,14 +76,14 @@ describe('ThresholdManager', function() {
it('should add fill for first thresholds to next threshold', function() { it('should add fill for first thresholds to next threshold', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(300); expect(markings[0].yaxis.from).toBe(300);
expect(markings[0].yaxis.to).to.be(200); expect(markings[0].yaxis.to).toBe(200);
}); });
it('should add fill for last thresholds to itself', function() { it('should add fill for last thresholds to itself', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(200); expect(markings[1].yaxis.from).toBe(200);
expect(markings[1].yaxis.to).to.be(200); expect(markings[1].yaxis.to).toBe(200);
}); });
}); });
@@ -97,14 +95,14 @@ describe('ThresholdManager', function() {
it('should add fill for first thresholds to next threshold', function() { it('should add fill for first thresholds to next threshold', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(300); expect(markings[0].yaxis.from).toBe(300);
expect(markings[0].yaxis.to).to.be(Infinity); expect(markings[0].yaxis.to).toBe(Infinity);
}); });
it('should add fill for last thresholds to itself', function() { it('should add fill for last thresholds to itself', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(200); expect(markings[1].yaxis.from).toBe(200);
expect(markings[1].yaxis.to).to.be(-Infinity); expect(markings[1].yaxis.to).toBe(-Infinity);
}); });
}); });
@@ -130,12 +128,12 @@ describe('ThresholdManager', function() {
it('should add first threshold for left axis', function() { it('should add first threshold for left axis', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(100); expect(markings[0].yaxis.from).toBe(100);
}); });
it('should add second threshold for right axis', function() { it('should add second threshold for right axis', function() {
var markings = ctx.options.grid.markings; var markings = ctx.options.grid.markings;
expect(markings[1].y2axis.from).to.be(200); expect(markings[1].y2axis.from).toBe(200);
}); });
}); });
}); });
@@ -1,5 +1,3 @@
import { describe, it, expect } from 'test/lib/common';
import { getColorForValue } from '../module'; import { getColorForValue } from '../module';
describe('grafanaSingleStat', function() { describe('grafanaSingleStat', function() {
@@ -11,31 +9,31 @@ describe('grafanaSingleStat', function() {
}; };
it('5 should return green', () => { it('5 should return green', () => {
expect(getColorForValue(data, 5)).to.be('green'); expect(getColorForValue(data, 5)).toBe('green');
}); });
it('19.9 should return green', () => { it('19.9 should return green', () => {
expect(getColorForValue(data, 19.9)).to.be('green'); expect(getColorForValue(data, 19.9)).toBe('green');
}); });
it('20 should return yellow', () => { it('20 should return yellow', () => {
expect(getColorForValue(data, 20)).to.be('yellow'); expect(getColorForValue(data, 20)).toBe('yellow');
}); });
it('20.1 should return yellow', () => { it('20.1 should return yellow', () => {
expect(getColorForValue(data, 20.1)).to.be('yellow'); expect(getColorForValue(data, 20.1)).toBe('yellow');
}); });
it('25 should return yellow', () => { it('25 should return yellow', () => {
expect(getColorForValue(data, 25)).to.be('yellow'); expect(getColorForValue(data, 25)).toBe('yellow');
}); });
it('50 should return red', () => { it('50 should return red', () => {
expect(getColorForValue(data, 50)).to.be('red'); expect(getColorForValue(data, 50)).toBe('red');
}); });
it('55 should return red', () => { it('55 should return red', () => {
expect(getColorForValue(data, 55)).to.be('red'); expect(getColorForValue(data, 55)).toBe('red');
}); });
}); });
}); });
@@ -47,15 +45,15 @@ describe('grafanaSingleStat', function() {
}; };
it('-30 should return green', () => { it('-30 should return green', () => {
expect(getColorForValue(data, -30)).to.be('green'); expect(getColorForValue(data, -30)).toBe('green');
}); });
it('1 should return green', () => { it('1 should return green', () => {
expect(getColorForValue(data, 1)).to.be('yellow'); expect(getColorForValue(data, 1)).toBe('yellow');
}); });
it('22 should return green', () => { it('22 should return green', () => {
expect(getColorForValue(data, 22)).to.be('red'); expect(getColorForValue(data, 22)).toBe('red');
}); });
}); });
@@ -66,7 +64,7 @@ describe('grafanaSingleStat', function() {
}; };
it('-30 should return green', () => { it('-30 should return green', () => {
expect(getColorForValue(data, -26)).to.be('yellow'); expect(getColorForValue(data, -26)).toBe('yellow');
}); });
}); });
}); });
+16
View File
@@ -100,6 +100,22 @@
// Success appears as green // Success appears as green
.btn-success { .btn-success {
@include buttonBackground($btn-success-bg, $btn-success-bg-hl); @include buttonBackground($btn-success-bg, $btn-success-bg-hl);
&--processing {
@include button-outline-variant($gray-1);
@include box-shadow(none);
cursor: default;
&:hover,
&:active,
&:active:hover,
&:focus,
&:disabled {
color: $gray-1;
background-color: transparent;
border-color: $gray-1;
}
}
} }
// Info appears as a neutral blue // Info appears as a neutral blue
.btn-secondary { .btn-secondary {
+12 -4
View File
@@ -11,11 +11,20 @@
display: inline-block; display: inline-block;
} }
.dashboard-row__drag, .dashboard-row__drag {
.dashboard-row__actions {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
.dashboard-row__actions {
visibility: hidden;
}
.dashboard-row__toggle-target {
flex: 1;
cursor: pointer;
margin-right: 15px;
}
} }
&:hover { &:hover {
@@ -43,7 +52,6 @@
color: $text-muted; color: $text-muted;
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
flex-grow: 1;
transition: 200ms opacity ease-in 200ms; transition: 200ms opacity ease-in 200ms;
a { a {
@@ -69,7 +77,7 @@
cursor: move; cursor: move;
width: 1rem; width: 1rem;
height: 100%; height: 100%;
background: url("../img/grab_dark.svg") no-repeat 50% 50%; background: url('../img/grab_dark.svg') no-repeat 50% 50%;
background-size: 8px; background-size: 8px;
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
+1 -1
View File
@@ -32,7 +32,7 @@
.panel-alert-icon:before { .panel-alert-icon:before {
content: '\e611'; content: '\e611';
position: relative; position: relative;
top: 1px; top: 5px;
left: -3px; left: -3px;
} }
} }
+15 -8
View File
@@ -68,17 +68,26 @@ div.flot-text {
font-weight: $font-weight-semi-bold; font-weight: $font-weight-semi-bold;
position: relative; position: relative;
width: 100%; width: 100%;
display: block; display: flex;
padding-bottom: 2px; flex-wrap: nowrap;
justify-content: center;
padding: 4px 0 4px;
} }
.panel-title-text { .panel-title-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: calc(100% - 38px);
cursor: pointer; cursor: pointer;
font-weight: $font-weight-semi-bold; font-weight: $font-weight-semi-bold;
&:hover { &:hover {
color: $link-hover-color; color: $link-hover-color;
} }
.panel-has-alert & {
max-width: calc(100% - 54px);
}
} }
.panel-menu-container { .panel-menu-container {
@@ -97,7 +106,7 @@ div.flot-text {
width: 16px; width: 16px;
height: 16px; height: 16px;
left: 1px; left: 1px;
top: 4px; top: 2px;
&:hover { &:hover {
color: $link-hover-color; color: $link-hover-color;
@@ -114,8 +123,6 @@ div.flot-text {
} }
.panel-header { .panel-header {
text-align: center;
&:hover { &:hover {
transition: background-color 0.1s ease-in-out; transition: background-color 0.1s ease-in-out;
background-color: $panel-header-hover-bg; background-color: $panel-header-hover-bg;
@@ -156,8 +163,8 @@ div.flot-text {
.fa { .fa {
position: relative; position: relative;
top: -4px; top: -2px;
left: -6px; left: 6px;
font-size: 75%; font-size: 75%;
z-index: 1; z-index: 1;
} }
@@ -174,7 +181,7 @@ div.flot-text {
display: block; display: block;
@include panel-corner-color(lighten($panel-bg, 4%)); @include panel-corner-color(lighten($panel-bg, 4%));
.fa { .fa {
left: -5px; left: 4px;
} }
.fa:before { .fa:before {
content: '\f08e'; content: '\f08e';
+13 -3
View File
@@ -13,6 +13,13 @@ $login-border: #8daac5;
justify-content: center; justify-content: center;
background-image: url(../img/heatmap_bg_test.svg); background-image: url(../img/heatmap_bg_test.svg);
background-size: cover; background-size: cover;
color: #d8d9da;
& a {
color: #d8d9da !important;
}
& .btn-primary {
@include buttonBackground(#ff6600, #bc3e06);
}
} }
input:-webkit-autofill, input:-webkit-autofill,
@@ -25,8 +32,9 @@ textarea:-webkit-autofill:focus,
select:-webkit-autofill, select:-webkit-autofill,
select:-webkit-autofill:hover, select:-webkit-autofill:hover,
select:-webkit-autofill:focus { select:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0px 1000px $black inset; -webkit-box-shadow: 0 0 0px 1000px $black inset !important;
-webkit-text-fill-color: $gray-7 !important; -webkit-text-fill-color: #fbfbfb !important;
box-shadow: 0 0 0px 1000px $black inset;
} }
.login-form-group { .login-form-group {
@@ -46,6 +54,8 @@ select:-webkit-autofill:focus {
border: 1px solid $login-border; border: 1px solid $login-border;
border-radius: 4px; border-radius: 4px;
opacity: 0.6; opacity: 0.6;
background: $black;
color: #fbfbfb;
&:focus { &:focus {
border: 1px solid $login-border; border: 1px solid $login-border;
@@ -103,7 +113,7 @@ select:-webkit-autofill:focus {
} }
.icon-gf-grafana_wordmark { .icon-gf-grafana_wordmark {
color: $link-color; color: darken($white, 11%);
position: relative; position: relative;
font-size: 2rem; font-size: 2rem;
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
@@ -1,5 +1,3 @@
import {describe, beforeEach, it, expect} from 'test/lib/common';
import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version'; import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version';
describe("SemVersion", () => { describe("SemVersion", () => {
@@ -8,10 +6,10 @@ describe("SemVersion", () => {
describe('parsing', () => { describe('parsing', () => {
it('should parse version properly', () => { it('should parse version properly', () => {
let semver = new SemVersion(version); let semver = new SemVersion(version);
expect(semver.major).to.be(1); expect(semver.major).toBe(1);
expect(semver.minor).to.be(0); expect(semver.minor).toBe(0);
expect(semver.patch).to.be(0); expect(semver.patch).toBe(0);
expect(semver.meta).to.be('alpha.1'); expect(semver.meta).toBe('alpha.1');
}); });
}); });
@@ -30,7 +28,7 @@ describe("SemVersion", () => {
{value: '3.5', expected: false}, {value: '3.5', expected: false},
]; ];
cases.forEach((testCase) => { cases.forEach((testCase) => {
expect(semver.isGtOrEq(testCase.value)).to.be(testCase.expected); expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected);
}); });
}); });
}); });
@@ -48,7 +46,7 @@ describe("SemVersion", () => {
{values: ['3.4.5', '3.5'], expected: false}, {values: ['3.4.5', '3.5'], expected: false},
]; ];
cases.forEach((testCase) => { cases.forEach((testCase) => {
expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).to.be(testCase.expected); expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected);
}); });
}); });
}); });
+18
View File
@@ -1,4 +1,22 @@
import { configure } from 'enzyme'; import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import 'jquery';
import $ from 'jquery';
import 'angular';
import angular from 'angular';
angular.module('grafana', ['ngRoute']);
angular.module('grafana.services', ['ngRoute', '$strap.directives']);
angular.module('grafana.panels', []);
angular.module('grafana.controllers', []);
angular.module('grafana.directives', []);
angular.module('grafana.filters', []);
angular.module('grafana.routes', ['ngRoute']);
jest.mock('app/core/core', () => ({}));
jest.mock('app/features/plugins/plugin_loader', () => ({}));
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });
var global = <any>window;
global.$ = global.jQuery = $;
+1 -1
View File
@@ -23,7 +23,7 @@ module.exports = merge(common, {
}, },
resolve: { resolve: {
extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png'], extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png', '.html'],
}, },
devtool: 'eval-source-map', devtool: 'eval-source-map',
+8444 -3855
View File
File diff suppressed because it is too large Load Diff
+554 -104
View File
File diff suppressed because it is too large Load Diff
+60 -32
View File
@@ -1,3 +1,5 @@
// +build cgo
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. // Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
// //
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
@@ -7,10 +9,13 @@ package sqlite3
/* /*
#cgo CFLAGS: -std=gnu99 #cgo CFLAGS: -std=gnu99
#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE=1 #cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE=1 -DHAVE_USLEEP=1
#cgo linux,!android CFLAGS: -DHAVE_PREAD64=1 -DHAVE_PWRITE64=1
#cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4_UNICODE61 #cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4_UNICODE61
#cgo CFLAGS: -DSQLITE_TRACE_SIZE_LIMIT=15 #cgo CFLAGS: -DSQLITE_TRACE_SIZE_LIMIT=15
#cgo CFLAGS: -DSQLITE_OMIT_DEPRECATED
#cgo CFLAGS: -DSQLITE_DISABLE_INTRINSIC #cgo CFLAGS: -DSQLITE_DISABLE_INTRINSIC
#cgo CFLAGS: -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
#cgo CFLAGS: -Wno-deprecated-declarations #cgo CFLAGS: -Wno-deprecated-declarations
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include <sqlite3-binding.h> #include <sqlite3-binding.h>
@@ -170,6 +175,12 @@ var SQLiteTimestampFormats = []string{
"2006-01-02", "2006-01-02",
} }
const (
columnDate string = "date"
columnDatetime string = "datetime"
columnTimestamp string = "timestamp"
)
func init() { func init() {
sql.Register("sqlite3", &SQLiteDriver{}) sql.Register("sqlite3", &SQLiteDriver{})
} }
@@ -389,7 +400,7 @@ func (c *SQLiteConn) RegisterCommitHook(callback func() int) {
if callback == nil { if callback == nil {
C.sqlite3_commit_hook(c.db, nil, nil) C.sqlite3_commit_hook(c.db, nil, nil)
} else { } else {
C.sqlite3_commit_hook(c.db, (*[0]byte)(unsafe.Pointer(C.commitHookTrampoline)), unsafe.Pointer(newHandle(c, callback))) C.sqlite3_commit_hook(c.db, (*[0]byte)(C.commitHookTrampoline), unsafe.Pointer(newHandle(c, callback)))
} }
} }
@@ -402,7 +413,7 @@ func (c *SQLiteConn) RegisterRollbackHook(callback func()) {
if callback == nil { if callback == nil {
C.sqlite3_rollback_hook(c.db, nil, nil) C.sqlite3_rollback_hook(c.db, nil, nil)
} else { } else {
C.sqlite3_rollback_hook(c.db, (*[0]byte)(unsafe.Pointer(C.rollbackHookTrampoline)), unsafe.Pointer(newHandle(c, callback))) C.sqlite3_rollback_hook(c.db, (*[0]byte)(C.rollbackHookTrampoline), unsafe.Pointer(newHandle(c, callback)))
} }
} }
@@ -419,7 +430,7 @@ func (c *SQLiteConn) RegisterUpdateHook(callback func(int, string, string, int64
if callback == nil { if callback == nil {
C.sqlite3_update_hook(c.db, nil, nil) C.sqlite3_update_hook(c.db, nil, nil)
} else { } else {
C.sqlite3_update_hook(c.db, (*[0]byte)(unsafe.Pointer(C.updateHookTrampoline)), unsafe.Pointer(newHandle(c, callback))) C.sqlite3_update_hook(c.db, (*[0]byte)(C.updateHookTrampoline), unsafe.Pointer(newHandle(c, callback)))
} }
} }
@@ -501,7 +512,7 @@ func (c *SQLiteConn) RegisterFunc(name string, impl interface{}, pure bool) erro
} }
func sqlite3CreateFunction(db *C.sqlite3, zFunctionName *C.char, nArg C.int, eTextRep C.int, pApp uintptr, xFunc unsafe.Pointer, xStep unsafe.Pointer, xFinal unsafe.Pointer) C.int { func sqlite3CreateFunction(db *C.sqlite3, zFunctionName *C.char, nArg C.int, eTextRep C.int, pApp uintptr, xFunc unsafe.Pointer, xStep unsafe.Pointer, xFinal unsafe.Pointer) C.int {
return C._sqlite3_create_function(db, zFunctionName, nArg, eTextRep, C.uintptr_t(pApp), (*[0]byte)(unsafe.Pointer(xFunc)), (*[0]byte)(unsafe.Pointer(xStep)), (*[0]byte)(unsafe.Pointer(xFinal))) return C._sqlite3_create_function(db, zFunctionName, nArg, eTextRep, C.uintptr_t(pApp), (*[0]byte)(xFunc), (*[0]byte)(xStep), (*[0]byte)(xFinal))
} }
// RegisterAggregator makes a Go type available as a SQLite aggregation function. // RegisterAggregator makes a Go type available as a SQLite aggregation function.
@@ -780,6 +791,8 @@ func errorString(err Error) string {
// Enable or disable enforcement of foreign keys. X can be 1 or 0. // Enable or disable enforcement of foreign keys. X can be 1 or 0.
// _recursive_triggers=X // _recursive_triggers=X
// Enable or disable recursive triggers. X can be 1 or 0. // Enable or disable recursive triggers. X can be 1 or 0.
// _mutex=XXX
// Specify mutex mode. XXX can be "no", "full".
func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
if C.sqlite3_threadsafe() == 0 { if C.sqlite3_threadsafe() == 0 {
return nil, errors.New("sqlite library was not compiled for thread-safe operation") return nil, errors.New("sqlite library was not compiled for thread-safe operation")
@@ -790,6 +803,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
busyTimeout := 5000 busyTimeout := 5000
foreignKeys := -1 foreignKeys := -1
recursiveTriggers := -1 recursiveTriggers := -1
mutex := C.int(C.SQLITE_OPEN_FULLMUTEX)
pos := strings.IndexRune(dsn, '?') pos := strings.IndexRune(dsn, '?')
if pos >= 1 { if pos >= 1 {
params, err := url.ParseQuery(dsn[pos+1:]) params, err := url.ParseQuery(dsn[pos+1:])
@@ -856,6 +870,18 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
} }
} }
// _mutex
if val := params.Get("_mutex"); val != "" {
switch val {
case "no":
mutex = C.SQLITE_OPEN_NOMUTEX
case "full":
mutex = C.SQLITE_OPEN_FULLMUTEX
default:
return nil, fmt.Errorf("Invalid _mutex: %v", val)
}
}
if !strings.HasPrefix(dsn, "file:") { if !strings.HasPrefix(dsn, "file:") {
dsn = dsn[:pos] dsn = dsn[:pos]
} }
@@ -865,9 +891,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
name := C.CString(dsn) name := C.CString(dsn)
defer C.free(unsafe.Pointer(name)) defer C.free(unsafe.Pointer(name))
rv := C._sqlite3_open_v2(name, &db, rv := C._sqlite3_open_v2(name, &db,
C.SQLITE_OPEN_FULLMUTEX| mutex|C.SQLITE_OPEN_READWRITE|C.SQLITE_OPEN_CREATE,
C.SQLITE_OPEN_READWRITE|
C.SQLITE_OPEN_CREATE,
nil) nil)
if rv != 0 { if rv != 0 {
return nil, Error{Code: ErrNo(rv)} return nil, Error{Code: ErrNo(rv)}
@@ -1070,7 +1094,7 @@ func (s *SQLiteStmt) bind(args []namedValue) error {
case int64: case int64:
rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v)) rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
case bool: case bool:
if bool(v) { if v {
rv = C.sqlite3_bind_int(s.s, n, 1) rv = C.sqlite3_bind_int(s.s, n, 1)
} else { } else {
rv = C.sqlite3_bind_int(s.s, n, 0) rv = C.sqlite3_bind_int(s.s, n, 0)
@@ -1121,18 +1145,20 @@ func (s *SQLiteStmt) query(ctx context.Context, args []namedValue) (driver.Rows,
done: make(chan struct{}), done: make(chan struct{}),
} }
go func(db *C.sqlite3) { if ctxdone := ctx.Done(); ctxdone != nil {
select { go func(db *C.sqlite3) {
case <-ctx.Done():
select { select {
case <-ctxdone:
select {
case <-rows.done:
default:
C.sqlite3_interrupt(db)
rows.Close()
}
case <-rows.done: case <-rows.done:
default:
C.sqlite3_interrupt(db)
rows.Close()
} }
case <-rows.done: }(s.c.db)
} }
}(s.c.db)
return rows, nil return rows, nil
} }
@@ -1166,19 +1192,21 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
return nil, err return nil, err
} }
done := make(chan struct{}) if ctxdone := ctx.Done(); ctxdone != nil {
defer close(done) done := make(chan struct{})
go func(db *C.sqlite3) { defer close(done)
select { go func(db *C.sqlite3) {
case <-done:
case <-ctx.Done():
select { select {
case <-done: case <-done:
default: case <-ctxdone:
C.sqlite3_interrupt(db) select {
case <-done:
default:
C.sqlite3_interrupt(db)
}
} }
} }(s.c.db)
}(s.c.db) }
var rowid, changes C.longlong var rowid, changes C.longlong
rv := C._sqlite3_step(s.s, &rowid, &changes) rv := C._sqlite3_step(s.s, &rowid, &changes)
@@ -1272,7 +1300,7 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
case C.SQLITE_INTEGER: case C.SQLITE_INTEGER:
val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i))) val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
switch rc.decltype[i] { switch rc.decltype[i] {
case "timestamp", "datetime", "date": case columnTimestamp, columnDatetime, columnDate:
var t time.Time var t time.Time
// Assume a millisecond unix timestamp if it's 13 digits -- too // Assume a millisecond unix timestamp if it's 13 digits -- too
// large to be a reasonable timestamp in seconds. // large to be a reasonable timestamp in seconds.
@@ -1303,10 +1331,10 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
n := int(C.sqlite3_column_bytes(rc.s.s, C.int(i))) n := int(C.sqlite3_column_bytes(rc.s.s, C.int(i)))
switch dest[i].(type) { switch dest[i].(type) {
case sql.RawBytes: case sql.RawBytes:
dest[i] = (*[1 << 30]byte)(unsafe.Pointer(p))[0:n] dest[i] = (*[1 << 30]byte)(p)[0:n]
default: default:
slice := make([]byte, n) slice := make([]byte, n)
copy(slice[:], (*[1 << 30]byte)(unsafe.Pointer(p))[0:n]) copy(slice[:], (*[1 << 30]byte)(p)[0:n])
dest[i] = slice dest[i] = slice
} }
case C.SQLITE_NULL: case C.SQLITE_NULL:
@@ -1319,7 +1347,7 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
s := C.GoStringN((*C.char)(unsafe.Pointer(C.sqlite3_column_text(rc.s.s, C.int(i)))), C.int(n)) s := C.GoStringN((*C.char)(unsafe.Pointer(C.sqlite3_column_text(rc.s.s, C.int(i)))), C.int(n))
switch rc.decltype[i] { switch rc.decltype[i] {
case "timestamp", "datetime", "date": case columnTimestamp, columnDatetime, columnDate:
var t time.Time var t time.Time
s = strings.TrimSuffix(s, "Z") s = strings.TrimSuffix(s, "Z")
for _, format := range SQLiteTimestampFormats { for _, format := range SQLiteTimestampFormats {
+2
View File
@@ -1,3 +1,5 @@
// +build cgo
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>. // Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
// //
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
+12
View File
@@ -0,0 +1,12 @@
// Copyright (C) 2018 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// +build solaris
package sqlite3
/*
#cgo CFLAGS: -D__EXTENSIONS__=1
*/
import "C"
+12 -10
View File
@@ -28,10 +28,10 @@ import (
// Trace... constants identify the possible events causing callback invocation. // Trace... constants identify the possible events causing callback invocation.
// Values are same as the corresponding SQLite Trace Event Codes. // Values are same as the corresponding SQLite Trace Event Codes.
const ( const (
TraceStmt = C.SQLITE_TRACE_STMT TraceStmt = uint32(C.SQLITE_TRACE_STMT)
TraceProfile = C.SQLITE_TRACE_PROFILE TraceProfile = uint32(C.SQLITE_TRACE_PROFILE)
TraceRow = C.SQLITE_TRACE_ROW TraceRow = uint32(C.SQLITE_TRACE_ROW)
TraceClose = C.SQLITE_TRACE_CLOSE TraceClose = uint32(C.SQLITE_TRACE_CLOSE)
) )
type TraceInfo struct { type TraceInfo struct {
@@ -71,7 +71,7 @@ type TraceUserCallback func(TraceInfo) int
type TraceConfig struct { type TraceConfig struct {
Callback TraceUserCallback Callback TraceUserCallback
EventMask C.uint EventMask uint32
WantExpandedSQL bool WantExpandedSQL bool
} }
@@ -105,6 +105,8 @@ func traceCallbackTrampoline(
// Parameter named 'X' in SQLite docs (eXtra event data?): // Parameter named 'X' in SQLite docs (eXtra event data?):
xValue unsafe.Pointer) C.int { xValue unsafe.Pointer) C.int {
eventCode := uint32(traceEventCode)
if ctx == nil { if ctx == nil {
panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode)) panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode))
} }
@@ -114,7 +116,7 @@ func traceCallbackTrampoline(
var traceConf TraceConfig var traceConf TraceConfig
var found bool var found bool
if traceEventCode == TraceClose { if eventCode == TraceClose {
// clean up traceMap: 'pop' means get and delete // clean up traceMap: 'pop' means get and delete
traceConf, found = popTraceMapping(connHandle) traceConf, found = popTraceMapping(connHandle)
} else { } else {
@@ -123,16 +125,16 @@ func traceCallbackTrampoline(
if !found { if !found {
panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)", panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)",
connHandle, traceEventCode)) connHandle, eventCode))
} }
var info TraceInfo var info TraceInfo
info.EventCode = uint32(traceEventCode) info.EventCode = eventCode
info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0) info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0)
info.ConnHandle = connHandle info.ConnHandle = connHandle
switch traceEventCode { switch eventCode {
case TraceStmt: case TraceStmt:
info.StmtHandle = uintptr(p) info.StmtHandle = uintptr(p)
@@ -183,7 +185,7 @@ func traceCallbackTrampoline(
// registering this callback trampoline with SQLite --- for cleanup. // registering this callback trampoline with SQLite --- for cleanup.
// In the future there may be more events forced to "selected" in SQLite // In the future there may be more events forced to "selected" in SQLite
// for the driver's needs. // for the driver's needs.
if traceConf.EventMask&traceEventCode == 0 { if traceConf.EventMask&eventCode == 0 {
return 0 return 0
} }
+7
View File
@@ -293,6 +293,9 @@ struct sqlite3_api_routines {
int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*)); int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*)); void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
void *(*value_pointer)(sqlite3_value*,const char*); void *(*value_pointer)(sqlite3_value*,const char*);
int (*vtab_nochange)(sqlite3_context*);
int (*value_nochange)(sqlite3_value*);
const char *(*vtab_collation)(sqlite3_index_info*,int);
}; };
/* /*
@@ -559,6 +562,10 @@ typedef int (*sqlite3_loadext_entry)(
#define sqlite3_bind_pointer sqlite3_api->bind_pointer #define sqlite3_bind_pointer sqlite3_api->bind_pointer
#define sqlite3_result_pointer sqlite3_api->result_pointer #define sqlite3_result_pointer sqlite3_api->result_pointer
#define sqlite3_value_pointer sqlite3_api->value_pointer #define sqlite3_value_pointer sqlite3_api->value_pointer
/* Version 3.22.0 and later */
#define sqlite3_vtab_nochange sqlite3_api->vtab_nochange
#define sqlite3_value_nochange sqlite3_api->value_nochange
#define sqlite3_vtab_collation sqlite3_api->vtab_collation
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
+21
View File
@@ -0,0 +1,21 @@
// +build !cgo
package sqlite3
import (
"database/sql"
"database/sql/driver"
"errors"
)
func init() {
sql.Register("sqlite3", &SQLiteDriverMock{})
}
type SQLiteDriverMock struct{}
var errorMsg = errors.New("Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub")
func (SQLiteDriverMock) Open(s string) (driver.Conn, error) {
return nil, errorMsg
}