Merge branch 'master' into alerting_reminder

* master: (84 commits)
  docs: adds info about grafana-dev container
  changelog: add notes about closing #12282
  Added Litre/min and milliLitre/min in Flow (#12282)
  remove papaparse dependency
  list name is deleteDatasources, not delete_datasources
  remove internal influx ifql datasource
  Document the endpoint for deleting an org
  tests: rewrite into table tests
  influxdb: adds mode func to backend
  changelog: add notes about closing #11484
  changelog: add notes about closing #11233
  Remove import
  Fix PR feedback
  Removed papaparse from external plugin exports
  Karma to Jest: query_builder
  dsproxy: move http client variable back
  Karma to Jest: threshold_mapper
  Expose react and slate to external plugins
  Karma to Jest: threshold_manager
  Karma to Jest: query_def, index_pattern
  ...
This commit is contained in:
bergquist
2018-06-15 12:45:35 +02:00
82 changed files with 10230 additions and 4873 deletions
+10 -5
View File
@@ -183,16 +183,21 @@ jobs:
command: 'sudo pip install awscli'
- run:
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:
name: Trigger Windows build
command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
- run:
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:
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:
docker:
@@ -241,8 +246,8 @@ workflows:
- mysql-integration-test
- postgres-integration-test
filters:
branches:
only: master
branches:
only: master
release:
jobs:
- build-all:
+17
View File
@@ -2,6 +2,21 @@
### 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)
# 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)
* **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)
@@ -35,6 +50,8 @@
* **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167)
* **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670)
* **Provisioning**: Support symlinked files in dashboard provisioning config files [#11958](https://github.com/grafana/grafana/issues/11958)
* **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525)
* **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657)
# 5.1.3 (2018-05-16)
Generated
+4 -4
View File
@@ -226,7 +226,7 @@
version = "v1.1.1"
[[projects]]
branch = "renderer"
branch = "master"
name = "github.com/grafana/grafana-plugin-model"
packages = [
"go/datasource",
@@ -331,8 +331,8 @@
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
version = "v1.6.0"
revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
version = "v1.7.0"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
@@ -670,6 +670,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "6c7ae4bcbe7fa4430d3bdbf204df1b7c59cba88151fbcefa167ce15e6351b6d3"
inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
solver-name = "gps-cdcl"
solver-version = 1
+2 -2
View File
@@ -100,7 +100,7 @@ ignored = [
version = "1.1.1"
[[constraint]]
branch = "renderer"
branch = "master"
name = "github.com/grafana/grafana-plugin-model"
[[constraint]]
@@ -129,7 +129,7 @@ ignored = [
[[constraint]]
name = "github.com/mattn/go-sqlite3"
version = "1.6.0"
version = "1.7.0"
[[constraint]]
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
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
@@ -20,7 +20,7 @@ queries through the use of query references.
## Adding the data source
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.
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.
More information in the [Provisioning documentation](/features/datasources/prometheus/).
More information in the [Provisioning documentation](/administration/provisioning/).
<div class="clearfix"></div>
+21
View File
@@ -331,6 +331,27 @@ Content-Type: application/json
{"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 /api/orgs/:orgId/users`
+21 -13
View File
@@ -419,25 +419,33 @@ allowed_organizations = github google
## [auth.google]
You need to create a Google project. You can do this in the [Google
Developer Console](https://console.developers.google.com/project). When
you create the project you will need to specify a callback URL. Specify
this as callback:
First, you need to create a Google OAuth Client:
```bash
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/google
```
1. Go to https://console.developers.google.com/apis/credentials
This callback URL must match the full HTTP address that you use in your
browser to access Grafana, but with the prefix path of `/login/google`.
When the Google project is created you will get a Client ID and a Client
Secret. Specify these in the Grafana configuration file. For example:
2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the
menu that drops down
3. Enter the following:
- Application Type: Web Application
- Name: Grafana
- Authorized Javascript Origins: https://grafana.mycompany.com
- Authorized Redirect URLs: https://grafana.mycompany.com/login/google
Replace https://grafana.mycompany.com with the URL of your Grafana instance.
4. Click Create
5. Copy the Client ID and Client Secret from the 'OAuth Client' modal
Specify the Client ID and Secret in the Grafana configuration file. For example:
```bash
[auth.google]
enabled = true
client_id = YOUR_GOOGLE_APP_CLIENT_ID
client_secret = YOUR_GOOGLE_APP_CLIENT_SECRET
client_id = CLIENT_ID
client_secret = CLIENT_SECRET
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token
+7
View File
@@ -49,6 +49,11 @@ $ docker run \
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
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)
> 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.
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)
- QueryCtrl (Required)
- ConfigCtrl (Required)
- QueryOptionsCtrl
- AnnotationsQueryCtrl
## 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.
## 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
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
- [Go 1.9.2](https://golang.org/dl/)
- [Go 1.10](https://golang.org/dl/)
- [Git](https://git-scm.com/downloads)
- [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.
@@ -66,13 +66,13 @@ You can run a local instance of Grafana by running:
```bash
./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`
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
does this.
@@ -124,7 +124,7 @@ Learn more about Grafana config options in the [Configuration section](/installa
## 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.
## Troubleshooting
# Troubleshooting
**Problem**: PhantomJS or node-sass errors when running grunt
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "5.0.4",
"testing": "5.0.4"
"stable": "5.1.3",
"testing": "5.1.3"
}
+1
View File
@@ -57,4 +57,5 @@ type ImportDashboardCommand struct {
Overwrite bool `json:"overwrite"`
Dashboard *simplejson.Json `json:"dashboard"`
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,
"ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"exploreEnabled": setting.ExploreEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm,
"externalUserMngInfo": setting.ExternalUserMngInfo,
+18 -3
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 {
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{
Text: "Create",
Id: "create",
@@ -233,7 +234,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
}
}
if c.OrgRole == m.ROLE_ADMIN {
if c.IsGrafanaAdmin || c.OrgRole == m.ROLE_ADMIN {
cfgNode := &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
@@ -287,10 +288,24 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
},
}
if c.IsGrafanaAdmin {
if c.OrgRole != m.ROLE_ADMIN {
cfgNode = &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/admin/users",
Children: make([]*dtos.NavLink, 0),
}
}
if c.OrgRole == m.ROLE_ADMIN && c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
})
}
if c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Text: "Server Admin",
HideFromTabs: true,
+20 -8
View File
@@ -25,12 +25,9 @@ import (
)
var (
logger = log.New("data-proxy-log")
client = &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
tokenCache = map[int64]*jwtToken{}
logger = log.New("data-proxy-log")
tokenCache = map[string]*jwtToken{}
client = newHTTPClient()
)
type jwtToken struct {
@@ -48,6 +45,10 @@ type DataSourceProxy struct {
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 {
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() {
if err := proxy.validateRequest(); err != 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) {
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)) {
logger.Info("Using token from cache")
return cachedToken.AccessToken, nil
@@ -350,12 +358,16 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
tokenCache[proxy.ds.Id] = &token
tokenCache[proxy.getAccessTokenCacheKey()] = &token
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
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) {
t, err := template.New("content").Parse(text)
if err != nil {
+134
View File
@@ -1,9 +1,13 @@
package pluginproxy
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
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() {
plugin := &plugins.DataSourcePlugin{}
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,
Inputs: apiCmd.Inputs,
Overwrite: apiCmd.Overwrite,
FolderId: apiCmd.FolderId,
Dashboard: apiCmd.Dashboard,
}
+3 -1
View File
@@ -16,6 +16,7 @@ type ImportDashboardCommand struct {
Path string
Inputs []ImportDashboardInput
Overwrite bool
FolderId int64
OrgId int64
User *m.SignedInUser
@@ -70,7 +71,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
UserId: cmd.User.UserId,
Overwrite: cmd.Overwrite,
PluginId: cmd.PluginId,
FolderId: dashboard.FolderId,
FolderId: cmd.FolderId,
}
dto := &dashboards.SaveDashboardDTO{
@@ -91,6 +92,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
Title: savedDash.Title,
Path: cmd.Path,
Revision: savedDash.Data.Get("revision").MustInt64(1),
FolderId: savedDash.FolderId,
ImportedUri: "db/" + savedDash.Slug,
ImportedUrl: savedDash.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
+1
View File
@@ -17,6 +17,7 @@ type PluginDashboardInfoDTO struct {
ImportedUrl string `json:"importedUrl"`
Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"`
FolderId int64 `json:"folderId"`
ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"`
Description string `json:"description"`
+10 -12
View File
@@ -41,10 +41,8 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
type TeamsNotifier struct {
NotifierBase
Url string
Recipient string
Mention string
log log.Logger
Url string
log log.Logger
}
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
@@ -75,17 +73,17 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
})
}
message := this.Mention
if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
} else {
message += " " // summary must not be empty
message := ""
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message = evalContext.Rule.Message
}
body := map[string]interface{}{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": message,
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
// 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(),
"themeColor": evalContext.GetStateModel().Color,
"sections": []map[string]interface{}{
+1 -1
View File
@@ -89,7 +89,7 @@ func (ss *SqlStore) ensureAdminUser() error {
systemUserCountQuery := m.GetSystemUserCountStatsQuery{}
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 {
+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) {
version, err := ds.JsonData.Get("esVersion").Int()
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()
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()
+1
View File
@@ -31,6 +31,7 @@ func init() {
renders["mean"] = QueryDefinition{Renderer: functionRenderer}
renders["median"] = QueryDefinition{Renderer: functionRenderer}
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer,
+29 -75
View File
@@ -4,85 +4,39 @@ import (
"testing"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
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")}
query := &Query{}
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
query := &Query{}
Convey("render field ", func() {
part, err := NewQueryPart("field", []string{"value"})
So(err, ShouldBeNil)
for _, tc := range tcs {
part, err := NewQueryPart(tc.mode, tc.params)
if err != nil {
t.Errorf("Expected NewQueryPart to not return an error. error: %v", err)
}
res := part.Render(query, queryContext, "value")
So(res, ShouldEqual, `"value"`)
})
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))`)
})
})
res := part.Render(query, queryContext, tc.input)
if res != tc.expected {
t.Errorf("expected %v to render into %s", tc, tc.expected)
}
}
}
+1 -1
View File
@@ -199,7 +199,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
body.mousemove(userActivityDetected);
body.keydown(userActivityDetected);
// 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
document.addEventListener('visibilitychange', userActivityDetected);
@@ -13,6 +13,10 @@
<i class="fa fa-plus"></i>
Folder
</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 class="page-action-bar page-action-bar--narrow" ng-show="ctrl.hasFilters">
@@ -294,6 +294,16 @@ export class ManageDashboardsCtrl {
return url;
}
importDashboardUrl() {
let url = 'dashboard/import';
if (this.folderId) {
url += `?folderId=${this.folderId}`;
}
return url;
}
}
export function manageDashboardsDirective() {
@@ -52,11 +52,11 @@
<a href="dashboards/folder/new" class="search-filter-box-link" ng-if="ctrl.isEditor">
<i class="gicon gicon-folder-new"></i> New folder
</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
</a>
<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>
</div>
</div>
+1
View File
@@ -16,6 +16,7 @@ class Settings {
defaultDatasource: string;
alertingEnabled: boolean;
authProxyEnabled: boolean;
exploreEnabled: boolean;
ldapEnabled: boolean;
oauth: any;
disableUserSignUp: boolean;
+2 -1
View File
@@ -1,6 +1,7 @@
import $ from 'jquery';
import _ from 'lodash';
import config from 'app/core/config';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { encodePathComponent } from 'app/core/utils/location_util';
@@ -178,7 +179,7 @@ export class KeybindingSrv {
});
// jump to explore if permissions allow
if (this.contextSrv.isEditor) {
if (this.contextSrv.isEditor && config.exploreEnabled) {
this.bind('x', async () => {
if (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;
}
}
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.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -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.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
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.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa');
kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa');
kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg');
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.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
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
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
@@ -1014,6 +1019,7 @@ kbn.getUnitFormats = function() {
{ text: 'Watt (W)', value: 'watt' },
{ text: 'Kilowatt (kW)', value: 'kwatt' },
{ text: 'Milliwatt (mW)', value: 'mwatt' },
{ text: 'Watt per square metre (W/m2)', value: 'Wm2' },
{ text: 'Volt-ampere (VA)', value: 'voltamp' },
{ text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
{ text: 'Volt-ampere reactive (var)', value: 'voltampreact' },
@@ -1049,6 +1055,7 @@ kbn.getUnitFormats = function() {
{ text: 'Bars', value: 'pressurebar' },
{ text: 'Kilobars', value: 'pressurekbar' },
{ text: 'Hectopascals', value: 'pressurehpa' },
{ text: 'Kilopascals', value: 'pressurekpa' },
{ text: 'Inches of mercury', value: 'pressurehg' },
{ text: 'PSI', value: 'pressurepsi' },
],
@@ -1069,6 +1076,9 @@ kbn.getUnitFormats = function() {
{ text: 'Cubic meters/sec (cms)', value: 'flowcms' },
{ text: 'Cubic feet/sec (cfs)', value: 'flowcfs' },
{ 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);
expect(updated).to.be(true);
expect(panel.thresholds[0].op).to.be('gt');
expect(panel.thresholds[0].value).to.be(100);
expect(updated).toBe(true);
expect(panel.thresholds[0].op).toBe('gt');
expect(panel.thresholds[0].value).toBe(100);
});
});
@@ -39,12 +39,12 @@ describe('ThresholdMapper', () => {
};
var updated = ThresholdMapper.alertToGraphThresholds(panel);
expect(updated).to.be(true);
expect(panel.thresholds[0].op).to.be('lt');
expect(panel.thresholds[0].value).to.be(100);
expect(updated).toBe(true);
expect(panel.thresholds[0].op).toBe('lt');
expect(panel.thresholds[0].value).toBe(100);
expect(panel.thresholds[1].op).to.be('gt');
expect(panel.thresholds[1].value).to.be(200);
expect(panel.thresholds[1].op).toBe('gt');
expect(panel.thresholds[1].value).toBe(200);
});
});
@@ -63,12 +63,12 @@ describe('ThresholdMapper', () => {
};
var updated = ThresholdMapper.alertToGraphThresholds(panel);
expect(updated).to.be(true);
expect(panel.thresholds[0].op).to.be('gt');
expect(panel.thresholds[0].value).to.be(100);
expect(updated).toBe(true);
expect(panel.thresholds[0].op).toBe('gt');
expect(panel.thresholds[0].value).toBe(100);
expect(panel.thresholds[1].op).to.be('lt');
expect(panel.thresholds[1].value).to.be(200);
expect(panel.thresholds[1].op).toBe('lt');
expect(panel.thresholds[1].value).toBe(200);
});
});
});
@@ -21,6 +21,9 @@ export class DashboardImportCtrl {
uidValidationError: any;
autoGenerateUid: boolean;
autoGenerateUidValue: string;
folderId: number;
initialFolderTitle: string;
isValidFolderSelection: boolean;
/** @ngInject */
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
@@ -31,6 +34,8 @@ export class DashboardImportCtrl {
this.uidExists = false;
this.autoGenerateUid = true;
this.autoGenerateUidValue = 'auto-generated';
this.folderId = $routeParams.folderId ? Number($routeParams.folderId) || 0 : null;
this.initialFolderTitle = 'Select a folder';
// check gnetId in url
if ($routeParams.gnetId) {
@@ -102,8 +107,9 @@ export class DashboardImportCtrl {
this.nameExists = false;
this.validationSrv
.validateNewDashboardName(0, this.dash.title)
.validateNewDashboardName(this.folderId, this.dash.title)
.then(() => {
this.nameExists = false;
this.hasNameValidationError = false;
})
.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() {
var inputs = this.inputs.map(input => {
return {
@@ -153,6 +176,7 @@ export class DashboardImportCtrl {
dashboard: this.dash,
overwrite: true,
inputs: inputs,
folderId: this.folderId,
})
.then(res => {
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) {
let regex = new RegExp(filter, 'i');
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">
<input
type="text"
className="gf-form-input max-width-20"
autoFocus
className="gf-form-input gf-form--grow"
placeholder="Panel Search Filter"
value={this.state.filter}
onChange={this.filterChange.bind(this)}
onKeyPress={this.filterKeyPress.bind(this)}
/>
<i className="gf-form-input-icon fa fa-search" />
</label>
@@ -84,15 +84,18 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
'fa-chevron-right': this.state.collapsed,
});
let title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
const hiddenPanels = this.props.panel.panels ? this.props.panel.panels.length : 0;
const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
const panels = count === 1 ? 'panel' : 'panels';
return (
<div className={classes}>
<a className="dashboard-row__title pointer" onClick={this.toggle}>
<i className={chevronClass} />
{title}
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
<span className="dashboard-row__panel_count">
({count} {panels})
</span>
</a>
{this.dashboard.meta.canEdit === true && (
<div className="dashboard-row__actions">
@@ -104,6 +107,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
</a>
</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>
);
@@ -132,23 +132,26 @@ export class FolderPickerCtrl {
}
private loadInitialValue() {
if (this.initialFolderId && this.initialFolderId > 0) {
this.getOptions('').then(result => {
this.folder = _.find(result, { value: this.initialFolderId });
if (!this.folder) {
this.folder = { text: this.initialTitle, value: this.initialFolderId };
}
this.onFolderLoad();
});
} else {
if (this.initialTitle && this.initialFolderId === null) {
this.folder = { text: this.initialTitle, value: null };
} else {
this.folder = { text: this.rootName, value: 0 };
const resetFolder = { text: this.initialTitle, value: null };
const rootFolder = { text: this.rootName, value: 0 };
this.getOptions('').then(result => {
let folder;
if (this.initialFolderId) {
folder = _.find(result, { value: this.initialFolderId });
} else if (this.enableReset && this.initialTitle && this.initialFolderId === null) {
folder = resetFolder;
}
if (!folder) {
if (this.isEditor) {
folder = rootFolder;
} else {
folder = result.length > 0 ? result[0] : resetFolder;
}
}
this.folder = folder;
this.onFolderLoad();
}
});
}
private onFolderLoad() {
@@ -80,6 +80,20 @@
</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 gf-form--grow">
<span class="gf-form-label width-15">
@@ -132,10 +146,10 @@
</div>
<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
</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)
</button>
<a class="btn btn-link" ng-click="ctrl.back()">Cancel</a>
+15 -2
View File
@@ -50,8 +50,17 @@ const template = `
</div>
<div class="gf-form-button-row text-center">
<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Save</button>
<a class="btn btn-link" ng-click="ctrl.dismiss();">Cancel</a>
<button
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>
</form>
</div>
@@ -68,6 +77,7 @@ export class SaveDashboardModalCtrl {
originalCurrent = [];
max: number;
saveForm: any;
isSaving: boolean;
dismiss: () => void;
timeChange = false;
variableValueChange = false;
@@ -76,6 +86,7 @@ export class SaveDashboardModalCtrl {
constructor(private dashboardSrv) {
this.message = '';
this.max = 64;
this.isSaving = false;
this.templating = dashboardSrv.dash.templating.list;
this.compareTemplating();
@@ -126,6 +137,8 @@ export class SaveDashboardModalCtrl {
var dashboard = this.dashboardSrv.getCurrent();
var saveModel = dashboard.getSaveModelClone(options);
this.isSaving = true;
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() {
const items = [];
if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
items.push({
text: '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>
</ul>
</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>`;
function renderMenuItem(item, ctrl) {
@@ -1,8 +1,19 @@
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 { PanelModel } from 'app/features/dashboard/panel_model';
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
describe('MetricsPanelCtrl', () => {
let ctrl;
@@ -1,5 +1,4 @@
import '../playlist_edit_ctrl';
import { describe, beforeEach, it, expect } from 'test/lib/common';
import { PlaylistEditCtrl } from '../playlist_edit_ctrl';
describe('PlaylistEditCtrl', () => {
@@ -20,13 +19,13 @@ describe('PlaylistEditCtrl', () => {
describe('searchresult returns 2 dashboards, ', () => {
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', () => {
ctx.filterFoundPlaylistItems();
expect(ctx.filteredDashboards.length).to.be(2);
expect(ctx.filteredTags.length).to.be(2);
expect(ctx.filteredDashboards.length).toBe(2);
expect(ctx.filteredTags.length).toBe(2);
});
describe('adds one dashboard to playlist, ', () => {
@@ -37,16 +36,16 @@ describe('PlaylistEditCtrl', () => {
});
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', () => {
expect(ctx.filteredDashboards.length).to.be(1);
expect(ctx.filteredTags.length).to.be(1);
expect(ctx.filteredDashboards.length).toBe(1);
expect(ctx.filteredTags.length).toBe(1);
});
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, ', () => {
@@ -57,14 +56,14 @@ describe('PlaylistEditCtrl', () => {
});
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', () => {
expect(ctx.dashboardresult.length).to.be(2);
expect(ctx.filteredDashboards.length).to.be(2);
expect(ctx.filteredTags.length).to.be(2);
expect(ctx.tagresult.length).to.be(2);
expect(ctx.dashboardresult.length).toBe(2);
expect(ctx.filteredDashboards.length).toBe(2);
expect(ctx.filteredTags.length).toBe(2);
expect(ctx.tagresult.length).toBe(2);
});
});
});
@@ -211,6 +211,10 @@ coreModule.directive('datasourceHttpSettings', function() {
pre: function($scope, elem, attrs) {
// do not show access option if direct access is disabled
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
$scope.showAccessHelp = false;
$scope.toggleAccessHelp = function() {
$scope.showAccessHelp = !$scope.showAccessHelp;
};
$scope.getSuggestUrls = function() {
return [$scope.suggestUrl];
@@ -30,7 +30,7 @@
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showAccessHelp = !ctrl.showAccessHelp">
<label class="gf-form-label query-keyword pointer" ng-click="toggleAccessHelp()">
Help&nbsp;
<i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp">&nbsp;</i>
@@ -38,7 +38,7 @@
</div>
</div>
<div class="alert alert-info" ng-show="ctrl.showAccessHelp">
<div class="alert alert-info" ng-show="showAccessHelp">
<div class="alert-body">
<p>
Access mode controls how requests to the data source will be handled.
@@ -6,7 +6,6 @@ import coreModule from 'app/core/core_module';
import { importPluginModule } from './plugin_loader';
import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
import { DashboardRowCtrl } from './row_ctrl';
/** @ngInject **/
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
@@ -59,15 +58,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
}
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 = {
name: 'panel-plugin-' + scope.panel.type,
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
case 'annotations-query-ctrl': {
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 angular from 'angular';
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 TimeSeries from 'app/core/time_series2';
import TableModel from 'app/core/table_model';
@@ -69,6 +78,14 @@ exposeToPlugin('d3', d3);
exposeToPlugin('rxjs/Subject', Subject);
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
exposeToPlugin('vendor/npm/rxjs/Rx', {
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 { ElasticConfigCtrl } from './config_ctrl';
class ElasticQueryOptionsCtrl {
static templateUrl = 'partials/query.options.html';
}
class ElasticAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
}
@@ -14,6 +10,5 @@ export {
ElasticDatasource as Datasource,
ElasticQueryCtrl as QueryCtrl,
ElasticConfigCtrl as ConfigCtrl,
ElasticQueryOptionsCtrl as QueryOptionsCtrl,
ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};
@@ -1,13 +1,12 @@
import { describe, beforeEach, it, expect } from 'test/lib/common';
import { ElasticResponse } from '../elastic_response';
describe('ElasticResponse', function() {
describe('ElasticResponse', () => {
var targets;
var response;
var result;
describe('simple query and count', function() {
beforeEach(function() {
describe('simple query and count', () => {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -39,19 +38,19 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 1 series', function() {
expect(result.data.length).to.be(1);
expect(result.data[0].target).to.be('Count');
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].datapoints[0][0]).to.be(10);
expect(result.data[0].datapoints[0][1]).to.be(1000);
it('should return 1 series', () => {
expect(result.data.length).toBe(1);
expect(result.data[0].target).toBe('Count');
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].datapoints[0][0]).toBe(10);
expect(result.data[0].datapoints[0][1]).toBe(1000);
});
});
describe('simple query count & avg aggregation', function() {
describe('simple query count & avg aggregation', () => {
var result;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -85,22 +84,22 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 2 series', function() {
expect(result.data.length).to.be(2);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].datapoints[0][0]).to.be(10);
expect(result.data[0].datapoints[0][1]).to.be(1000);
it('should return 2 series', () => {
expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].datapoints[0][0]).toBe(10);
expect(result.data[0].datapoints[0][1]).toBe(1000);
expect(result.data[1].target).to.be('Average value');
expect(result.data[1].datapoints[0][0]).to.be(88);
expect(result.data[1].datapoints[1][0]).to.be(99);
expect(result.data[1].target).toBe('Average value');
expect(result.data[1].datapoints[0][0]).toBe(88);
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;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -141,18 +140,18 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 2 series', function() {
expect(result.data.length).to.be(2);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].target).to.be('server1');
expect(result.data[1].target).to.be('server2');
it('should return 2 series', () => {
expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).toBe('server1');
expect(result.data[1].target).toBe('server2');
});
});
describe('single group by query two metrics', function() {
describe('single group by query two metrics', () => {
var result;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -199,20 +198,20 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 2 series', function() {
expect(result.data.length).to.be(4);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].target).to.be('server1 Count');
expect(result.data[1].target).to.be('server1 Average @value');
expect(result.data[2].target).to.be('server2 Count');
expect(result.data[3].target).to.be('server2 Average @value');
it('should return 2 series', () => {
expect(result.data.length).toBe(4);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).toBe('server1 Count');
expect(result.data[1].target).toBe('server1 Average @value');
expect(result.data[2].target).toBe('server2 Count');
expect(result.data[3].target).toBe('server2 Average @value');
});
});
describe('with percentiles ', function() {
describe('with percentiles ', () => {
var result;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -246,21 +245,21 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 2 series', function() {
expect(result.data.length).to.be(2);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].target).to.be('p75');
expect(result.data[1].target).to.be('p90');
expect(result.data[0].datapoints[0][0]).to.be(3.3);
expect(result.data[0].datapoints[0][1]).to.be(1000);
expect(result.data[1].datapoints[1][0]).to.be(4.5);
it('should return 2 series', () => {
expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).toBe('p75');
expect(result.data[1].target).toBe('p90');
expect(result.data[0].datapoints[0][0]).toBe(3.3);
expect(result.data[0].datapoints[0][1]).toBe(1000);
expect(result.data[1].datapoints[1][0]).toBe(4.5);
});
});
describe('with extended_stats', function() {
describe('with extended_stats', () => {
var result;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -322,21 +321,21 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 4 series', function() {
expect(result.data.length).to.be(4);
expect(result.data[0].datapoints.length).to.be(1);
expect(result.data[0].target).to.be('server1 Max');
expect(result.data[1].target).to.be('server1 Std Dev Upper');
it('should return 4 series', () => {
expect(result.data.length).toBe(4);
expect(result.data[0].datapoints.length).toBe(1);
expect(result.data[0].target).toBe('server1 Max');
expect(result.data[1].target).toBe('server1 Std Dev Upper');
expect(result.data[0].datapoints[0][0]).to.be(10.2);
expect(result.data[1].datapoints[0][0]).to.be(3);
expect(result.data[0].datapoints[0][0]).toBe(10.2);
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;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -385,19 +384,19 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 2 series', function() {
expect(result.data.length).to.be(3);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].target).to.be('server1 Count and {{not_exist}} server1');
expect(result.data[1].target).to.be('server2 Count and {{not_exist}} server2');
expect(result.data[2].target).to.be('0 Count and {{not_exist}} 0');
it('should return 2 series', () => {
expect(result.data.length).toBe(3);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).toBe('server1 Count and {{not_exist}} server1');
expect(result.data[1].target).toBe('server2 Count and {{not_exist}} server2');
expect(result.data[2].target).toBe('0 Count and {{not_exist}} 0');
});
});
describe('histogram response', function() {
describe('histogram response', () => {
var result;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -420,16 +419,16 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return table with byte and count', function() {
expect(result.data[0].rows.length).to.be(3);
expect(result.data[0].columns).to.eql([{ text: 'bytes', filterable: true }, { text: 'Count' }]);
it('should return table with byte and count', () => {
expect(result.data[0].rows.length).toBe(3);
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;
beforeEach(function() {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -472,16 +471,16 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return 2 series', function() {
expect(result.data.length).to.be(2);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].target).to.be('@metric:cpu');
expect(result.data[1].target).to.be('@metric:logins.count');
it('should return 2 series', () => {
expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].target).toBe('@metric:cpu');
expect(result.data[1].target).toBe('@metric:logins.count');
});
});
describe('with dropfirst and last aggregation', function() {
beforeEach(function() {
describe('with dropfirst and last aggregation', () => {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -528,14 +527,14 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should remove first and last value', function() {
expect(result.data.length).to.be(2);
expect(result.data[0].datapoints.length).to.be(1);
it('should remove first and last value', () => {
expect(result.data.length).toBe(2);
expect(result.data[0].datapoints.length).toBe(1);
});
});
describe('No group by time', function() {
beforeEach(function() {
describe('No group by time', () => {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -570,21 +569,21 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return table', function() {
expect(result.data.length).to.be(1);
expect(result.data[0].type).to.be('table');
expect(result.data[0].rows.length).to.be(2);
expect(result.data[0].rows[0][0]).to.be('server-1');
expect(result.data[0].rows[0][1]).to.be(1000);
expect(result.data[0].rows[0][2]).to.be(369);
it('should return table', () => {
expect(result.data.length).toBe(1);
expect(result.data[0].type).toBe('table');
expect(result.data[0].rows.length).toBe(2);
expect(result.data[0].rows[0][0]).toBe('server-1');
expect(result.data[0].rows[0][1]).toBe(1000);
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][1]).to.be(2000);
expect(result.data[0].rows[1][0]).toBe('server-2');
expect(result.data[0].rows[1][1]).toBe(2000);
});
});
describe('Multiple metrics of same type', function() {
beforeEach(function() {
describe('Multiple metrics of same type', () => {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -615,15 +614,15 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should include field in metric name', function() {
expect(result.data[0].type).to.be('table');
expect(result.data[0].rows[0][1]).to.be(1000);
expect(result.data[0].rows[0][2]).to.be(3000);
it('should include field in metric name', () => {
expect(result.data[0].type).toBe('table');
expect(result.data[0].rows[0][1]).toBe(1000);
expect(result.data[0].rows[0][2]).toBe(3000);
});
});
describe('Raw documents query', function() {
beforeEach(function() {
describe('Raw documents query', () => {
beforeEach(() => {
targets = [
{
refId: 'A',
@@ -657,13 +656,13 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return docs', function() {
expect(result.data.length).to.be(1);
expect(result.data[0].type).to.be('docs');
expect(result.data[0].total).to.be(100);
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].datapoints[0].sourceProp).to.be('asd');
expect(result.data[0].datapoints[0].fieldProp).to.be('field');
it('should return docs', () => {
expect(result.data.length).toBe(1);
expect(result.data[0].type).toBe('docs');
expect(result.data[0].total).toBe(100);
expect(result.data[0].datapoints.length).toBe(2);
expect(result.data[0].datapoints[0].sourceProp).toBe('asd');
expect(result.data[0].datapoints[0].fieldProp).toBe('field');
});
});
});
@@ -1,38 +1,37 @@
///<amd-dependency path="test/specs/helpers" name="helpers" />
import { describe, it, expect } from 'test/lib/common';
import moment from 'moment';
import { IndexPattern } from '../index_pattern';
describe('IndexPattern', function() {
describe('when getting index for today', function() {
it('should return correct index name', function() {
describe('IndexPattern', () => {
describe('when getting index for today', () => {
test('should return correct index name', () => {
var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
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('no interval', function() {
it('should return correct index', function() {
describe('when getting index list for time range', () => {
describe('no interval', () => {
test('should return correct index', () => {
var pattern = new IndexPattern('my-metrics', null);
var from = new Date(2015, 4, 30, 1, 2, 3);
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() {
it('should return correct index list', function() {
describe('daily', () => {
test('should return correct index list', () => {
var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
var from = new Date(1432940523000);
var to = new Date(1433153106000);
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';
describe('ElasticQueryBuilder', function() {
describe('ElasticQueryBuilder', () => {
var builder;
beforeEach(function() {
beforeEach(() => {
builder = new ElasticQueryBuilder({ timeField: '@timestamp' });
});
it('with defaults', function() {
it('with defaults', () => {
var query = builder.build({
metrics: [{ type: 'Count', id: '0' }],
timeField: '@timestamp',
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
});
expect(query.query.bool.filter[0].range['@timestamp'].gte).to.be('$timeFrom');
expect(query.aggs['1'].date_histogram.extended_bounds.min).to.be('$timeFrom');
expect(query.query.bool.filter[0].range['@timestamp'].gte).toBe('$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({
timeField: '@timestamp',
esVersion: 5,
@@ -31,11 +30,11 @@ describe('ElasticQueryBuilder', function() {
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
});
expect(query.query.bool.filter[0].range['@timestamp'].gte).to.be('$timeFrom');
expect(query.aggs['1'].date_histogram.extended_bounds.min).to.be('$timeFrom');
expect(query.query.bool.filter[0].range['@timestamp'].gte).toBe('$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({
metrics: [{ type: 'count', id: '1' }],
timeField: '@timestamp',
@@ -45,11 +44,11 @@ describe('ElasticQueryBuilder', function() {
],
});
expect(query.aggs['2'].terms.field).to.be('@host');
expect(query.aggs['2'].aggs['3'].date_histogram.field).to.be('@timestamp');
expect(query.aggs['2'].terms.field).toBe('@host');
expect(query.aggs['2'].aggs['3'].date_histogram.field).toBe('@timestamp');
});
it('with select field', function() {
it('with select field', () => {
var query = builder.build(
{
metrics: [{ type: 'avg', field: '@value', id: '1' }],
@@ -60,10 +59,10 @@ describe('ElasticQueryBuilder', function() {
);
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(
{
metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
@@ -84,11 +83,11 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['2'];
var secondLevel = firstLevel.aggs['3'];
expect(firstLevel.aggs['5'].avg.field).to.be('@value');
expect(secondLevel.aggs['5'].avg.field).to.be('@value');
expect(firstLevel.aggs['5'].avg.field).toBe('@value');
expect(secondLevel.aggs['5'].avg.field).toBe('@value');
});
it('with metric percentiles', function() {
it('with metric percentiles', () => {
var query = builder.build(
{
metrics: [
@@ -109,11 +108,11 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['1'].percentiles.field).to.be('@load_time');
expect(firstLevel.aggs['1'].percentiles.percents).to.eql([1, 2, 3, 4]);
expect(firstLevel.aggs['1'].percentiles.field).toBe('@load_time');
expect(firstLevel.aggs['1'].percentiles.percents).toEqual([1, 2, 3, 4]);
});
it('with filters aggs', function() {
it('with filters aggs', () => {
var query = builder.build({
metrics: [{ type: 'count', id: '1' }],
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:logins.count'].query_string.query).to.be('@metric:logins.count');
expect(query.aggs['2'].aggs['4'].date_histogram.field).to.be('@timestamp');
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).toBe('@metric:logins.count');
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({
timeField: '@timestamp',
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:logins.count'].query_string.query).to.be('@metric:logins.count');
expect(query.aggs['2'].aggs['4'].date_histogram.field).to.be('@timestamp');
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).toBe('@metric:logins.count');
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({
metrics: [{ type: 'raw_document', id: '1', settings: {} }],
timeField: '@timestamp',
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({
metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }],
timeField: '@timestamp',
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({
metrics: [
{
@@ -198,12 +197,12 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['2']).not.to.be(undefined);
expect(firstLevel.aggs['2'].moving_avg).not.to.be(undefined);
expect(firstLevel.aggs['2'].moving_avg.buckets_path).to.be('3');
expect(firstLevel.aggs['2']).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined);
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({
metrics: [
{
@@ -227,13 +226,13 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['2']).not.to.be(undefined);
expect(firstLevel.aggs['2'].moving_avg).not.to.be(undefined);
expect(firstLevel.aggs['2'].moving_avg.buckets_path).to.be('3');
expect(firstLevel.aggs['4']).to.be(undefined);
expect(firstLevel.aggs['2']).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined);
expect(firstLevel.aggs['2'].moving_avg.buckets_path).toBe('3');
expect(firstLevel.aggs['4']).toBe(undefined);
});
it('with derivative', function() {
it('with derivative', () => {
var query = builder.build({
metrics: [
{
@@ -252,12 +251,12 @@ describe('ElasticQueryBuilder', function() {
var firstLevel = query.aggs['3'];
expect(firstLevel.aggs['2']).not.to.be(undefined);
expect(firstLevel.aggs['2'].derivative).not.to.be(undefined);
expect(firstLevel.aggs['2'].derivative.buckets_path).to.be('3');
expect(firstLevel.aggs['2']).not.toBe(undefined);
expect(firstLevel.aggs['2'].derivative).not.toBe(undefined);
expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('3');
});
it('with histogram', function() {
it('with histogram', () => {
var query = builder.build({
metrics: [{ id: '1', type: 'count' }],
bucketAggs: [
@@ -271,13 +270,13 @@ describe('ElasticQueryBuilder', function() {
});
var firstLevel = query.aggs['3'];
expect(firstLevel.histogram.field).to.be('bytes');
expect(firstLevel.histogram.interval).to.be(10);
expect(firstLevel.histogram.min_doc_count).to.be(2);
expect(firstLevel.histogram.missing).to.be(5);
expect(firstLevel.histogram.field).toBe('bytes');
expect(firstLevel.histogram.interval).toBe(10);
expect(firstLevel.histogram.min_doc_count).toBe(2);
expect(firstLevel.histogram.missing).toBe(5);
});
it('with adhoc filters', function() {
it('with adhoc filters', () => {
var query = builder.build(
{
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[1].match_phrase['key2'].query).to.be('value2');
expect(query.query.bool.must_not[0].match_phrase['key2'].query).to.be('value2');
expect(query.query.bool.filter[2].range['key3'].lt).to.be('value3');
expect(query.query.bool.filter[3].range['key4'].gt).to.be('value4');
expect(query.query.bool.filter[4].regexp['key5']).to.be('value5');
expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).to.be('value6');
expect(query.query.bool.must[0].match_phrase['key1'].query).toBe('value1');
expect(query.query.bool.must[1].match_phrase['key2'].query).toBe('value2');
expect(query.query.bool.must_not[0].match_phrase['key2'].query).toBe('value2');
expect(query.query.bool.filter[2].range['key3'].lt).toBe('value3');
expect(query.query.bool.filter[3].range['key4'].gt).toBe('value4');
expect(query.query.bool.filter[4].regexp['key5']).toBe('value5');
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';
describe('InfluxQueryBuilder', function() {
@@ -23,10 +23,11 @@
</div>
<div class="gf-form">
<folder-picker root-name="All"
initial-folder-id="ctrl.panel.folderId"
<folder-picker initial-folder-id="ctrl.panel.folderId"
on-change="ctrl.onFolderChange($folder)"
label-class="width-6">
label-class="width-6"
initial-title="'All'"
enable-reset="true">
</folder-picker>
</div>
+3 -2
View File
@@ -17,7 +17,7 @@ class DashListCtrl extends PanelCtrl {
search: false,
starred: true,
headings: true,
folderId: 0,
folderId: null,
};
/** @ngInject */
@@ -85,7 +85,8 @@ class DashListCtrl extends PanelCtrl {
limit: this.panel.limit,
query: this.panel.query,
tag: this.panel.tags,
folderId: this.panel.folderId,
folderIds: this.panel.folderId,
type: 'dash-db',
};
return this.backendSrv.search(params).then(result => {
@@ -1,11 +1,11 @@
import { describe, beforeEach, it, sinon, expect } from '../../../../../test/lib/common';
jest.mock('app/core/core', () => ({}));
import $ from 'jquery';
import GraphTooltip from '../graph_tooltip';
var scope = {
appEvent: sinon.spy(),
onAppEvent: sinon.spy(),
appEvent: jest.fn(),
onAppEvent: jest.fn(),
ctrl: {},
};
@@ -47,22 +47,22 @@ describe('findHoverIndexFromData', function() {
it('should return 0 if posX out of lower bounds', function() {
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() {
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() {
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() {
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() {
expect(ctx.results.length).to.be(2);
expect(ctx.results.length).toBe(2);
});
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() {
expect(ctx.results[0].value).to.be(15);
expect(ctx.results[1].value).to.be(2);
expect(ctx.results[0].hoverIndex).to.be(0);
expect(ctx.results[0].value).toBe(15);
expect(ctx.results[1].value).toBe(2);
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() {
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() {
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() {
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 TimeSeries from 'app/core/time_series2';
import { ThresholdManager } from '../threshold_manager';
@@ -38,16 +36,16 @@ describe('ThresholdManager', function() {
it('should add fill for threshold with fill: true', function() {
var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(300);
expect(markings[0].yaxis.to).to.be(Infinity);
expect(markings[0].color).to.be('rgba(234, 112, 112, 0.12)');
expect(markings[0].yaxis.from).toBe(300);
expect(markings[0].yaxis.to).toBe(Infinity);
expect(markings[0].color).toBe('rgba(234, 112, 112, 0.12)');
});
it('should add line', function() {
var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(300);
expect(markings[1].yaxis.to).to.be(300);
expect(markings[1].color).to.be('rgba(237, 46, 24, 0.60)');
expect(markings[1].yaxis.from).toBe(300);
expect(markings[1].yaxis.to).toBe(300);
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() {
var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(200);
expect(markings[0].yaxis.to).to.be(300);
expect(markings[0].yaxis.from).toBe(200);
expect(markings[0].yaxis.to).toBe(300);
});
it('should add fill for last thresholds to infinity', function() {
var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(300);
expect(markings[1].yaxis.to).to.be(Infinity);
expect(markings[1].yaxis.from).toBe(300);
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() {
var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(300);
expect(markings[0].yaxis.to).to.be(200);
expect(markings[0].yaxis.from).toBe(300);
expect(markings[0].yaxis.to).toBe(200);
});
it('should add fill for last thresholds to itself', function() {
var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(200);
expect(markings[1].yaxis.to).to.be(200);
expect(markings[1].yaxis.from).toBe(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() {
var markings = ctx.options.grid.markings;
expect(markings[0].yaxis.from).to.be(300);
expect(markings[0].yaxis.to).to.be(Infinity);
expect(markings[0].yaxis.from).toBe(300);
expect(markings[0].yaxis.to).toBe(Infinity);
});
it('should add fill for last thresholds to itself', function() {
var markings = ctx.options.grid.markings;
expect(markings[1].yaxis.from).to.be(200);
expect(markings[1].yaxis.to).to.be(-Infinity);
expect(markings[1].yaxis.from).toBe(200);
expect(markings[1].yaxis.to).toBe(-Infinity);
});
});
@@ -130,12 +128,12 @@ describe('ThresholdManager', function() {
it('should add first threshold for left axis', function() {
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() {
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';
describe('grafanaSingleStat', function() {
@@ -11,31 +9,31 @@ describe('grafanaSingleStat', function() {
};
it('5 should return green', () => {
expect(getColorForValue(data, 5)).to.be('green');
expect(getColorForValue(data, 5)).toBe('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', () => {
expect(getColorForValue(data, 20)).to.be('yellow');
expect(getColorForValue(data, 20)).toBe('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', () => {
expect(getColorForValue(data, 25)).to.be('yellow');
expect(getColorForValue(data, 25)).toBe('yellow');
});
it('50 should return red', () => {
expect(getColorForValue(data, 50)).to.be('red');
expect(getColorForValue(data, 50)).toBe('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', () => {
expect(getColorForValue(data, -30)).to.be('green');
expect(getColorForValue(data, -30)).toBe('green');
});
it('1 should return green', () => {
expect(getColorForValue(data, 1)).to.be('yellow');
expect(getColorForValue(data, 1)).toBe('yellow');
});
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', () => {
expect(getColorForValue(data, -26)).to.be('yellow');
expect(getColorForValue(data, -26)).toBe('yellow');
});
});
});
+16
View File
@@ -100,6 +100,22 @@
// Success appears as green
.btn-success {
@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
.btn-secondary {
+12 -4
View File
@@ -11,11 +11,20 @@
display: inline-block;
}
.dashboard-row__drag,
.dashboard-row__actions {
.dashboard-row__drag {
visibility: visible;
opacity: 1;
}
.dashboard-row__actions {
visibility: hidden;
}
.dashboard-row__toggle-target {
flex: 1;
cursor: pointer;
margin-right: 15px;
}
}
&:hover {
@@ -43,7 +52,6 @@
color: $text-muted;
visibility: hidden;
opacity: 0;
flex-grow: 1;
transition: 200ms opacity ease-in 200ms;
a {
@@ -69,7 +77,7 @@
cursor: move;
width: 1rem;
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;
visibility: hidden;
position: absolute;
+1 -1
View File
@@ -32,7 +32,7 @@
.panel-alert-icon:before {
content: '\e611';
position: relative;
top: 1px;
top: 5px;
left: -3px;
}
}
+15 -8
View File
@@ -68,17 +68,26 @@ div.flot-text {
font-weight: $font-weight-semi-bold;
position: relative;
width: 100%;
display: block;
padding-bottom: 2px;
display: flex;
flex-wrap: nowrap;
justify-content: center;
padding: 4px 0 4px;
}
.panel-title-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: calc(100% - 38px);
cursor: pointer;
font-weight: $font-weight-semi-bold;
&:hover {
color: $link-hover-color;
}
.panel-has-alert & {
max-width: calc(100% - 54px);
}
}
.panel-menu-container {
@@ -97,7 +106,7 @@ div.flot-text {
width: 16px;
height: 16px;
left: 1px;
top: 4px;
top: 2px;
&:hover {
color: $link-hover-color;
@@ -114,8 +123,6 @@ div.flot-text {
}
.panel-header {
text-align: center;
&:hover {
transition: background-color 0.1s ease-in-out;
background-color: $panel-header-hover-bg;
@@ -156,8 +163,8 @@ div.flot-text {
.fa {
position: relative;
top: -4px;
left: -6px;
top: -2px;
left: 6px;
font-size: 75%;
z-index: 1;
}
@@ -174,7 +181,7 @@ div.flot-text {
display: block;
@include panel-corner-color(lighten($panel-bg, 4%));
.fa {
left: -5px;
left: 4px;
}
.fa:before {
content: '\f08e';
+13 -3
View File
@@ -13,6 +13,13 @@ $login-border: #8daac5;
justify-content: center;
background-image: url(../img/heatmap_bg_test.svg);
background-size: cover;
color: #d8d9da;
& a {
color: #d8d9da !important;
}
& .btn-primary {
@include buttonBackground(#ff6600, #bc3e06);
}
}
input:-webkit-autofill,
@@ -25,8 +32,9 @@ textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0px 1000px $black inset;
-webkit-text-fill-color: $gray-7 !important;
-webkit-box-shadow: 0 0 0px 1000px $black inset !important;
-webkit-text-fill-color: #fbfbfb !important;
box-shadow: 0 0 0px 1000px $black inset;
}
.login-form-group {
@@ -46,6 +54,8 @@ select:-webkit-autofill:focus {
border: 1px solid $login-border;
border-radius: 4px;
opacity: 0.6;
background: $black;
color: #fbfbfb;
&:focus {
border: 1px solid $login-border;
@@ -103,7 +113,7 @@ select:-webkit-autofill:focus {
}
.icon-gf-grafana_wordmark {
color: $link-color;
color: darken($white, 11%);
position: relative;
font-size: 2rem;
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';
describe("SemVersion", () => {
@@ -8,10 +6,10 @@ describe("SemVersion", () => {
describe('parsing', () => {
it('should parse version properly', () => {
let semver = new SemVersion(version);
expect(semver.major).to.be(1);
expect(semver.minor).to.be(0);
expect(semver.patch).to.be(0);
expect(semver.meta).to.be('alpha.1');
expect(semver.major).toBe(1);
expect(semver.minor).toBe(0);
expect(semver.patch).toBe(0);
expect(semver.meta).toBe('alpha.1');
});
});
@@ -30,7 +28,7 @@ describe("SemVersion", () => {
{value: '3.5', expected: false},
];
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},
];
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 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() });
var global = <any>window;
global.$ = global.jQuery = $;
+1 -1
View File
@@ -23,7 +23,7 @@ module.exports = merge(common, {
},
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',
+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>.
//
// Use of this source code is governed by an MIT-style
@@ -7,10 +9,13 @@ package sqlite3
/*
#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_TRACE_SIZE_LIMIT=15
#cgo CFLAGS: -DSQLITE_OMIT_DEPRECATED
#cgo CFLAGS: -DSQLITE_DISABLE_INTRINSIC
#cgo CFLAGS: -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
#cgo CFLAGS: -Wno-deprecated-declarations
#ifndef USE_LIBSQLITE3
#include <sqlite3-binding.h>
@@ -170,6 +175,12 @@ var SQLiteTimestampFormats = []string{
"2006-01-02",
}
const (
columnDate string = "date"
columnDatetime string = "datetime"
columnTimestamp string = "timestamp"
)
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}
@@ -389,7 +400,7 @@ func (c *SQLiteConn) RegisterCommitHook(callback func() int) {
if callback == nil {
C.sqlite3_commit_hook(c.db, nil, nil)
} 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 {
C.sqlite3_rollback_hook(c.db, nil, nil)
} 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 {
C.sqlite3_update_hook(c.db, nil, nil)
} 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 {
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.
@@ -780,6 +791,8 @@ func errorString(err Error) string {
// Enable or disable enforcement of foreign keys. X can be 1 or 0.
// _recursive_triggers=X
// 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) {
if C.sqlite3_threadsafe() == 0 {
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
foreignKeys := -1
recursiveTriggers := -1
mutex := C.int(C.SQLITE_OPEN_FULLMUTEX)
pos := strings.IndexRune(dsn, '?')
if 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:") {
dsn = dsn[:pos]
}
@@ -865,9 +891,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
name := C.CString(dsn)
defer C.free(unsafe.Pointer(name))
rv := C._sqlite3_open_v2(name, &db,
C.SQLITE_OPEN_FULLMUTEX|
C.SQLITE_OPEN_READWRITE|
C.SQLITE_OPEN_CREATE,
mutex|C.SQLITE_OPEN_READWRITE|C.SQLITE_OPEN_CREATE,
nil)
if rv != 0 {
return nil, Error{Code: ErrNo(rv)}
@@ -1070,7 +1094,7 @@ func (s *SQLiteStmt) bind(args []namedValue) error {
case int64:
rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
case bool:
if bool(v) {
if v {
rv = C.sqlite3_bind_int(s.s, n, 1)
} else {
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{}),
}
go func(db *C.sqlite3) {
select {
case <-ctx.Done():
if ctxdone := ctx.Done(); ctxdone != nil {
go func(db *C.sqlite3) {
select {
case <-ctxdone:
select {
case <-rows.done:
default:
C.sqlite3_interrupt(db)
rows.Close()
}
case <-rows.done:
default:
C.sqlite3_interrupt(db)
rows.Close()
}
case <-rows.done:
}
}(s.c.db)
}(s.c.db)
}
return rows, nil
}
@@ -1166,19 +1192,21 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
return nil, err
}
done := make(chan struct{})
defer close(done)
go func(db *C.sqlite3) {
select {
case <-done:
case <-ctx.Done():
if ctxdone := ctx.Done(); ctxdone != nil {
done := make(chan struct{})
defer close(done)
go func(db *C.sqlite3) {
select {
case <-done:
default:
C.sqlite3_interrupt(db)
case <-ctxdone:
select {
case <-done:
default:
C.sqlite3_interrupt(db)
}
}
}
}(s.c.db)
}(s.c.db)
}
var rowid, changes C.longlong
rv := C._sqlite3_step(s.s, &rowid, &changes)
@@ -1272,7 +1300,7 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
case C.SQLITE_INTEGER:
val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
switch rc.decltype[i] {
case "timestamp", "datetime", "date":
case columnTimestamp, columnDatetime, columnDate:
var t time.Time
// Assume a millisecond unix timestamp if it's 13 digits -- too
// 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)))
switch dest[i].(type) {
case sql.RawBytes:
dest[i] = (*[1 << 30]byte)(unsafe.Pointer(p))[0:n]
dest[i] = (*[1 << 30]byte)(p)[0:n]
default:
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
}
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))
switch rc.decltype[i] {
case "timestamp", "datetime", "date":
case columnTimestamp, columnDatetime, columnDate:
var t time.Time
s = strings.TrimSuffix(s, "Z")
for _, format := range SQLiteTimestampFormats {
+2
View File
@@ -1,3 +1,5 @@
// +build cgo
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// 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.
// Values are same as the corresponding SQLite Trace Event Codes.
const (
TraceStmt = C.SQLITE_TRACE_STMT
TraceProfile = C.SQLITE_TRACE_PROFILE
TraceRow = C.SQLITE_TRACE_ROW
TraceClose = C.SQLITE_TRACE_CLOSE
TraceStmt = uint32(C.SQLITE_TRACE_STMT)
TraceProfile = uint32(C.SQLITE_TRACE_PROFILE)
TraceRow = uint32(C.SQLITE_TRACE_ROW)
TraceClose = uint32(C.SQLITE_TRACE_CLOSE)
)
type TraceInfo struct {
@@ -71,7 +71,7 @@ type TraceUserCallback func(TraceInfo) int
type TraceConfig struct {
Callback TraceUserCallback
EventMask C.uint
EventMask uint32
WantExpandedSQL bool
}
@@ -105,6 +105,8 @@ func traceCallbackTrampoline(
// Parameter named 'X' in SQLite docs (eXtra event data?):
xValue unsafe.Pointer) C.int {
eventCode := uint32(traceEventCode)
if ctx == nil {
panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode))
}
@@ -114,7 +116,7 @@ func traceCallbackTrampoline(
var traceConf TraceConfig
var found bool
if traceEventCode == TraceClose {
if eventCode == TraceClose {
// clean up traceMap: 'pop' means get and delete
traceConf, found = popTraceMapping(connHandle)
} else {
@@ -123,16 +125,16 @@ func traceCallbackTrampoline(
if !found {
panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)",
connHandle, traceEventCode))
connHandle, eventCode))
}
var info TraceInfo
info.EventCode = uint32(traceEventCode)
info.EventCode = eventCode
info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0)
info.ConnHandle = connHandle
switch traceEventCode {
switch eventCode {
case TraceStmt:
info.StmtHandle = uintptr(p)
@@ -183,7 +185,7 @@ func traceCallbackTrampoline(
// registering this callback trampoline with SQLite --- for cleanup.
// In the future there may be more events forced to "selected" in SQLite
// for the driver's needs.
if traceConf.EventMask&traceEventCode == 0 {
if traceConf.EventMask&eventCode == 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*));
void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
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_result_pointer sqlite3_api->result_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) */
#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
}