diff --git a/.circleci/config.yml b/.circleci/config.yml index 3dd8f800b94..dba6c5f8bd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ version: 2 jobs: mysql-integration-test: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 - image: circleci/mysql:5.6-ram environment: MYSQL_ROOT_PASSWORD: rootpass @@ -39,7 +39,7 @@ jobs: postgres-integration-test: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 - image: circleci/postgres:9.3-ram environment: POSTGRES_USER: grafanatest @@ -74,7 +74,7 @@ jobs: gometalinter: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 environment: # we need CGO because of go-sqlite3 CGO_ENABLED: 1 @@ -117,7 +117,7 @@ jobs: test-backend: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -175,7 +175,7 @@ jobs: build: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 92366849a44..671740f7225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### New Features * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster) +* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109) ### Minor @@ -13,12 +14,19 @@ * **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK) * **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483) * **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548) +* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard) +* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas) +* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik) +* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok) + +### Bug fixes +* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) # 5.4.2 (2018-12-13) * **Datasource admin**: Fix for issue creating new data source when same name exists [#14467](https://github.com/grafana/grafana/issues/14467) * **OAuth**: Fix for oauth auto login setting, can now be set using env variable [#14435](https://github.com/grafana/grafana/issues/14435) -* **Dashboard search**: Fix for searching tags in tags filter dropdown. +* **Dashboard search**: Fix for searching tags in tags filter dropdown. # 5.4.1 (2018-12-10) diff --git a/Dockerfile b/Dockerfile index 65260e1a6a8..c3af89b6092 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Golang build container -FROM golang:1.11 +FROM golang:1.11.4 WORKDIR $GOPATH/src/github.com/grafana/grafana diff --git a/README.md b/README.md index 03fe840d5f1..1ce4ffbe109 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Choose this option to build on platforms other than linux/amd64 and/or not have The resulting image will be tagged as `grafana/grafana:dev` -Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Perferences -> Advanced), otherwize you may faild at `grunt build` +Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Preferences -> Advanced), otherwize you may faild at `grunt build` ### Dev config diff --git a/appveyor.yml b/appveyor.yml index 4bbd3668e19..5f97784dd38 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "8" GOPATH: C:\gopath - GOVERSION: 1.11 + GOVERSION: 1.11.4 install: - rmdir c:\go /s /q diff --git a/conf/defaults.ini b/conf/defaults.ini index 2ef2ad7942a..7f61ac96870 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -103,6 +103,9 @@ server_cert_name = # For "sqlite3" only, path relative to data_path setting path = grafana.db +# For "sqlite3" only. cache mode setting used for connecting to the database +cache_mode = private + #################################### Session ############################# [session] # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" @@ -335,6 +338,7 @@ tls_skip_verify_insecure = false tls_client_cert = tls_client_key = tls_client_ca = +send_client_credentials_via_post = false #################################### Basic Auth ########################## [auth.basic] diff --git a/conf/sample.ini b/conf/sample.ini index ba65727dc4b..014016d45bc 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -99,6 +99,9 @@ # Set to true to log the sql calls and execution times. log_queries = +# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) +;cache_mode = private + #################################### Session #################################### [session] # Either "memory", "file", "redis", "mysql", "postgres", default is "file" @@ -284,6 +287,10 @@ log_queries = ;tls_client_key = ;tls_client_ca = +; Set to true to enable sending client_id and client_secret via POST body instead of Basic authentication HTTP header +; This might be required if the OAuth provider is not RFC6749 compliant, only supporting credentials passed via POST payload +;send_client_credentials_via_post = false + #################################### Grafana.com Auth #################### [auth.grafana_com] ;enabled = false diff --git a/devenv/docker/blocks/alert_webhook_listener/Dockerfile b/devenv/docker/blocks/alert_webhook_listener/Dockerfile new file mode 100644 index 00000000000..a6bb87f15f5 --- /dev/null +++ b/devenv/docker/blocks/alert_webhook_listener/Dockerfile @@ -0,0 +1,7 @@ + +FROM golang:latest +ADD main.go / +WORKDIR / +RUN go build -o main . +EXPOSE 3010 +ENTRYPOINT ["/main"] diff --git a/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml b/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml new file mode 100644 index 00000000000..3e6c5389f6c --- /dev/null +++ b/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml @@ -0,0 +1,5 @@ + alert_webhook_listener: + build: docker/blocks/alert_webhook_listener + network_mode: host + ports: + - "3010:3010" diff --git a/devenv/docker/blocks/alert_webhook_listener/main.go b/devenv/docker/blocks/alert_webhook_listener/main.go new file mode 100644 index 00000000000..355c903e9f6 --- /dev/null +++ b/devenv/docker/blocks/alert_webhook_listener/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" +) + +func hello(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return + } + + line := fmt.Sprintf("webbhook: -> %s", string(body)) + fmt.Println(line) + io.WriteString(w, line) +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3010", nil) +} diff --git a/docs/sources/auth/auth-proxy.md b/docs/sources/auth/auth-proxy.md index e066eed9190..2274771b91a 100644 --- a/docs/sources/auth/auth-proxy.md +++ b/docs/sources/auth/auth-proxy.md @@ -31,9 +31,10 @@ auto_sign_up = true ldap_sync_ttl = 60 # Limit where auth proxy requests come from by configuring a list of IP addresses. # This can be used to prevent users spoofing the X-WEBAUTH-USER header. +# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120` whitelist = # Optionally define more headers to sync other user attributes -# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`` +# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL` headers = ``` diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 6fa6531fc98..c3c44426ba7 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -17,7 +17,7 @@ can find examples using Okta, BitBucket, OneLogin and Azure. 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/generic_oauth`. -You may have to set the `root_url` option of `[server]` for the callback URL to be +You may have to set the `root_url` option of `[server]` for the callback URL to be correct. For example in case you are serving Grafana behind a proxy. Example config: @@ -209,6 +209,17 @@ allowed_organizations = token_url = https://.my.centrify.com/OAuth2/Token/ ``` +## Set up OAuth2 with non-compliant providers + +Some OAuth2 providers might not support `client_id` and `client_secret` passed via Basic Authentication HTTP header, which +results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be +send via POST body, which can be enabled via the following settings: + + ```bash + [auth.generic_oauth] + send_client_credentials_via_post = true + ``` +
diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 2d4be21bb78..a27fd2aac14 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -285,7 +285,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User permissions updated"} +{"message": "User permissions updated"} ``` ## Delete global User @@ -308,7 +308,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User deleted"} +{"message": "User deleted"} ``` ## Pause all alerts @@ -339,5 +339,5 @@ JSON Body schema: HTTP/1.1 200 Content-Type: application/json -{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100} +{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100} ``` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 30ef020a3de..0e5a55b3c0e 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -250,6 +250,12 @@ Sets the maximum amount of time a connection may be reused. The default is 14400 Set to `true` to log the sql calls and execution times. +### cache_mode + +For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared) +Defaults to private. + +
## [security] diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7ed44572533..d26af5277a1 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -34,32 +34,29 @@ sudo dpkg -i grafana__amd64.deb Example: ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb +wget https://dl.grafana.com/oss/release/grafana_5.4.2_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.4_amd64.deb +sudo dpkg -i grafana_5.4.2_amd64.deb ``` ## APT Repository -Add the following line to your `/etc/apt/sources.list` file. +Create a file `/etc/apt/sources.list.d/grafana.list` and add the following to it. ```bash -deb https://packagecloud.io/grafana/stable/debian/ stretch main +deb https://packages.grafana.com/oss/deb stable main ``` -Use the above line even if you are on Ubuntu or another Debian version. -There is also a testing repository if you want beta or release -candidates. +There is a separate repository if you want beta releases. ```bash -deb https://packagecloud.io/grafana/testing/debian/ stretch main +deb https://packages.grafana.com/oss/deb beta main ``` -Then add the [Package Cloud](https://packagecloud.io/grafana) key. This -allows you to install signed packages. +Use the above line even if you are on Ubuntu or another Debian version. Then add our gpg key. This allows you to install signed packages. ```bash -curl https://packagecloud.io/gpg.key | sudo apt-key add - +curl https://packages.grafana.com/gpg.key | sudo apt-key add - ``` Update your Apt repositories and install Grafana diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 5bf3b7ed745..156f703e164 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -32,7 +32,7 @@ $ sudo yum install Example: ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ sudo yum install https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` Or install manually using `rpm`. First execute @@ -44,7 +44,7 @@ $ wget Example: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ wget https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` ### On CentOS / Fedora / Redhat: @@ -67,19 +67,27 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo` ```bash [grafana] name=grafana -baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch +baseurl=https://packages.grafana.com/oss/rpm repo_gpgcheck=1 enabled=1 gpgcheck=1 -gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana +gpgkey=https://packages.grafana.com/gpg.key sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` -There is also a testing repository if you want beta or release candidates. +There is a separate repository if you want beta releases. ```bash -baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch +[grafana] +name=grafana +baseurl=https://packages.grafana.com/oss/rpm-beta +repo_gpgcheck=1 +enabled=1 +gpgcheck=1 +gpgkey=https://packages.grafana.com/gpg.key +sslverify=1 +sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` Then install Grafana via the `yum` command. @@ -91,7 +99,7 @@ $ sudo yum install grafana ### RPM GPG Key The RPMs are signed, you can verify the signature with this [public GPG -key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana). +key](https://packages.grafana.com/gpg.key). ## Package details diff --git a/jest.config.js b/jest.config.js index cac634fbf10..c5c6bcb9f5f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,9 @@ module.exports = { }, "moduleDirectories": ["node_modules", "public"], "roots": [ - "/public" + "/public/app", + "/public/test", + "/packages" ], "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$", "moduleFileExtensions": [ diff --git a/package.json b/package.json index d374cd524b4..eefe2cbbe53 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "private": true, "author": { "name": "Torkel Ödegaard", "company": "Grafana Labs" @@ -11,14 +12,16 @@ }, "devDependencies": { "@babel/core": "^7.1.2", - "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1", "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/preset-env": "^7.1.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.1.0", + "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1", + "@types/classnames": "^2.2.6", "@types/d3": "^4.10.1", "@types/enzyme": "^3.1.13", "@types/jest": "^23.3.2", + "@types/jquery": "^1.10.35", "@types/node": "^8.0.31", "@types/react": "^16.7.6", "@types/react-custom-scrollbars": "^4.0.5", @@ -49,15 +52,12 @@ "grunt-cli": "~1.2.0", "grunt-contrib-clean": "~1.0.0", "grunt-contrib-compress": "^1.3.0", - "grunt-contrib-concat": "^1.0.1", "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-cssmin": "~1.0.2", "grunt-exec": "^1.0.1", "grunt-newer": "^1.3.0", "grunt-notify": "^0.4.5", "grunt-postcss": "^0.8.0", - "grunt-sass": "^2.0.0", - "grunt-sass-lint": "^0.2.2", + "grunt-sass-lint": "^0.2.4", "grunt-usemin": "3.1.1", "grunt-webpack": "^3.0.2", "html-loader": "^0.5.1", @@ -73,6 +73,7 @@ "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", "npm": "^5.4.2", + "node-sass": "^4.11.0", "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", @@ -92,6 +93,7 @@ "tslib": "^1.9.3", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", + "tslint-react": "^3.6.0", "typescript": "^3.0.3", "uglifyjs-webpack-plugin": "^1.2.7", "webpack": "4.19.1", @@ -108,15 +110,30 @@ "watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js", "build": "grunt build", "test": "grunt test", - "lint": "tslint -c tslint.json --project tsconfig.json", + "tslint": "tslint -c tslint.json --project tsconfig.json", + "typecheck": "tsc --noEmit", "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", - "precommit": "lint-staged && grunt precommit" + "precommit": "grunt precommit" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged && grunt precommit" + } }, "lint-staged": { - "*.{ts,tsx}": ["prettier --write", "git add"], - "*.scss": ["prettier --write", "git add"], - "*pkg/**/*.go": ["gofmt -w -s", "git add"] + "*.{ts,tsx}": [ + "prettier --write", + "git add" + ], + "*.scss": [ + "prettier --write", + "git add" + ], + "*pkg/**/*.go": [ + "gofmt -w -s", + "git add" + ] }, "prettier": { "trailingComma": "es5", @@ -126,6 +143,7 @@ "license": "Apache-2.0", "dependencies": { "@babel/polyfill": "^7.0.0", + "@torkelo/react-select": "2.1.1", "angular": "1.6.6", "angular-bindonce": "0.3.1", "angular-native-dragdrop": "1.2.2", @@ -133,7 +151,7 @@ "angular-sanitize": "1.6.6", "baron": "^3.0.3", "brace": "^0.10.0", - "classnames": "^2.2.5", + "classnames": "^2.2.6", "clipboard": "^1.7.1", "d3": "^4.11.0", "d3-scale-chromatic": "^1.3.0", @@ -152,10 +170,9 @@ "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-grid-layout": "0.16.6", - "react-popper": "^1.3.0", "react-highlight-words": "0.11.0", + "react-popper": "^1.3.0", "react-redux": "^5.0.7", - "@torkelo/react-select": "2.1.1", "react-sizeme": "^2.3.6", "react-table": "^6.8.6", "react-transition-group": "^2.2.1", @@ -165,18 +182,26 @@ "redux-thunk": "^2.3.0", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", - "rxjs": "^5.4.3", + "rxjs": "^6.3.3", "slate": "^0.33.4", "slate-plain-serializer": "^0.5.10", "slate-prism": "^0.5.0", "slate-react": "^0.12.4", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop/tarball/master", - "tinycolor2": "^1.4.1", - "tslint-react": "^3.6.0" + "tinycolor2": "^1.4.1" }, "resolutions": { "caniuse-db": "1.0.30000772", "**/@types/react": "16.7.6" + }, + "workspaces": { + "packages": [ + "packages/*" + ], + "nohoist": [ + "**/@types/*", + "**/@types/*/**" + ] } } diff --git a/packages/grafana-build/README.md b/packages/grafana-build/README.md new file mode 100644 index 00000000000..588d91861d3 --- /dev/null +++ b/packages/grafana-build/README.md @@ -0,0 +1,4 @@ +# Shared build scripts + +Shared build scripts for plugins & internal packages. + diff --git a/packages/grafana-build/package.json b/packages/grafana-build/package.json new file mode 100644 index 00000000000..24fb648c8d4 --- /dev/null +++ b/packages/grafana-build/package.json @@ -0,0 +1,13 @@ +{ + "name": "@grafana/build", + "private": true, + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "tslint": "echo \"Nothing to do\"", + "typecheck": "echo \"Nothing to do\"" + }, + "author": "", + "license": "ISC" +} diff --git a/packages/grafana-ui/README.md b/packages/grafana-ui/README.md new file mode 100644 index 00000000000..1413965f7da --- /dev/null +++ b/packages/grafana-ui/README.md @@ -0,0 +1,3 @@ +# Grafana (WIP) shared component library + +Used by internal & external plugins. diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json new file mode 100644 index 00000000000..2fb210e3b46 --- /dev/null +++ b/packages/grafana-ui/package.json @@ -0,0 +1,33 @@ +{ + "name": "@grafana/ui", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "tslint": "tslint -c tslint.json --project tsconfig.json", + "typecheck": "tsc --noEmit" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@torkelo/react-select": "2.1.1", + "classnames": "^2.2.5", + "jquery": "^3.2.1", + "lodash": "^4.17.10", + "moment": "^2.22.2", + "react": "^16.6.3", + "react-dom": "^16.6.3", + "react-highlight-words": "0.11.0", + "react-popper": "^1.3.0", + "react-transition-group": "^2.2.1", + "react-virtualized": "^9.21.0" + }, + "devDependencies": { + "@types/jest": "^23.3.2", + "@types/lodash": "^4.14.119", + "@types/react": "^16.7.6", + "@types/classnames": "^2.2.6", + "@types/jquery": "^1.10.35", + "typescript": "^3.2.2" + } +} diff --git a/public/app/core/components/DeleteButton/DeleteButton.test.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx similarity index 85% rename from public/app/core/components/DeleteButton/DeleteButton.test.tsx rename to packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx index 12acadee18a..f6d5a676971 100644 --- a/public/app/core/components/DeleteButton/DeleteButton.test.tsx +++ b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import DeleteButton from './DeleteButton'; +import { DeleteButton } from './DeleteButton'; import { shallow } from 'enzyme'; describe('DeleteButton', () => { - let wrapper; - let deleted; + let wrapper: any; + let deleted: any; beforeAll(() => { deleted = false; @@ -12,7 +12,8 @@ describe('DeleteButton', () => { function deleteItem() { deleted = true; } - wrapper = shallow( deleteItem()} />); + + wrapper = shallow( deleteItem()} />); }); it('should show confirm delete when clicked', () => { diff --git a/public/app/core/components/DeleteButton/DeleteButton.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx similarity index 74% rename from public/app/core/components/DeleteButton/DeleteButton.tsx rename to packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx index a83ce6097ad..df65d156ab3 100644 --- a/public/app/core/components/DeleteButton/DeleteButton.tsx +++ b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx @@ -1,19 +1,19 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, SyntheticEvent } from 'react'; -export interface DeleteButtonProps { - onConfirmDelete(); +interface Props { + onConfirm(): void; } -export interface DeleteButtonStates { +interface State { showConfirm: boolean; } -export default class DeleteButton extends PureComponent { - state: DeleteButtonStates = { +export class DeleteButton extends PureComponent { + state: State = { showConfirm: false, }; - onClickDelete = event => { + onClickDelete = (event: SyntheticEvent) => { if (event) { event.preventDefault(); } @@ -23,7 +23,7 @@ export default class DeleteButton extends PureComponent { + onClickCancel = (event: SyntheticEvent) => { if (event) { event.preventDefault(); } @@ -33,7 +33,7 @@ export default class DeleteButton extends PureComponent Cancel - + Confirm Delete diff --git a/public/sass/components/_delete_button.scss b/packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss similarity index 100% rename from public/sass/components/_delete_button.scss rename to packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss new file mode 100644 index 00000000000..d52508c946c --- /dev/null +++ b/packages/grafana-ui/src/components/index.scss @@ -0,0 +1 @@ +@import 'DeleteButton/DeleteButton'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts new file mode 100644 index 00000000000..b57b9bcfdb7 --- /dev/null +++ b/packages/grafana-ui/src/components/index.ts @@ -0,0 +1 @@ +export { DeleteButton } from './DeleteButton/DeleteButton'; diff --git a/packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx new file mode 100644 index 00000000000..8b80de64696 --- /dev/null +++ b/packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx @@ -0,0 +1,23 @@ +import React, { SFC, ReactNode } from 'react'; +import classNames from 'classnames'; + +interface Props { + children: ReactNode; + htmlFor?: string; + className?: string; + isFocused?: boolean; + isInvalid?: boolean; +} + +export const GfFormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => { + const classes = classNames('gf-form-label', className, { + 'gf-form-label--is-focused': isFocused, + 'gf-form-label--is-invalid': isInvalid, + }); + + return ( + + ); +}; diff --git a/packages/grafana-ui/src/forms/index.ts b/packages/grafana-ui/src/forms/index.ts new file mode 100644 index 00000000000..bb6998b0025 --- /dev/null +++ b/packages/grafana-ui/src/forms/index.ts @@ -0,0 +1 @@ +export { GfFormLabel } from './GfFormLabel/GfFormLabel'; diff --git a/packages/grafana-ui/src/index.scss b/packages/grafana-ui/src/index.scss new file mode 100644 index 00000000000..841415620d6 --- /dev/null +++ b/packages/grafana-ui/src/index.scss @@ -0,0 +1 @@ +@import 'components/index'; diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts new file mode 100644 index 00000000000..b22152497b9 --- /dev/null +++ b/packages/grafana-ui/src/index.ts @@ -0,0 +1,5 @@ +export * from './components'; +export * from './visualizations'; +export * from './types'; +export * from './utils'; +export * from './forms'; diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts new file mode 100644 index 00000000000..f618ce6db34 --- /dev/null +++ b/packages/grafana-ui/src/types/index.ts @@ -0,0 +1,3 @@ +export * from './series'; +export * from './time'; +export * from './panel'; diff --git a/packages/grafana-ui/src/types/jquery.d.ts b/packages/grafana-ui/src/types/jquery.d.ts new file mode 100644 index 00000000000..4a6f60b6029 --- /dev/null +++ b/packages/grafana-ui/src/types/jquery.d.ts @@ -0,0 +1,17 @@ +interface JQueryPlot { + (element: HTMLElement | JQuery, data: any, options: any): void; + plugins: any[]; +} + +interface JQueryStatic { + plot: JQueryPlot; +} + +interface JQuery { + place_tt: any; + modal: any; + tagsinput: any; + typeahead: any; + accessKey: any; + tooltip: any; +} diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts new file mode 100644 index 00000000000..44336555a81 --- /dev/null +++ b/packages/grafana-ui/src/types/panel.ts @@ -0,0 +1,31 @@ +import { TimeSeries, LoadingState } from './series'; +import { TimeRange } from './time'; + +export interface PanelProps { + timeSeries: TimeSeries[]; + timeRange: TimeRange; + loading: LoadingState; + options: T; + renderCounter: number; + width: number; + height: number; +} + +export interface PanelOptionsProps { + options: T; + onChange: (options: T) => void; +} + +export interface PanelSize { + width: number; + height: number; +} + +export interface PanelMenuItem { + type?: 'submenu' | 'divider'; + text?: string; + iconClassName?: string; + onClick?: () => void; + shortcut?: string; + subMenu?: PanelMenuItem[]; +} diff --git a/packages/grafana-ui/src/types/series.ts b/packages/grafana-ui/src/types/series.ts new file mode 100644 index 00000000000..49662e9872d --- /dev/null +++ b/packages/grafana-ui/src/types/series.ts @@ -0,0 +1,53 @@ +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + Error = 'Error', +} + +export type TimeSeriesValue = number | null; + +export type TimeSeriesPoints = TimeSeriesValue[][]; + +export interface TimeSeries { + target: string; + datapoints: TimeSeriesPoints; + unit?: string; +} + +/** View model projection of a time series */ +export interface TimeSeriesVM { + label: string; + color: string; + data: TimeSeriesValue[][]; + stats: TimeSeriesStats; +} + +export interface TimeSeriesStats { + total: number | null; + max: number | null; + min: number | null; + logmin: number; + avg: number | null; + current: number | null; + first: number | null; + delta: number; + diff: number | null; + range: number | null; + timeStep: number; + count: number; + allIsNull: boolean; + allIsZero: boolean; +} + +export enum NullValueMode { + Null = 'null', + Ignore = 'connected', + AsZero = 'null as zero', +} + +/** View model projection of many time series */ +export interface TimeSeriesVMs { + [index: number]: TimeSeriesVM; + length: number; +} diff --git a/packages/grafana-ui/src/types/time.ts b/packages/grafana-ui/src/types/time.ts new file mode 100644 index 00000000000..b6acf7f07b6 --- /dev/null +++ b/packages/grafana-ui/src/types/time.ts @@ -0,0 +1,17 @@ +import { Moment } from 'moment'; + +export interface RawTimeRange { + from: Moment | string; + to: Moment | string; +} + +export interface TimeRange { + from: Moment; + to: Moment; + raw: RawTimeRange; +} + +export interface IntervalValues { + interval: string; // 10s,5m + intervalMs: number; +} diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts new file mode 100644 index 00000000000..4d9b9a4b948 --- /dev/null +++ b/packages/grafana-ui/src/utils/index.ts @@ -0,0 +1 @@ +export * from './processTimeSeries'; diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts new file mode 100644 index 00000000000..e92aaf0c1a6 --- /dev/null +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -0,0 +1,174 @@ +// Libraries +import _ from 'lodash'; + +// Types +import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; + +interface Options { + timeSeries: TimeSeries[]; + nullValueMode: NullValueMode; + colorPalette: string[]; +} + +export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs { + const vmSeries = timeSeries.map((item, index) => { + const colorIndex = index % colorPalette.length; + const label = item.target; + const result = []; + + // stat defaults + let total = 0; + let max: TimeSeriesValue = -Number.MAX_VALUE; + let min: TimeSeriesValue = Number.MAX_VALUE; + let logmin = Number.MAX_VALUE; + let avg: TimeSeriesValue = null; + let current: TimeSeriesValue = null; + let first: TimeSeriesValue = null; + let delta: TimeSeriesValue = 0; + let diff: TimeSeriesValue = null; + let range: TimeSeriesValue = null; + let timeStep = Number.MAX_VALUE; + let allIsNull = true; + let allIsZero = true; + + const ignoreNulls = nullValueMode === NullValueMode.Ignore; + const nullAsZero = nullValueMode === NullValueMode.AsZero; + + let currentTime: TimeSeriesValue = null; + let currentValue: TimeSeriesValue = null; + let nonNulls = 0; + let previousTime: TimeSeriesValue = null; + let previousValue = 0; + let previousDeltaUp = true; + + for (let i = 0; i < item.datapoints.length; i++) { + currentValue = item.datapoints[i][0]; + currentTime = item.datapoints[i][1]; + + if (typeof currentTime !== 'number') { + continue; + } + + if (typeof currentValue !== 'number') { + continue; + } + + // Due to missing values we could have different timeStep all along the series + // so we have to find the minimum one (could occur with aggregators such as ZimSum) + if (previousTime !== null && currentTime !== null) { + const currentStep = currentTime - previousTime; + if (currentStep < timeStep) { + timeStep = currentStep; + } + } + + previousTime = currentTime; + + if (currentValue === null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + if (currentValue !== null) { + if (_.isNumber(currentValue)) { + total += currentValue; + allIsNull = false; + nonNulls++; + } + + if (currentValue > max) { + max = currentValue; + } + + if (currentValue < min) { + min = currentValue; + } + + if (first === null) { + first = currentValue; + } else { + if (previousValue > currentValue) { + // counter reset + previousDeltaUp = false; + if (i === item.datapoints.length - 1) { + // reset on last + delta += currentValue; + } + } else { + if (previousDeltaUp) { + delta += currentValue - previousValue; // normal increment + } else { + delta += currentValue; // account for counter reset + } + previousDeltaUp = true; + } + } + previousValue = currentValue; + + if (currentValue < logmin && currentValue > 0) { + logmin = currentValue; + } + + if (currentValue !== 0) { + allIsZero = false; + } + } + + result.push([currentTime, currentValue]); + } + + if (max === -Number.MAX_VALUE) { + max = null; + } + + if (min === Number.MAX_VALUE) { + min = null; + } + + if (result.length && !allIsNull) { + avg = total / nonNulls; + current = result[result.length - 1][1]; + if (current === null && result.length > 1) { + current = result[result.length - 2][1]; + } + } + + if (max !== null && min !== null) { + range = max - min; + } + + if (current !== null && first !== null) { + diff = current - first; + } + + const count = result.length; + + return { + data: result, + label: label, + color: colorPalette[colorIndex], + stats: { + total, + min, + max, + current, + logmin, + avg, + diff, + delta, + timeStep, + range, + count, + first, + allIsZero, + allIsNull, + }, + }; + }); + + return vmSeries; +} diff --git a/public/app/viz/Graph.tsx b/packages/grafana-ui/src/visualizations/Graph/Graph.tsx similarity index 89% rename from public/app/viz/Graph.tsx rename to packages/grafana-ui/src/visualizations/Graph/Graph.tsx index bdababb3e50..ad038cebcda 100644 --- a/public/app/viz/Graph.tsx +++ b/packages/grafana-ui/src/visualizations/Graph/Graph.tsx @@ -1,11 +1,9 @@ // Libraries import $ from 'jquery'; import React, { PureComponent } from 'react'; -import 'vendor/flot/jquery.flot'; -import 'vendor/flot/jquery.flot.time'; // Types -import { TimeRange, TimeSeriesVMs } from 'app/types'; +import { TimeRange, TimeSeriesVMs } from '../../types'; interface GraphProps { timeSeries: TimeSeriesVMs; @@ -24,7 +22,7 @@ export class Graph extends PureComponent { showBars: false, }; - element: HTMLElement; + element: HTMLElement | null; componentDidUpdate() { this.draw(); @@ -35,6 +33,10 @@ export class Graph extends PureComponent { } draw() { + if (this.element === null) { + return; + } + const { width, timeSeries, timeRange, showLines, showBars, showPoints } = this.props; if (!width) { @@ -76,7 +78,7 @@ export class Graph extends PureComponent { max: max, label: 'Datetime', ticks: ticks, - timeformat: time_format(ticks, min, max), + timeformat: timeFormat(ticks, min, max), }, grid: { minBorderMargin: 0, @@ -96,6 +98,7 @@ export class Graph extends PureComponent { $.plot(this.element, timeSeries, flotOptions); } catch (err) { console.log('Graph rendering error', err, flotOptions, timeSeries); + throw new Error('Error rendering panel'); } } @@ -109,7 +112,7 @@ export class Graph extends PureComponent { } // Copied from graph.ts -function time_format(ticks, min, max) { +function timeFormat(ticks: number, min: number, max: number): string { if (min && max && ticks) { const range = max - min; const secPerTick = range / ticks / 1000; diff --git a/packages/grafana-ui/src/visualizations/index.ts b/packages/grafana-ui/src/visualizations/index.ts new file mode 100644 index 00000000000..967432d37c9 --- /dev/null +++ b/packages/grafana-ui/src/visualizations/index.ts @@ -0,0 +1 @@ +export { Graph } from './Graph/Graph'; diff --git a/packages/grafana-ui/tsconfig.json b/packages/grafana-ui/tsconfig.json new file mode 100644 index 00000000000..ed6009f1ebd --- /dev/null +++ b/packages/grafana-ui/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "dist" + ], + "compilerOptions": { + "rootDir": ".", + "module": "esnext", + "outDir": "dist", + "declaration": true, + "noImplicitAny": true, + "strictNullChecks": true + } +} diff --git a/packages/grafana-ui/tslint.json b/packages/grafana-ui/tslint.json new file mode 100644 index 00000000000..0946f20963a --- /dev/null +++ b/packages/grafana-ui/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tslint.json" +} diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index e4e9c9d040f..ca7e78a58cd 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -1,10 +1,15 @@ package api import ( + "bytes" + "encoding/json" + "fmt" + "net/http" "time" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" @@ -12,6 +17,11 @@ import ( "github.com/grafana/grafana/pkg/util" ) +var client = &http.Client{ + Timeout: time.Second * 5, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, +} + func GetSharingOptions(c *m.ReqContext) { c.JSON(200, util.DynMap{ "externalSnapshotURL": setting.ExternalSnapshotUrl, @@ -20,26 +30,79 @@ func GetSharingOptions(c *m.ReqContext) { }) } +type CreateExternalSnapshotResponse struct { + Key string `json:"key"` + DeleteKey string `json:"deleteKey"` + Url string `json:"url"` + DeleteUrl string `json:"deleteUrl"` +} + +func createExternalDashboardSnapshot(cmd m.CreateDashboardSnapshotCommand) (*CreateExternalSnapshotResponse, error) { + var createSnapshotResponse CreateExternalSnapshotResponse + message := map[string]interface{}{ + "name": cmd.Name, + "expires": cmd.Expires, + "dashboard": cmd.Dashboard, + } + + messageBytes, err := simplejson.NewFromAny(message).Encode() + if err != nil { + return nil, err + } + + response, err := client.Post(setting.ExternalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes)) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != 200 { + return nil, fmt.Errorf("Create external snapshot response status code %d", response.StatusCode) + } + + if err := json.NewDecoder(response.Body).Decode(&createSnapshotResponse); err != nil { + return nil, err + } + + return &createSnapshotResponse, nil +} + +// POST /api/snapshots func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotCommand) { if cmd.Name == "" { cmd.Name = "Unnamed snapshot" } + var url string + cmd.ExternalUrl = "" + cmd.OrgId = c.OrgId + cmd.UserId = c.UserId + if cmd.External { - // external snapshot ref requires key and delete key - if cmd.Key == "" || cmd.DeleteKey == "" { - c.JsonApiErr(400, "Missing key and delete key for external snapshot", nil) + if !setting.ExternalEnabled { + c.JsonApiErr(403, "External dashboard creation is disabled", nil) return } - cmd.OrgId = -1 - cmd.UserId = -1 + response, err := createExternalDashboardSnapshot(cmd) + if err != nil { + c.JsonApiErr(500, "Failed to create external snaphost", err) + return + } + + url = response.Url + cmd.Key = response.Key + cmd.DeleteKey = response.DeleteKey + cmd.ExternalUrl = response.Url + cmd.ExternalDeleteUrl = response.DeleteUrl + cmd.Dashboard = simplejson.New() + metrics.M_Api_Dashboard_Snapshot_External.Inc() } else { cmd.Key = util.GetRandomString(32) cmd.DeleteKey = util.GetRandomString(32) - cmd.OrgId = c.OrgId - cmd.UserId = c.UserId + url = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key) + metrics.M_Api_Dashboard_Snapshot_Create.Inc() } @@ -51,7 +114,7 @@ func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotComma c.JSON(200, util.DynMap{ "key": cmd.Key, "deleteKey": cmd.DeleteKey, - "url": setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key), + "url": url, "deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey), }) } @@ -91,6 +154,33 @@ func GetDashboardSnapshot(c *m.ReqContext) { c.JSON(200, dto) } +func deleteExternalDashboardSnapshot(externalUrl string) error { + response, err := client.Get(externalUrl) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode == 200 { + return nil + } + + // Gracefully ignore "snapshot not found" errors as they could have already + // been removed either via the cleanup script or by request. + if response.StatusCode == 500 { + var respJson map[string]interface{} + if err := json.NewDecoder(response.Body).Decode(&respJson); err != nil { + return err + } + + if respJson["message"] == "Failed to get dashboard snapshot" { + return nil + } + } + + return fmt.Errorf("Unexpected response when deleting external snapshot. Status code: %d", response.StatusCode) +} + // GET /api/snapshots-delete/:deleteKey func DeleteDashboardSnapshotByDeleteKey(c *m.ReqContext) Response { key := c.Params(":deleteKey") @@ -102,6 +192,13 @@ func DeleteDashboardSnapshotByDeleteKey(c *m.ReqContext) Response { return Error(500, "Failed to get dashboard snapshot", err) } + if query.Result.External { + err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl) + if err != nil { + return Error(500, "Failed to delete external dashboard", err) + } + } + cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey} if err := bus.Dispatch(cmd); err != nil { @@ -138,6 +235,13 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response { return Error(403, "Access denied to this snapshot", nil) } + if query.Result.External { + err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl) + if err != nil { + return Error(500, "Failed to delete external dashboard", err) + } + } + cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey} if err := bus.Dispatch(cmd); err != nil { diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index e58f2c4712d..a24d0f38d85 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -1,6 +1,9 @@ package api import ( + "fmt" + "net/http" + "net/http/httptest" "testing" "time" @@ -13,13 +16,17 @@ import ( func TestDashboardSnapshotApiEndpoint(t *testing.T) { Convey("Given a single snapshot", t, func() { + var externalRequest *http.Request jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`)) mockSnapshotResult := &m.DashboardSnapshot{ Id: 1, + Key: "12345", + DeleteKey: "54321", Dashboard: jsonModel, Expires: time.Now().Add(time.Duration(1000) * time.Second), UserId: 999999, + External: true, } bus.AddHandler("test", func(query *m.GetDashboardSnapshotQuery) error { @@ -45,13 +52,25 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { return nil }) + setupRemoteServer := func(fn func(http.ResponseWriter, *http.Request)) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fn(rw, r) + })) + } + Convey("When user has editor role and is not in the ACL", func() { Convey("Should not be able to delete snapshot", func() { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + externalRequest = req + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL sc.handlerFunc = DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() So(sc.resp.Code, ShouldEqual, 403) + So(externalRequest, ShouldBeNil) }) }) }) @@ -59,6 +78,12 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { Convey("When user is anonymous", func() { Convey("Should be able to delete snapshot by deleteKey", func() { anonymousUserScenario("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(200) + externalRequest = req + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL sc.handlerFunc = DeleteDashboardSnapshotByDeleteKey sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec() @@ -67,6 +92,10 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { So(err, ShouldBeNil) So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") + + So(externalRequest.Method, ShouldEqual, http.MethodGet) + So(fmt.Sprintf("http://%s", externalRequest.Host), ShouldEqual, ts.URL) + So(externalRequest.URL.EscapedPath(), ShouldEqual, "/") }) }) }) @@ -79,6 +108,12 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { Convey("Should be able to delete a snapshot", func() { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(200) + externalRequest = req + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL sc.handlerFunc = DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -87,6 +122,8 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { So(err, ShouldBeNil) So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") + So(fmt.Sprintf("http://%s", externalRequest.Host), ShouldEqual, ts.URL) + So(externalRequest.URL.EscapedPath(), ShouldEqual, "/") }) }) }) @@ -94,6 +131,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { Convey("When user is editor and is the creator of the snapshot", func() { aclMockResp = []*m.DashboardAclInfoDTO{} mockSnapshotResult.UserId = TestUserID + mockSnapshotResult.External = false Convey("Should be able to delete a snapshot", func() { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { @@ -108,5 +146,54 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { }) }) }) + + Convey("When deleting an external snapshot", func() { + aclMockResp = []*m.DashboardAclInfoDTO{} + mockSnapshotResult.UserId = TestUserID + + Convey("Should gracefully delete local snapshot when remote snapshot has already been removed", func() { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(`{"message":"Failed to get dashboard snapshot"}`)) + rw.WriteHeader(500) + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("Should fail to delete local snapshot when an unexpected 500 error occurs", func() { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(500) + rw.Write([]byte(`{"message":"Unexpected"}`)) + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + So(sc.resp.Code, ShouldEqual, 500) + }) + }) + + Convey("Should fail to delete local snapshot when an unexpected remote error occurs", func() { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(404) + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + So(sc.resp.Code, ShouldEqual, 500) + }) + }) + }) }) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 455420e4688..0d54f0707a6 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -164,6 +164,14 @@ func GetPluginMarkdown(c *m.ReqContext) Response { return Error(500, "Could not get markdown file", err) } + // fallback try readme + if len(content) == 0 { + content, err = plugins.GetPluginMarkdown(pluginID, "readme") + if err != nil { + return Error(501, "Could not get markdown file", err) + } + } + resp := Respond(200, content) resp.Header("Content-Type", "text/plain; charset=utf-8") return resp diff --git a/pkg/components/dashdiffs/formatter_json.go b/pkg/components/dashdiffs/formatter_json.go index 488a345d492..09dac1ae5ac 100644 --- a/pkg/components/dashdiffs/formatter_json.go +++ b/pkg/components/dashdiffs/formatter_json.go @@ -206,10 +206,9 @@ func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []di // Added for _, delta := range deltas { - switch delta.(type) { + switch delta := delta.(type) { case *diff.Added: - d := delta.(*diff.Added) - f.printRecursive(d.Position.String(), d.Value, ChangeAdded) + f.printRecursive(delta.Position.String(), delta.Value, ChangeAdded) } } @@ -222,9 +221,8 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi if len(matchedDeltas) > 0 { for _, matchedDelta := range matchedDeltas { - switch matchedDelta.(type) { + switch matchedDelta := matchedDelta.(type) { case *diff.Object: - d := matchedDelta.(*diff.Object) switch value.(type) { case map[string]interface{}: //ok @@ -238,7 +236,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("{") f.closeLine() f.push(positionStr, len(o), false) - f.processObject(o, d.Deltas) + f.processObject(o, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("}") @@ -246,7 +244,6 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Array: - d := matchedDelta.(*diff.Array) switch value.(type) { case []interface{}: //ok @@ -260,7 +257,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("[") f.closeLine() f.push(positionStr, len(a), true) - f.processArray(a, d.Deltas) + f.processArray(a, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("]") @@ -268,27 +265,23 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Added: - d := matchedDelta.(*diff.Added) - f.printRecursive(positionStr, d.Value, ChangeAdded) + f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded) f.size[len(f.size)-1]++ case *diff.Modified: - d := matchedDelta.(*diff.Modified) savedSize := f.size[len(f.size)-1] - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.TextDiff: savedSize := f.size[len(f.size)-1] - d := matchedDelta.(*diff.TextDiff) - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.Deleted: - d := matchedDelta.(*diff.Deleted) - f.printRecursive(positionStr, d.Value, ChangeDeleted) + f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted) default: return errors.New("Unknown Delta type detected") @@ -305,13 +298,13 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) { results = make([]diff.Delta, 0) for _, delta := range deltas { - switch delta.(type) { + switch typedDelta := delta.(type) { case diff.PostDelta: - if delta.(diff.PostDelta).PostPosition() == position { + if typedDelta.PostPosition() == position { results = append(results, delta) } case diff.PreDelta: - if delta.(diff.PreDelta).PrePosition() == position { + if typedDelta.PrePosition() == position { results = append(results, delta) } default: @@ -417,20 +410,19 @@ func (f *JSONFormatter) print(a string) { } func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) { - switch value.(type) { + switch value := value.(type) { case map[string]interface{}: f.newLine(change) f.printKey(name) f.print("{") f.closeLine() - m := value.(map[string]interface{}) - size := len(m) + size := len(value) f.push(name, size, false) - keys := sortKeys(m) + keys := sortKeys(value) for _, key := range keys { - f.printRecursive(key, m[key], change) + f.printRecursive(key, value[key], change) } f.pop() @@ -445,10 +437,9 @@ func (f *JSONFormatter) printRecursive(name string, value interface{}, change Ch f.print("[") f.closeLine() - s := value.([]interface{}) - size := len(s) + size := len(value) f.push("", size, true) - for _, item := range s { + for _, item := range value { f.printRecursive("", item, change) } f.pop() diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 218f7321e65..402160ef5e3 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -292,6 +292,8 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1), } + a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq)) + searchResult, err = a.conn.Search(&searchReq) if err != nil { return nil, err diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 29bd305b336..fc109ac707f 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -198,17 +198,31 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error } proxies := strings.Split(setting.AuthProxyWhitelist, ",") - sourceIP, _, err := net.SplitHostPort(remoteAddr) - if err != nil { - return err + var proxyObjs []*net.IPNet + for _, proxy := range proxies { + proxyObjs = append(proxyObjs, coerceProxyAddress(proxy)) } - // Compare allowed IP addresses to actual address - for _, proxyIP := range proxies { - if sourceIP == strings.TrimSpace(proxyIP) { + sourceIP, _, _ := net.SplitHostPort(remoteAddr) + sourceObj := net.ParseIP(sourceIP) + + for _, proxyObj := range proxyObjs { + if proxyObj.Contains(sourceObj) { return nil } } - return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP) } + +func coerceProxyAddress(proxyAddr string) *net.IPNet { + proxyAddr = strings.TrimSpace(proxyAddr) + if !strings.Contains(proxyAddr, "/") { + proxyAddr = strings.Join([]string{proxyAddr, "32"}, "/") + } + + _, network, err := net.ParseCIDR(proxyAddr) + if err != nil { + fmt.Println(err) + } + return network +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index e9a3c8059f8..b9a8afce6c6 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -271,6 +271,23 @@ func TestMiddlewareContext(t *testing.T) { }) }) + middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is not within trusted CIDR block", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.req.RemoteAddr = "192.168.3.1:12345" + sc.exec() + + Convey("should return 407 status code", func() { + So(sc.resp.Code, ShouldEqual, 407) + So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.3.1 is not from the authentication proxy") + }) + }) + middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not trusted", func(sc *scenarioContext) { setting.AuthProxyEnabled = true setting.AuthProxyHeaderName = "X-WEBAUTH-USER" @@ -288,6 +305,23 @@ func TestMiddlewareContext(t *testing.T) { }) }) + middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not within trusted CIDR block", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.req.RemoteAddr = "[2001:23]:12345" + sc.exec() + + Convey("should return 407 status code", func() { + So(sc.resp.Code, ShouldEqual, 407) + So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 2001:23 is not from the authentication proxy") + }) + }) + middlewareScenario("When auth_proxy is enabled and request RemoteAddr is trusted", func(sc *scenarioContext) { setting.AuthProxyEnabled = true setting.AuthProxyHeaderName = "X-WEBAUTH-USER" @@ -316,6 +350,62 @@ func TestMiddlewareContext(t *testing.T) { }) }) + middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is within trusted CIDR block", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} + return nil + }) + + bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { + cmd.Result = &m.User{Id: 33} + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.req.RemoteAddr = "192.168.1.10:12345" + sc.exec() + + Convey("Should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 33) + So(sc.context.OrgId, ShouldEqual, 4) + }) + }) + + middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is within trusted CIDR block", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} + return nil + }) + + bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { + cmd.Result = &m.User{Id: 33} + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.req.RemoteAddr = "[2001::23]:12345" + sc.exec() + + Convey("Should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 33) + So(sc.context.OrgId, ShouldEqual, 4) + }) + }) + middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) { setting.AuthProxyEnabled = true setting.AuthProxyHeaderName = "X-WEBAUTH-USER" diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index 3024ba94122..e8db0372758 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -8,14 +8,15 @@ import ( // DashboardSnapshot model type DashboardSnapshot struct { - Id int64 - Name string - Key string - DeleteKey string - OrgId int64 - UserId int64 - External bool - ExternalUrl string + Id int64 + Name string + Key string + DeleteKey string + OrgId int64 + UserId int64 + External bool + ExternalUrl string + ExternalDeleteUrl string Expires time.Time Created time.Time @@ -48,7 +49,10 @@ type CreateDashboardSnapshotCommand struct { Expires int64 `json:"expires"` // these are passed when storing an external snapshot ref - External bool `json:"external"` + External bool `json:"external"` + ExternalUrl string `json:"-"` + ExternalDeleteUrl string `json:"-"` + Key string `json:"key"` DeleteKey string `json:"deleteKey"` diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index 04b77a892c5..dd4ae6972aa 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -3,10 +3,8 @@ package plugins import ( "context" "encoding/json" - "os" "os/exec" "path" - "path/filepath" "time" "github.com/grafana/grafana-plugin-model/go/datasource" @@ -29,7 +27,6 @@ type DataSourcePlugin struct { QueryOptions map[string]bool `json:"queryOptions,omitempty"` BuiltIn bool `json:"builtIn,omitempty"` Mixed bool `json:"mixed,omitempty"` - HasQueryHelp bool `json:"hasQueryHelp,omitempty"` Routes []*AppPluginRoute `json:"routes"` Backend bool `json:"backend,omitempty"` @@ -48,15 +45,6 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error { return err } - // look for help markdown - helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md") - if _, err := os.Stat(helpPath); os.IsNotExist(err) { - helpPath = filepath.Join(p.PluginDir, "query_help.md") - } - if _, err := os.Stat(helpPath); err == nil { - p.HasQueryHelp = true - } - DataSources[p.Id] = p return nil } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 9ce50eadd6b..75c68615750 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -166,7 +166,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] var result notifierStateSlice for _, notification := range query.Result { - not, err := n.createNotifierFor(notification) + not, err := InitNotifier(notification) if err != nil { n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err) continue @@ -195,7 +195,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] return result, nil } -func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) { +// InitNotifier instantiate a new notifier based on the model +func InitNotifier(model *m.AlertNotification) (Notifier, error) { notifierPlugin, found := notifierFactories[model.Type] if !found { return nil, errors.New("Unsupported notification type") @@ -208,6 +209,7 @@ type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) var notifierFactories = make(map[string]*NotifierPlugin) +// RegisterNotifier register an notifier func RegisterNotifier(plugin *NotifierPlugin) { notifierFactories[plugin.Type] = plugin } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index 8aa1b80aa22..b6e59f694c8 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -32,7 +32,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error { Settings: cmd.Settings, } - notifiers, err := notifier.createNotifierFor(model) + notifiers, err := InitNotifier(model) if err != nil { log.Error2("Failed to create notifier", "error", err.Error()) diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 7e334ff656f..59ceefa0be5 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, return nil, models.ErrDashboardFolderCannotHaveParent } - if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) { + if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) { return nil, models.ErrDashboardFolderNameExists } @@ -175,7 +175,9 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, dto.User = &models.SignedInUser{ UserId: 0, OrgRole: models.ROLE_ADMIN, + OrgId: dto.OrgId, } + cmd, err := dr.buildSaveDashboardCommand(dto, true, false) if err != nil { return nil, err diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index a236a1d1c4e..dbe441c915e 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "io/ioutil" "net" "net/http" @@ -69,11 +70,14 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook * return err } + defer resp.Body.Close() + if resp.StatusCode/100 == 2 { + // flushing the body enables the transport to reuse the same connection + io.Copy(ioutil.Discard, resp.Body) return nil } - defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err diff --git a/pkg/services/session/mysql.go b/pkg/services/session/mysql.go index f8c5d828cfa..694e16dfb90 100644 --- a/pkg/services/session/mysql.go +++ b/pkg/services/session/mysql.go @@ -29,18 +29,22 @@ import ( // MysqlStore represents a mysql session store implementation. type MysqlStore struct { - c *sql.DB - sid string - lock sync.RWMutex - data map[interface{}]interface{} + c *sql.DB + sid string + lock sync.RWMutex + data map[interface{}]interface{} + expiry int64 + dirty bool } // NewMysqlStore creates and returns a mysql session store. -func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore { +func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}, expiry int64) *MysqlStore { return &MysqlStore{ - c: c, - sid: sid, - data: kv, + c: c, + sid: sid, + data: kv, + expiry: expiry, + dirty: false, } } @@ -50,6 +54,7 @@ func (s *MysqlStore) Set(key, val interface{}) error { defer s.lock.Unlock() s.data[key] = val + s.dirty = true return nil } @@ -67,6 +72,7 @@ func (s *MysqlStore) Delete(key interface{}) error { defer s.lock.Unlock() delete(s.data, key) + s.dirty = true return nil } @@ -77,13 +83,20 @@ func (s *MysqlStore) ID() string { // Release releases resource and save data to provider. func (s *MysqlStore) Release() error { + newExpiry := time.Now().Unix() + if !s.dirty && (s.expiry+60) >= newExpiry { + return nil + } + data, err := session.EncodeGob(s.data) if err != nil { return err } _, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?", - data, time.Now().Unix(), s.sid) + data, newExpiry, s.sid) + s.dirty = false + s.expiry = newExpiry return err } @@ -93,6 +106,7 @@ func (s *MysqlStore) Flush() error { defer s.lock.Unlock() s.data = make(map[interface{}]interface{}) + s.dirty = true return nil } @@ -117,11 +131,12 @@ func (p *MysqlProvider) Init(expire int64, connStr string) (err error) { // Read returns raw session store by session ID. func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { + expiry := time.Now().Unix() var data []byte - err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + err := p.c.QueryRow("SELECT data,expiry FROM session WHERE `key`=?", sid).Scan(&data, &expiry) if err == sql.ErrNoRows { _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", - sid, "", time.Now().Unix()) + sid, "", expiry) } if err != nil { return nil, err @@ -137,7 +152,7 @@ func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { } } - return NewMysqlStore(p.c, sid, kv), nil + return NewMysqlStore(p.c, sid, kv, expiry), nil } // Exist returns true if session with given ID exists. diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index 2e2ea8a4783..d0af676a305 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -47,16 +47,18 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { } snapshot := &m.DashboardSnapshot{ - Name: cmd.Name, - Key: cmd.Key, - DeleteKey: cmd.DeleteKey, - OrgId: cmd.OrgId, - UserId: cmd.UserId, - External: cmd.External, - Dashboard: cmd.Dashboard, - Expires: expires, - Created: time.Now(), - Updated: time.Now(), + Name: cmd.Name, + Key: cmd.Key, + DeleteKey: cmd.DeleteKey, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + External: cmd.External, + ExternalUrl: cmd.ExternalUrl, + ExternalDeleteUrl: cmd.ExternalDeleteUrl, + Dashboard: cmd.Dashboard, + Expires: expires, + Created: time.Now(), + Updated: time.Now(), } _, err := sess.Insert(snapshot) diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 7f70e5c25fc..ccab1106880 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -53,14 +53,14 @@ func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error { } func GetDataSources(query *m.GetDataSourcesQuery) error { - sess := x.Limit(1000, 0).Where("org_id=?", query.OrgId).Asc("name") + sess := x.Limit(5000, 0).Where("org_id=?", query.OrgId).Asc("name") query.Result = make([]*m.DataSource, 0) return sess.Find(&query.Result) } func GetAllDataSources(query *m.GetAllDataSourcesQuery) error { - sess := x.Limit(1000, 0).Asc("name") + sess := x.Limit(5000, 0).Asc("name") query.Result = make([]*m.DataSource, 0) return sess.Find(&query.Result) diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go index 78da198e8e7..ceff2394dce 100644 --- a/pkg/services/sqlstore/login_attempt.go +++ b/pkg/services/sqlstore/login_attempt.go @@ -78,14 +78,14 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error { } func toInt64(i interface{}) int64 { - switch i.(type) { + switch i := i.(type) { case []byte: - n, _ := strconv.ParseInt(string(i.([]byte)), 10, 64) + n, _ := strconv.ParseInt(string(i), 10, 64) return n case int: - return int64(i.(int)) + return int64(i) case int64: - return i.(int64) + return i } return 0 } diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index b880497cd23..be0bc80134c 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -60,4 +60,8 @@ func addDashboardSnapshotMigrations(mg *Migrator) { {Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "dashboard", Type: DB_MediumText, Nullable: false}, })) + + mg.AddMigration("Add column external_delete_url to dashboard_snapshots table", NewAddColumnMigration(snapshotV5, &Column{ + Name: "external_delete_url", Type: DB_NVarchar, Length: 255, Nullable: true, + })) } diff --git a/pkg/services/sqlstore/migrator/conditions.go b/pkg/services/sqlstore/migrator/conditions.go index 79bf7c7ed27..b6adfdbfa29 100644 --- a/pkg/services/sqlstore/migrator/conditions.go +++ b/pkg/services/sqlstore/migrator/conditions.go @@ -2,12 +2,47 @@ package migrator type MigrationCondition interface { Sql(dialect Dialect) (string, []interface{}) + IsFulfilled(results []map[string][]byte) bool } -type IfTableExistsCondition struct { +type ExistsMigrationCondition struct{} + +func (c *ExistsMigrationCondition) IsFulfilled(results []map[string][]byte) bool { + return len(results) >= 1 +} + +type NotExistsMigrationCondition struct{} + +func (c *NotExistsMigrationCondition) IsFulfilled(results []map[string][]byte) bool { + return len(results) == 0 +} + +type IfIndexExistsCondition struct { + ExistsMigrationCondition TableName string + IndexName string } -func (c *IfTableExistsCondition) Sql(dialect Dialect) (string, []interface{}) { - return dialect.TableCheckSql(c.TableName) +func (c *IfIndexExistsCondition) Sql(dialect Dialect) (string, []interface{}) { + return dialect.IndexCheckSql(c.TableName, c.IndexName) +} + +type IfIndexNotExistsCondition struct { + NotExistsMigrationCondition + TableName string + IndexName string +} + +func (c *IfIndexNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) { + return dialect.IndexCheckSql(c.TableName, c.IndexName) +} + +type IfColumnNotExistsCondition struct { + NotExistsMigrationCondition + TableName string + ColumnName string +} + +func (c *IfColumnNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) { + return dialect.ColumnCheckSql(c.TableName, c.ColumnName) } diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index 506a01c3ed8..d2e3c3e0777 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -29,10 +29,12 @@ type Dialect interface { DropTable(tableName string) string DropIndexSql(tableName string, index *Index) string - TableCheckSql(tableName string) (string, []interface{}) RenameTable(oldName string, newName string) string UpdateTableSql(tableName string, columns []*Column) string + IndexCheckSql(tableName, indexName string) (string, []interface{}) + ColumnCheckSql(tableName, columnName string) (string, []interface{}) + ColString(*Column) string ColStringNoPk(*Column) string @@ -182,6 +184,10 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string { return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", quote(oldName), quote(newName)) } +func (db *BaseDialect) ColumnCheckSql(tableName, columnName string) (string, []interface{}) { + return "", nil +} + func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string { quote := db.dialect.Quote name := index.XName(tableName) diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go index fd71cc3d290..cb32c6212e6 100644 --- a/pkg/services/sqlstore/migrator/migrations.go +++ b/pkg/services/sqlstore/migrator/migrations.go @@ -85,7 +85,9 @@ type AddColumnMigration struct { } func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration { - return &AddColumnMigration{tableName: table.Name, column: col} + m := &AddColumnMigration{tableName: table.Name, column: col} + m.Condition = &IfColumnNotExistsCondition{TableName: table.Name, ColumnName: col.Name} + return m } func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration { @@ -109,7 +111,9 @@ type AddIndexMigration struct { } func NewAddIndexMigration(table Table, index *Index) *AddIndexMigration { - return &AddIndexMigration{tableName: table.Name, index: index} + m := &AddIndexMigration{tableName: table.Name, index: index} + m.Condition = &IfIndexNotExistsCondition{TableName: table.Name, IndexName: index.XName(table.Name)} + return m } func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration { @@ -128,7 +132,9 @@ type DropIndexMigration struct { } func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration { - return &DropIndexMigration{tableName: table.Name, index: index} + m := &DropIndexMigration{tableName: table.Name, index: index} + m.Condition = &IfIndexExistsCondition{TableName: table.Name, IndexName: index.XName(table.Name)} + return m } func (m *DropIndexMigration) Sql(dialect Dialect) string { @@ -179,11 +185,6 @@ func NewRenameTableMigration(oldName string, newName string) *RenameTableMigrati return &RenameTableMigration{oldName: oldName, newName: newName} } -func (m *RenameTableMigration) IfTableExists(tableName string) *RenameTableMigration { - m.Condition = &IfTableExistsCondition{TableName: tableName} - return m -} - func (m *RenameTableMigration) Rename(oldName string, newName string) *RenameTableMigration { m.oldName = oldName m.newName = newName @@ -212,11 +213,6 @@ func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap ma return m } -func (m *CopyTableDataMigration) IfTableExists(tableName string) *CopyTableDataMigration { - m.Condition = &IfTableExistsCondition{TableName: tableName} - return m -} - func (m *CopyTableDataMigration) Sql(d Dialect) string { return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols) } diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index dead6f2b416..ce0b2663def 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -94,8 +94,6 @@ func (mg *Migrator) Start() error { Timestamp: time.Now(), } - mg.Logger.Debug("Executing", "sql", sql) - err := mg.inTransaction(func(sess *xorm.Session) error { err := mg.exec(m, sess) if err != nil { @@ -123,18 +121,30 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { condition := m.GetCondition() if condition != nil { sql, args := condition.Sql(mg.Dialect) - results, err := sess.SQL(sql).Query(args...) - if err != nil || len(results) == 0 { - mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id()) - return sess.Rollback() + + if sql != "" { + mg.Logger.Debug("Executing migration condition sql", "id", m.Id(), "sql", sql, "args", args) + results, err := sess.SQL(sql, args...).Query() + if err != nil { + mg.Logger.Error("Executing migration condition failed", "id", m.Id(), "error", err) + return err + } + + if !condition.IsFulfilled(results) { + mg.Logger.Warn("Skipping migration: Already executed, but not recorded in migration log", "id", m.Id()) + return nil + } } } var err error if codeMigration, ok := m.(CodeMigration); ok { + mg.Logger.Debug("Executing code migration", "id", m.Id()) err = codeMigration.Exec(sess, mg) } else { - _, err = sess.Exec(m.Sql(mg.Dialect)) + sql := m.Sql(mg.Dialect) + mg.Logger.Debug("Executing sql migration", "id", m.Id(), "sql", sql) + _, err = sess.Exec(sql) } if err != nil { diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index 7daa4597430..4de55c9a12f 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -90,12 +90,6 @@ func (db *Mysql) SqlType(c *Column) string { return res } -func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) { - args := []interface{}{"grafana", tableName} - sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?" - return sql, args -} - func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string { var statements = []string{} @@ -108,6 +102,18 @@ func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string { return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";" } +func (db *Mysql) IndexCheckSql(tableName, indexName string) (string, []interface{}) { + args := []interface{}{tableName, indexName} + sql := "SELECT 1 FROM " + db.Quote("INFORMATION_SCHEMA") + "." + db.Quote("STATISTICS") + " WHERE " + db.Quote("TABLE_SCHEMA") + " = DATABASE() AND " + db.Quote("TABLE_NAME") + "=? AND " + db.Quote("INDEX_NAME") + "=?" + return sql, args +} + +func (db *Mysql) ColumnCheckSql(tableName, columnName string) (string, []interface{}) { + args := []interface{}{tableName, columnName} + sql := "SELECT 1 FROM " + db.Quote("INFORMATION_SCHEMA") + "." + db.Quote("COLUMNS") + " WHERE " + db.Quote("TABLE_SCHEMA") + " = DATABASE() AND " + db.Quote("TABLE_NAME") + "=? AND " + db.Quote("COLUMN_NAME") + "=?" + return sql, args +} + func (db *Mysql) CleanDB() error { tables, _ := db.engine.DBMetas() sess := db.engine.NewSession() diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index ab8812a1e26..ce529920a39 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -101,9 +101,9 @@ func (db *Postgres) SqlType(c *Column) string { return res } -func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) { - args := []interface{}{"grafana", tableName} - sql := "SELECT table_name FROM information_schema.tables WHERE table_schema=? and table_name=?" +func (db *Postgres) IndexCheckSql(tableName, indexName string) (string, []interface{}) { + args := []interface{}{tableName, indexName} + sql := "SELECT 1 FROM " + db.Quote("pg_indexes") + " WHERE" + db.Quote("tablename") + "=? AND " + db.Quote("indexname") + "=?" return sql, args } diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go index 446e3fcef12..9c0dec05727 100644 --- a/pkg/services/sqlstore/migrator/sqlite_dialect.go +++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go @@ -68,9 +68,10 @@ func (db *Sqlite3) SqlType(c *Column) string { } } -func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) { - args := []interface{}{tableName} - return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +func (db *Sqlite3) IndexCheckSql(tableName, indexName string) (string, []interface{}) { + args := []interface{}{tableName, indexName} + sql := "SELECT 1 FROM " + db.Quote("sqlite_master") + " WHERE " + db.Quote("type") + "='index' AND " + db.Quote("tbl_name") + "=? AND " + db.Quote("name") + "=?" + return sql, args } func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 95b53be9d4a..d0e93177d8b 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -243,7 +243,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) { ss.dbCfg.Path = filepath.Join(ss.Cfg.DataPath, ss.dbCfg.Path) } os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm) - cnnstr = "file:" + ss.dbCfg.Path + "?cache=shared&mode=rwc" + cnnstr = fmt.Sprintf("file:%s?cache=%s&mode=rwc", ss.dbCfg.Path, ss.dbCfg.CacheMode) default: return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type) } @@ -319,6 +319,8 @@ func (ss *SqlStore) readConfig() { ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String() ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String() ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db") + + ss.dbCfg.CacheMode = sec.Key("cache_mode").MustString("private") } func InitTestDB(t *testing.T) *SqlStore { @@ -391,13 +393,20 @@ func IsTestDbPostgres() bool { } type DatabaseConfig struct { - Type, Host, Name, User, Pwd, Path, SslMode string - CaCertPath string - ClientKeyPath string - ClientCertPath string - ServerCertName string - ConnectionString string - MaxOpenConn int - MaxIdleConn int - ConnMaxLifetime int + Type string + Host string + Name string + User string + Pwd string + Path string + SslMode string + CaCertPath string + ClientKeyPath string + ClientCertPath string + ServerCertName string + ConnectionString string + MaxOpenConn int + MaxIdleConn int + ConnMaxLifetime int + CacheMode string } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index a3ccb93b30c..312877751c9 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -345,8 +345,12 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error { return err } +func newSignedInUserCacheKey(orgID, userID int64) string { + return fmt.Sprintf("signed-in-user-%d-%d", userID, orgID) +} + func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error { - cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId) + cacheKey := newSignedInUserCacheKey(query.OrgId, query.UserId) if cached, found := ss.CacheService.Get(cacheKey); found { query.Result = cached.(*m.SignedInUser) return nil @@ -357,6 +361,7 @@ func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) erro return err } + cacheKey = newSignedInUserCacheKey(query.Result.OrgId, query.UserId) ss.CacheService.Set(cacheKey, query.Result, time.Second*5) return nil } diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 627f2ab1ca5..526c17a8256 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -13,7 +13,7 @@ import ( func TestUserDataAccess(t *testing.T) { Convey("Testing DB", t, func() { - InitTestDB(t) + ss := InitTestDB(t) Convey("Creating a user", func() { cmd := &m.CreateUserCommand{ @@ -153,6 +153,27 @@ func TestUserDataAccess(t *testing.T) { So(prefsQuery.Result.UserId, ShouldEqual, 0) }) }) + + Convey("when retreiving signed in user for orgId=0 result should return active org id", func() { + ss.CacheService.Flush() + + query := &m.GetSignedInUserQuery{OrgId: users[1].OrgId, UserId: users[1].Id} + err := ss.GetSignedInUserWithCache(query) + So(err, ShouldBeNil) + So(query.Result, ShouldNotBeNil) + So(query.OrgId, ShouldEqual, users[1].OrgId) + err = SetUsingOrg(&m.SetUsingOrgCommand{UserId: users[1].Id, OrgId: users[0].OrgId}) + So(err, ShouldBeNil) + query = &m.GetSignedInUserQuery{OrgId: 0, UserId: users[1].Id} + err = ss.GetSignedInUserWithCache(query) + So(err, ShouldBeNil) + So(query.Result, ShouldNotBeNil) + So(query.Result.OrgId, ShouldEqual, users[0].OrgId) + + cacheKey := newSignedInUserCacheKey(query.Result.OrgId, query.UserId) + _, found := ss.CacheService.Get(cacheKey) + So(found, ShouldBeTrue) + }) }) }) diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index 93b1ab6f101..f0a3beccb44 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -1,20 +1,21 @@ package setting type OAuthInfo struct { - ClientId, ClientSecret string - Scopes []string - AuthUrl, TokenUrl string - Enabled bool - EmailAttributeName string - AllowedDomains []string - HostedDomain string - ApiUrl string - AllowSignup bool - Name string - TlsClientCert string - TlsClientKey string - TlsClientCa string - TlsSkipVerify bool + ClientId, ClientSecret string + Scopes []string + AuthUrl, TokenUrl string + Enabled bool + EmailAttributeName string + AllowedDomains []string + HostedDomain string + ApiUrl string + AllowSignup bool + Name string + TlsClientCert string + TlsClientKey string + TlsClientCa string + TlsSkipVerify bool + SendClientCredentialsViaPost bool } type OAuther struct { diff --git a/pkg/social/social.go b/pkg/social/social.go index 8918507f3b9..60099a028d6 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -63,28 +63,34 @@ func NewOAuthService() { for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) info := &setting.OAuthInfo{ - ClientId: sec.Key("client_id").String(), - ClientSecret: sec.Key("client_secret").String(), - Scopes: util.SplitString(sec.Key("scopes").String()), - AuthUrl: sec.Key("auth_url").String(), - TokenUrl: sec.Key("token_url").String(), - ApiUrl: sec.Key("api_url").String(), - Enabled: sec.Key("enabled").MustBool(), - EmailAttributeName: sec.Key("email_attribute_name").String(), - AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), - HostedDomain: sec.Key("hosted_domain").String(), - AllowSignup: sec.Key("allow_sign_up").MustBool(), - Name: sec.Key("name").MustString(name), - TlsClientCert: sec.Key("tls_client_cert").String(), - TlsClientKey: sec.Key("tls_client_key").String(), - TlsClientCa: sec.Key("tls_client_ca").String(), - TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), + ClientId: sec.Key("client_id").String(), + ClientSecret: sec.Key("client_secret").String(), + Scopes: util.SplitString(sec.Key("scopes").String()), + AuthUrl: sec.Key("auth_url").String(), + TokenUrl: sec.Key("token_url").String(), + ApiUrl: sec.Key("api_url").String(), + Enabled: sec.Key("enabled").MustBool(), + EmailAttributeName: sec.Key("email_attribute_name").String(), + AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), + HostedDomain: sec.Key("hosted_domain").String(), + AllowSignup: sec.Key("allow_sign_up").MustBool(), + Name: sec.Key("name").MustString(name), + TlsClientCert: sec.Key("tls_client_cert").String(), + TlsClientKey: sec.Key("tls_client_key").String(), + TlsClientCa: sec.Key("tls_client_ca").String(), + TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), + SendClientCredentialsViaPost: sec.Key("send_client_credentials_via_post").MustBool(), } if !info.Enabled { continue } + // handle the clients that do not properly support Basic auth headers and require passing client_id/client_secret via POST payload + if info.SendClientCredentialsViaPost { + oauth2.RegisterBrokenAuthHeaderProvider(info.TokenUrl) + } + if name == "grafananet" { name = grafanaCom } diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 165f8fdbe97..fb92c827f7c 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -3,7 +3,6 @@ package cloudwatch import ( "fmt" "os" - "strings" "sync" "time" @@ -43,7 +42,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { secretAccessKey := "" sessionToken := "" var expiration *time.Time = nil - if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 { + if dsInfo.AuthType == "arn" { params := &sts.AssumeRoleInput{ RoleArn: aws.String(dsInfo.AssumeRoleArn), RoleSessionName: aws.String("GrafanaSession"), diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index f1113511bae..94f37904eee 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -16,6 +16,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data rawQuery := model.Get("query").MustString("") useRawQuery := model.Get("rawQuery").MustBool(false) alias := model.Get("alias").MustString("") + tz := model.Get("tz").MustString("") measurement := model.Get("measurement").MustString("") @@ -55,6 +56,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data Interval: parsedInterval, Alias: alias, UseRawQuery: useRawQuery, + Tz: tz, }, nil } diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go index 7be9cae9702..e620b0c631d 100644 --- a/pkg/tsdb/influxdb/model_parser_test.go +++ b/pkg/tsdb/influxdb/model_parser_test.go @@ -41,6 +41,7 @@ func TestInfluxdbQueryParser(t *testing.T) { } ], "measurement": "logins.count", + "tz": "Europe/Paris", "policy": "default", "refId": "B", "resultFormat": "time_series", @@ -115,6 +116,7 @@ func TestInfluxdbQueryParser(t *testing.T) { So(len(res.GroupBy), ShouldEqual, 3) So(len(res.Selects), ShouldEqual, 3) So(len(res.Tags), ShouldEqual, 2) + So(res.Tz, ShouldEqual, "Europe/Paris") So(res.Interval, ShouldEqual, time.Second*20) So(res.Alias, ShouldEqual, "serie alias") }) diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index 82ed72c2a18..cbe82e5e2ef 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -13,6 +13,7 @@ type Query struct { UseRawQuery bool Alias string Interval time.Duration + Tz string } type Tag struct { diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 7cb8f0ecd82..f1a8c745956 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -26,6 +26,7 @@ func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) { res += query.renderWhereClause() res += query.renderTimeFilter(queryContext) res += query.renderGroupBy(queryContext) + res += query.renderTz() } calculator := tsdb.NewIntervalCalculator(&tsdb.IntervalOptions{}) @@ -154,3 +155,12 @@ func (query *Query) renderGroupBy(queryContext *tsdb.TsdbQuery) string { return groupBy } + +func (query *Query) renderTz() string { + tz := query.Tz + if tz == "" { + return "" + } else { + return fmt.Sprintf(" tz('%s')", tz) + } +} diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index cc1358a72d7..f85419c8ae2 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -47,6 +47,20 @@ func TestInfluxdbQueryBuilder(t *testing.T) { So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`) }) + Convey("can build query with tz", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2}}, + Measurement: "cpu", + GroupBy: []*QueryPart{groupBy1}, + Tz: "Europe/Paris", + Interval: time.Second * 5, + } + + rawQuery, err := query.Build(queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(5s) tz('Europe/Paris')`) + }) + Convey("can build query with group bys", func() { query := &Query{ Selects: []*Select{{*qp1, *qp2}}, diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 26a3d1e53ee..0b3685c2610 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -86,11 +86,11 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 6a71caedeb9..90f6c6d879a 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -41,7 +41,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) }) Convey("interpolate __timeFrom function", func() { @@ -138,7 +138,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) }) Convey("interpolate __unixEpochFilter function", func() { @@ -158,7 +158,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) }) Convey("interpolate __unixEpochFilter function", func() { @@ -168,5 +168,22 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix())) }) }) + + Convey("Given a time range between 1960-02-01 07:00:00.5 and 1980-02-03 08:00:00.5", func() { + from := time.Date(1960, 2, 1, 7, 0, 0, 500e6, time.UTC) + to := time.Date(1980, 2, 3, 8, 0, 0, 500e6, time.UTC) + timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10)) + + So(from.Format(time.RFC3339Nano), ShouldEqual, "1960-02-01T07:00:00.5Z") + So(to.Format(time.RFC3339Nano), ShouldEqual, "1980-02-03T08:00:00.5Z") + Convey("interpolate __timeFilter function", func() { + sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) + }) + + }) + }) } diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx index 590c11c0615..977892c637d 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx @@ -28,8 +28,10 @@ class CustomScrollbar extends PureComponent {
} renderTrackVertical={props =>
} renderThumbHorizontal={props =>
} diff --git a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 310eb714af9..0a7de5fcffe 100644 --- a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` style={ Object { "height": "auto", - "maxHeight": "inherit", - "minHeight": "inherit", + "maxHeight": "100%", + "minHeight": "0", "overflow": "hidden", "position": "relative", "width": "100%", @@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` "left": undefined, "marginBottom": 0, "marginRight": 0, - "maxHeight": "calc(inherit + 0px)", - "minHeight": "calc(inherit + 0px)", + "maxHeight": "calc(100% + 0px)", + "minHeight": "calc(0 + 0px)", "overflow": "scroll", "position": "relative", "right": undefined, diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index ae0e39cc26d..d63af72ae4d 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -24,12 +24,14 @@ class EmptyListCTA extends Component { {buttonTitle} -
- ProTip: {proTip} - - {proTipLinkTitle} - -
+ {proTip && ( +
+ ProTip: {proTip} + + {proTipLinkTitle} + +
+ )}
); } diff --git a/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx b/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000000..188750b0fef --- /dev/null +++ b/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,44 @@ +import { Component } from 'react'; + +interface ErrorInfo { + componentStack: string; +} + +interface RenderProps { + error: Error; + errorInfo: ErrorInfo; +} + +interface Props { + children: (r: RenderProps) => JSX.Element; +} + +interface State { + error: Error; + errorInfo: ErrorInfo; +} + +class ErrorBoundary extends Component { + readonly state: State = { + error: null, + errorInfo: null, + }; + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ + error: error, + errorInfo: errorInfo + }); + } + + render() { + const { children } = this.props; + const { error, errorInfo } = this.state; + return children({ + error, + errorInfo, + }); + } +} + +export default ErrorBoundary; diff --git a/public/app/core/components/Form/Element.tsx b/public/app/core/components/Form/Element.tsx deleted file mode 100644 index 997d7f0e717..00000000000 --- a/public/app/core/components/Form/Element.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { PureComponent, ReactNode, ReactElement } from 'react'; -import { Label } from './Label'; -import { uniqueId } from 'lodash'; - -interface Props { - label?: ReactNode; - labelClassName?: string; - id?: string; - children: ReactElement; -} - -export class Element extends PureComponent { - elementId: string = this.props.id || uniqueId('form-element-'); - - get elementLabel() { - const { label, labelClassName } = this.props; - - if (label) { - return ( - - ); - } - - return null; - } - - get children() { - const { children } = this.props; - - return React.cloneElement(children, { id: this.elementId }); - } - - render() { - return ( -
- {this.elementLabel} - {this.children} -
- ); - } -} diff --git a/public/app/core/components/Form/Label.tsx b/public/app/core/components/Form/Label.tsx deleted file mode 100644 index 385a1b325be..00000000000 --- a/public/app/core/components/Form/Label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { PureComponent, ReactNode } from 'react'; - -interface Props { - children: ReactNode; - htmlFor?: string; - className?: string; -} - -export class Label extends PureComponent { - render() { - const { children, htmlFor, className } = this.props; - - return ( - - ); - } -} diff --git a/public/app/core/components/Form/index.ts b/public/app/core/components/Form/index.ts index e4c8197aaa9..6322cf3241a 100644 --- a/public/app/core/components/Form/index.ts +++ b/public/app/core/components/Form/index.ts @@ -1,3 +1 @@ -export { Element } from './Element'; export { Input } from './Input'; -export { Label } from './Label'; diff --git a/public/app/core/components/PluginHelp/PluginHelp.tsx b/public/app/core/components/PluginHelp/PluginHelp.tsx new file mode 100644 index 00000000000..c37498afc45 --- /dev/null +++ b/public/app/core/components/PluginHelp/PluginHelp.tsx @@ -0,0 +1,83 @@ +import React, { PureComponent } from 'react'; +import Remarkable from 'remarkable'; +import { getBackendSrv } from '../../services/backend_srv'; + +interface Props { + plugin: { + name: string; + id: string; + }; + type: string; +} + +interface State { + isError: boolean; + isLoading: boolean; + help: string; +} + +export class PluginHelp extends PureComponent { + state = { + isError: false, + isLoading: false, + help: '', + }; + + componentDidMount(): void { + this.loadHelp(); + } + + constructPlaceholderInfo() { + return 'No plugin help or readme markdown file was found'; + } + + loadHelp = () => { + const { plugin, type } = this.props; + this.setState({ isLoading: true }); + + getBackendSrv() + .get(`/api/plugins/${plugin.id}/markdown/${type}`) + .then(response => { + const markdown = new Remarkable(); + const helpHtml = markdown.render(response); + + if (response === '' && type === 'help') { + this.setState({ + isError: false, + isLoading: false, + help: this.constructPlaceholderInfo(), + }); + } else { + this.setState({ + isError: false, + isLoading: false, + help: helpHtml, + }); + } + }) + .catch(() => { + this.setState({ + isError: true, + isLoading: false, + }); + }); + }; + + render() { + const { type } = this.props; + const { isError, isLoading, help } = this.state; + + if (isLoading) { + return

Loading help...

; + } + + if (isError) { + return

'Error occurred when loading help'

; + } + + if (type === 'panel_help' && help === '') { + } + + return
; + } +} diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 872f2f28bb8..2524a265054 100644 --- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -52,7 +52,11 @@ export const ToggleButton: SFC = ({ ); if (tooltip) { - return {button}; + return ( + + {button} + + ); } else { return button; } diff --git a/public/app/core/components/Tooltip/Popper.tsx b/public/app/core/components/Tooltip/Popper.tsx index 36cf0fe837e..65ef510ba8f 100644 --- a/public/app/core/components/Tooltip/Popper.tsx +++ b/public/app/core/components/Tooltip/Popper.tsx @@ -3,6 +3,11 @@ import Portal from 'app/core/components/Portal/Portal'; import { Manager, Popper as ReactPopper, Reference } from 'react-popper'; import Transition from 'react-transition-group/Transition'; +export enum Themes { + Default = 'popper__background--default', + Error = 'popper__background--error', +} + const defaultTransitionStyles = { transition: 'opacity 200ms linear', opacity: 0, @@ -21,13 +26,16 @@ interface Props { placement?: any; content: string | ((props: any) => JSX.Element); refClassName?: string; + theme?: Themes; } class Popper extends PureComponent { render() { - const { children, renderContent, show, placement, refClassName } = this.props; + const { children, renderContent, show, placement, refClassName, theme } = this.props; const { content } = this.props; + const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : ''); + return ( @@ -53,7 +61,7 @@ class Popper extends PureComponent { data-placement={placement} className="popper" > -
+
{renderContent(content)}
diff --git a/public/app/core/components/Tooltip/withPopper.tsx b/public/app/core/components/Tooltip/withPopper.tsx index 4ba05937531..3766b78f0f6 100644 --- a/public/app/core/components/Tooltip/withPopper.tsx +++ b/public/app/core/components/Tooltip/withPopper.tsx @@ -1,5 +1,5 @@ import React from 'react'; - +import { Themes } from './Popper'; export interface UsingPopperProps { showPopper: (prevState: object) => void; hidePopper: (prevState: object) => void; @@ -9,6 +9,7 @@ export interface UsingPopperProps { content: string | ((props: any) => JSX.Element); className?: string; refClassName?: string; + theme?: Themes; } interface Props { @@ -16,6 +17,7 @@ interface Props { className?: string; refClassName?: string; content: string | ((props: any) => JSX.Element); + theme?: Themes; } interface State { @@ -71,7 +73,6 @@ export default function withPopper(WrappedComponent) { render() { const { show, placement } = this.state; const className = this.props.className || ''; - return (
`; @@ -61,7 +61,7 @@ function link(scope, elem, attrs) { const maxLines = attrs.maxLines || DEFAULT_MAX_LINES; const showGutter = attrs.showGutter !== undefined; const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIORS; const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index c039d818281..65e7502a810 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -1,5 +1,5 @@ // Based on work https://github.com/mohsen1/json-formatter-js -// Licence MIT, Copyright (c) 2015 Mohsen Azimi +// License MIT, Copyright (c) 2015 Mohsen Azimi /* * Escapes `"` characters from string diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 9a344d3195b..228154f9884 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -1,5 +1,5 @@ // Based on work https://github.com/mohsen1/json-formatter-js -// Licence MIT, Copyright (c) 2015 Mohsen Azimi +// License MIT, Copyright (c) 2015 Mohsen Azimi import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers'; diff --git a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx index 8eaed4ca264..b52e5311dc5 100644 --- a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx +++ b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx @@ -36,7 +36,7 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - it('should render organisation switcher', () => { + it('should render organization switcher', () => { const wrapper = setup({ link: { showOrgSwitcher: true, diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap index f3181b617ad..ae8c9c753aa 100644 --- a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap @@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
`; -exports[`Render should render organisation switcher 1`] = ` +exports[`Render should render organization switcher 1`] = `
diff --git a/public/app/core/directives/tags.ts b/public/app/core/directives/tags.ts index 33a2252a683..27bddfb1883 100644 --- a/public/app/core/directives/tags.ts +++ b/public/app/core/directives/tags.ts @@ -69,7 +69,7 @@ function bootstrapTagsinput() { }, }); - select.on('itemAdded', event => { + select.on('itemAdded', (event: any) => { if (scope.model.indexOf(event.item) === -1) { scope.model.push(event.item); if (scope.onTagsUpdated) { @@ -85,7 +85,7 @@ function bootstrapTagsinput() { setColor(event.item, tagElement); }); - select.on('itemRemoved', event => { + select.on('itemRemoved', (event: any) => { const idx = scope.model.indexOf(event.item); if (idx !== -1) { scope.model.splice(idx, 1); diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index b49c38b9007..3666fde3b91 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import config from 'app/core/config'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; export class LiveSrv { conn: any; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index fabb29c6e18..4e8c6207959 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -2,14 +2,23 @@ import _ from 'lodash'; import { TimeSeries } from 'app/core/core'; import colors, { getThemeColor } from 'app/core/utils/colors'; +/** + * Mapping of log level abbreviation to canonical log level. + * Supported levels are reduce to limit color variation. + */ export enum LogLevel { + emerg = 'critical', + alert = 'critical', crit = 'critical', critical = 'critical', warn = 'warning', warning = 'warning', err = 'error', + eror = 'error', error = 'error', info = 'info', + notice = 'info', + dbug = 'debug', debug = 'debug', trace = 'trace', unkown = 'unkown', @@ -81,7 +90,9 @@ export interface LogsStream { export interface LogsStreamEntry { line: string; - timestamp: string; + ts: string; + // Legacy, was renamed to ts + timestamp?: string; } export interface LogsStreamLabels { diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 144567efeb9..854169ad4b0 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -5,7 +5,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; export class BackendSrv { private inFlightRequests = {}; - private HTTP_REQUEST_CANCELLED = -1; + private HTTP_REQUEST_CANCELED = -1; private noBackendCache: boolean; /** @ngInject */ @@ -178,7 +178,7 @@ export class BackendSrv { return response; }) .catch(err => { - if (err.status === this.HTTP_REQUEST_CANCELLED) { + if (err.status === this.HTTP_REQUEST_CANCELED) { throw { err, cancelled: true }; } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index aa5e146530b..22d33921ebd 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -31,7 +31,7 @@ export class SearchSrv { } private queryForRecentDashboards() { - const dashIds = _.take(impressionSrv.getDashboardOpened(), 5); + const dashIds = _.take(impressionSrv.getDashboardOpened(), 30); if (dashIds.length === 0) { return Promise.resolve([]); } @@ -70,7 +70,7 @@ export class SearchSrv { return Promise.resolve(); } - return this.backendSrv.search({ starred: true, limit: 5 }).then(result => { + return this.backendSrv.search({ starred: true, limit: 30 }).then(result => { if (result.length > 0) { sections['starred'] = { title: 'Starred', diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts index 8052b3f2e2c..8234b6288d4 100644 --- a/public/app/core/services/timer.ts +++ b/public/app/core/services/timer.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; // This service really just tracks a list of $timeout promises to give us a -// method for cancelling them all when we need to +// method for canceling them all when we need to export class Timer { timers = []; diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts index 52ec4ccea19..98f5a5be742 100644 --- a/public/app/core/specs/file_export.test.ts +++ b/public/app/core/specs/file_export.test.ts @@ -73,6 +73,7 @@ describe('file_export', () => { ], rows: [ [123, 'some_string', 1.234, true], + [1000, 'some_string', 1.234567891, true], [0o765, 'some string with " in the middle', 1e-2, false], [0o765, 'some string with "" in the middle', 1e-2, false], [0o765, 'some string with """ in the middle', 1e-2, false], @@ -89,6 +90,7 @@ describe('file_export', () => { const expectedText = '"integer_value";"string_value";"float_value";"boolean_value"\r\n' + '123;"some_string";1.234;true\r\n' + + '1000;"some_string";1.234567891;true\r\n' + '501;"some string with "" in the middle";0.01;false\r\n' + '501;"some string with """" in the middle";0.01;false\r\n' + '501;"some string with """""" in the middle";0.01;false\r\n' + diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index fd99db69249..fa7170bed13 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -40,7 +40,7 @@ export default class TableModel { this.rows.sort((a, b) => { a = a[options.col]; b = b[options.col]; - // Sort null or undefined seperately from comparable values + // Sort null or undefined separately from comparable values return +(a == null) - +(b == null) || +(a > b) || -(a < b); }); diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 7ceebbd8047..a3b08516d16 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -14,7 +14,6 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { datasourceError: null, datasourceLoading: null, datasourceMissing: false, - datasourceName: '', exploreDatasources: [], graphInterval: 1000, history: [], @@ -69,7 +68,7 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - datasourceName: 'foo', + initialDatasource: 'foo', range: { from: 'now-5h', to: 'now', @@ -94,7 +93,7 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - datasourceName: 'foo', + initialDatasource: 'foo', range: { from: 'now-5h', to: 'now', @@ -120,7 +119,7 @@ describe('state functions', () => { it('can parse the serialized state into the original state', () => { const state = { ...DEFAULT_EXPLORE_STATE, - datasourceName: 'foo', + initialDatasource: 'foo', range: { from: 'now - 5h', to: 'now', @@ -144,7 +143,7 @@ describe('state functions', () => { const resultState = { ...rest, datasource: DEFAULT_EXPLORE_STATE.datasource, - datasourceName: datasource, + initialDatasource: datasource, initialQueries: queries, }; diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index f978ec1ef8c..bea166075dc 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -9,7 +9,8 @@ import { parse as parseDate } from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore'; -import { DataQuery, RawTimeRange, IntervalValues, DataSourceApi } from 'app/types/series'; +import { DataQuery, DataSourceApi } from 'app/types/series'; +import { RawTimeRange, IntervalValues } from '@grafana/ui'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -104,7 +105,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { const urlState: ExploreUrlState = { - datasource: state.datasourceName, + datasource: state.initialDatasource, queries: state.initialQueries.map(clearQueryKeys), range: state.range, }; diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 4fbdea0f953..1f999da72a5 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -41,10 +41,8 @@ function formatSpecialHeader(useExcelHeader) { function formatRow(row, addEndRowDelimiter = true) { let text = ''; for (let i = 0; i < row.length; i += 1) { - if (isBoolean(row[i]) || isNullOrUndefined(row[i])) { + if (isBoolean(row[i]) || isNumber(row[i]) || isNullOrUndefined(row[i])) { text += row[i]; - } else if (isNumber(row[i])) { - text += row[i].toLocaleString(); } else { text += `${QUOTE}${csvEscaped(htmlUnescaped(htmlDecoded(row[i])))}${QUOTE}`; } diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 30fdf2bac6f..d32844c44ed 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -484,6 +484,14 @@ kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); +// Floating Point Operations per Second +kbn.valueFormats.flops = kbn.formatBuilders.decimalSIPrefix('FLOP/s'); +kbn.valueFormats.mflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 2); +kbn.valueFormats.gflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 3); +kbn.valueFormats.tflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 4); +kbn.valueFormats.pflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 5); +kbn.valueFormats.eflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 6); + // Hash Rate kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s'); kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1); @@ -621,6 +629,8 @@ kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³'); kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³'); kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³'); kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³'); +kbn.valueFormats.conmgdL = kbn.formatBuilders.fixedUnit('mg/dL'); +kbn.valueFormats.conmmolL = kbn.formatBuilders.fixedUnit('mmol/L'); // Time kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); @@ -1019,6 +1029,17 @@ kbn.getUnitFormats = () => { { text: 'exahashes/sec', value: 'EHs' }, ], }, + { + text: 'computation throughput', + submenu: [ + { text: 'FLOP/s', value: 'flops' }, + { text: 'MFLOP/s', value: 'mflops' }, + { text: 'GFLOP/s', value: 'gflops' }, + { text: 'TFLOP/s', value: 'tflops' }, + { text: 'PFLOP/s', value: 'pflops' }, + { text: 'EFLOP/s', value: 'eflops' }, + ], + }, { text: 'throughput', submenu: [ @@ -1085,7 +1106,7 @@ kbn.getUnitFormats = () => { { text: 'Watt (W)', value: 'watt' }, { text: 'Kilowatt (kW)', value: 'kwatt' }, { text: 'Milliwatt (mW)', value: 'mwatt' }, - { text: 'Watt per square metre (W/m²)', value: 'Wm2' }, + { text: 'Watt per square meter (W/m²)', value: 'Wm2' }, { text: 'Volt-ampere (VA)', value: 'voltamp' }, { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, @@ -1182,14 +1203,16 @@ kbn.getUnitFormats = () => { submenu: [ { text: 'parts-per-million (ppm)', value: 'ppm' }, { text: 'parts-per-billion (ppb)', value: 'conppb' }, - { text: 'nanogram per cubic metre (ng/m³)', value: 'conngm3' }, - { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' }, - { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' }, - { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' }, - { text: 'milligram per cubic metre (mg/m³)', value: 'conmgm3' }, - { text: 'milligram per normal cubic metre (mg/Nm³)', value: 'conmgNm3' }, - { text: 'gram per cubic metre (g/m³)', value: 'congm3' }, - { text: 'gram per normal cubic metre (g/Nm³)', value: 'congNm3' }, + { text: 'nanogram per cubic meter (ng/m³)', value: 'conngm3' }, + { text: 'nanogram per normal cubic meter (ng/Nm³)', value: 'conngNm3' }, + { text: 'microgram per cubic meter (μg/m³)', value: 'conμgm3' }, + { text: 'microgram per normal cubic meter (μg/Nm³)', value: 'conμgNm3' }, + { text: 'milligram per cubic meter (mg/m³)', value: 'conmgm3' }, + { text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' }, + { text: 'gram per cubic meter (g/m³)', value: 'congm3' }, + { text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' }, + { text: 'milligrams per decilitre (mg/dL)', value: 'conmgdL' }, + { text: 'millimoles per litre (mmol/L)', value: 'conmmolL' }, ], }, ]; diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 0150e80f1ed..310c8ab8533 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import moment from 'moment'; -import { RawTimeRange } from 'app/types/series'; +import { RawTimeRange } from '@grafana/ui'; import * as dateMath from './datemath'; diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index f47a6348303..86bb0207460 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; -import classNames from 'classnames/bind'; +import classNames from 'classnames'; import { AlertRule } from '../../types'; export interface Props { @@ -23,7 +23,7 @@ class AlertRuleItem extends PureComponent { render() { const { rule, onTogglePause } = this.props; - const stateClass = classNames({ + const iconClassName = classNames({ fa: true, 'fa-play': rule.state === 'paused', 'fa-pause': rule.state !== 'paused', @@ -55,7 +55,7 @@ class AlertRuleItem extends PureComponent { title="Pausing an alert rule prevents it from executing" onClick={onTogglePause} > - + diff --git a/public/app/features/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx new file mode 100644 index 00000000000..a5afbc198fc --- /dev/null +++ b/public/app/features/alerting/AlertTab.tsx @@ -0,0 +1,141 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Services & Utils +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +import appEvents from 'app/core/app_events'; + +// Components +import { EditorTabBody, EditorToolbarView } from '../dashboard/dashgrid/EditorTabBody'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import StateHistory from './StateHistory'; +import 'app/features/alerting/AlertTabCtrl'; + +// Types +import { DashboardModel } from '../dashboard/dashboard_model'; +import { PanelModel } from '../dashboard/panel_model'; + +interface Props { + angularPanel?: AngularComponent; + dashboard: DashboardModel; + panel: PanelModel; +} + +export class AlertTab extends PureComponent { + element: any; + component: AngularComponent; + panelCtrl: any; + + componentDidMount() { + if (this.shouldLoadAlertTab()) { + this.loadAlertTab(); + } + } + + componentDidUpdate(prevProps: Props) { + if (this.shouldLoadAlertTab()) { + this.loadAlertTab(); + } + } + + shouldLoadAlertTab() { + return this.props.angularPanel && this.element && !this.component; + } + + componentWillUnmount() { + if (this.component) { + this.component.destroy(); + } + } + + loadAlertTab() { + const { angularPanel } = this.props; + + const scope = angularPanel.getScope(); + + // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet + if (!scope.$$childHead) { + setTimeout(() => { + this.forceUpdate(); + }); + return; + } + + this.panelCtrl = scope.$$childHead.ctrl; + const loader = getAngularLoader(); + const template = ''; + + const scopeProps = { + ctrl: this.panelCtrl, + }; + + this.component = loader.load(this.element, scopeProps, template); + } + + stateHistory = (): EditorToolbarView => { + return { + title: 'State history', + render: () => { + return ( + + ); + }, + }; + }; + + deleteAlert = (): EditorToolbarView => { + const { panel } = this.props; + return { + title: 'Delete', + btnType: 'danger', + onClick: () => { + appEvents.emit('confirm-modal', { + title: 'Delete Alert', + text: 'Are you sure you want to delete this alert rule?', + text2: 'You need to save dashboard for the delete to take effect', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + delete panel.alert; + panel.thresholds = []; + this.panelCtrl.alertState = null; + this.panelCtrl.render(); + this.forceUpdate(); + }, + }); + }, + }; + }; + + onAddAlert = () => { + this.panelCtrl._enableAlert(); + this.component.digest(); + this.forceUpdate(); + }; + + render() { + const { alert } = this.props.panel; + + const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : []; + + const model = { + title: 'Panel has no alert rule defined', + icon: 'icon-gf icon-gf-alert', + onClick: this.onAddAlert, + buttonTitle: 'Create Alert', + }; + + return ( + + <> +
(this.element = element)} /> + {!alert && } + + + ); + } +} diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 6d87c159d02..2be25e9df6a 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -45,6 +45,7 @@ export class AlertTabCtrl { this.noDataModes = alertDef.noDataModes; this.executionErrorModes = alertDef.executionErrorModes; this.appSubUrl = config.appSubUrl; + this.panelCtrl._enableAlert = this.enable; } $onInit() { @@ -114,7 +115,7 @@ export class AlertTabCtrl { } getNotifications() { - return Promise.resolve( + return this.$q.when( this.notifications.map(item => { return this.uiSegmentSrv.newSegment(item.name); }) @@ -147,6 +148,7 @@ export class AlertTabCtrl { // reset plus button this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html; + this.addNotificationSegment.fake = true; } removeNotification(index) { @@ -353,11 +355,11 @@ export class AlertTabCtrl { }); } - enable() { + enable = () => { this.panel.alert = {}; this.initModel(); this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes - } + }; evaluatorParamsChanged() { ThresholdMapper.alertToGraphThresholds(this.panel); diff --git a/public/app/features/alerting/StateHistory.tsx b/public/app/features/alerting/StateHistory.tsx new file mode 100644 index 00000000000..eb5541f6094 --- /dev/null +++ b/public/app/features/alerting/StateHistory.tsx @@ -0,0 +1,110 @@ +import React, { PureComponent } from 'react'; +import alertDef from './state/alertDef'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import appEvents from '../../core/app_events'; + +interface Props { + dashboard: DashboardModel; + panelId: number; + onRefresh: () => void; +} + +interface State { + stateHistoryItems: any[]; +} + +class StateHistory extends PureComponent { + state = { + stateHistoryItems: [], + }; + + componentDidMount(): void { + const { dashboard, panelId } = this.props; + + getBackendSrv() + .get(`/api/annotations?dashboardId=${dashboard.id}&panelId=${panelId}&limit=50&type=alert`) + .then(res => { + const items = res.map(item => { + return { + stateModel: alertDef.getStateDisplayModel(item.newState), + time: dashboard.formatDate(item.time, 'MMM D, YYYY HH:mm:ss'), + info: alertDef.getAlertAnnotationInfo(item), + }; + }); + + this.setState({ + stateHistoryItems: items, + }); + }); + } + + clearHistory = () => { + const { dashboard, onRefresh, panelId } = this.props; + + appEvents.emit('confirm-modal', { + title: 'Delete Alert History', + text: 'Are you sure you want to remove all history & annotations for this alert?', + icon: 'fa-trash', + yesText: 'Yes', + onConfirm: () => { + getBackendSrv() + .post('/api/annotations/mass-delete', { + dashboardId: dashboard.id, + panelId: panelId, + }) + .then(() => { + onRefresh(); + }); + + this.setState({ + stateHistoryItems: [], + }); + }, + }); + }; + + render() { + const { stateHistoryItems } = this.state; + + return ( +
+ {stateHistoryItems.length > 0 && ( +
+ Last 50 state changes + +
+ )} +
    + {stateHistoryItems.length > 0 ? ( + stateHistoryItems.map((item, index) => { + return ( +
  1. +
    + +
    +
    +
    +

    {item.alertName}

    +
    + {item.stateModel.text} +
    +
    + {item.info} +
    +
    {item.time}
    +
  2. + ); + }) + ) : ( + No state changes recorded + )} +
+
+ ); + } +} + +export default StateHistory; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 0e4e48a89a9..da862203da6 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,191 +1,168 @@ -
-
- +
+
+ {{ctrl.error}} +
+
+
+
+

Rule

+
+
+ Name + +
+
+ Evaluate every + +
+
+ + + + If an alert rule has a configured For and the query violates the configured + threshold it + will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert + rule + has + been firing for more than For duration, it will change to Alerting and send alert + notifications. + +
+
+
-
-
-
- {{ctrl.error}} -
+
+

Conditions

+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
-
Alert Config
-
- Name - -
-
-
- Evaluate every - -
-
- - - - If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. - Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications. - -
-
-
+
+ +
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+

No Data & Error Handling

+
+
+ If no data or all values are null +
+
+ SET STATE TO +
+ +
+
+
-
- -
-
+
+
+ If execution error or timeout +
+
+ SET STATE TO +
+ +
+
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ Evaluating rule +
-
- -
-
+
+ +
+
+
-
- Evaluating rule -
- -
- -
-
- -
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
- -
- -
- State history (last 50 state changes) -
- -
-
- No state changes recorded -
- -
    -
  1. -
    - -
    -
    -
    -
    - {{al.stateModel.text}} -
    -
    - {{al.info}} -
    -
    - {{al.time}} -
    -
  2. -
-
-
-
-
- -
-
-
Panel has no alert rule defined
- -
-
+
+
Notifications
+
+
+
+ Send to +
+
+ +  {{nc.name}}  + + +
+
+ +
+
+
+ Message + +
+
+
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index d2aa1f24c57..e14873fa9f6 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -13,7 +13,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { DeleteButton } from '@grafana/ui'; export interface Props { navModel: NavModel; @@ -224,7 +224,7 @@ export class ApiKeysPage extends PureComponent { {key.name} {key.role} - this.onDeleteApiKey(key)} /> + this.onDeleteApiKey(key)} /> ); diff --git a/public/app/features/dashboard/dashgrid/AlertTab.tsx b/public/app/features/dashboard/dashgrid/AlertTab.tsx deleted file mode 100644 index 7df7864c758..00000000000 --- a/public/app/features/dashboard/dashgrid/AlertTab.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; -import { EditorTabBody } from './EditorTabBody'; -import 'app/features/alerting/AlertTabCtrl'; - -interface Props { - angularPanel?: AngularComponent; -} - -export class AlertTab extends PureComponent { - element: any; - component: AngularComponent; - - constructor(props) { - super(props); - } - - componentDidMount() { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - componentDidUpdate(prevProps: Props) { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - shouldLoadAlertTab() { - return this.props.angularPanel && this.element; - } - - componentWillUnmount() { - if (this.component) { - this.component.destroy(); - } - } - - loadAlertTab() { - const { angularPanel } = this.props; - - const scope = angularPanel.getScope(); - - // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet - if (!scope.$$childHead) { - setTimeout(() => { - this.forceUpdate(); - }); - return; - } - - const panelCtrl = scope.$$childHead.ctrl; - const loader = getAngularLoader(); - const template = ''; - - const scopeProps = { - ctrl: panelCtrl, - }; - - this.component = loader.load(this.element, scopeProps, template); - } - - render() { - return ( - -
(this.element = element)} /> - - ); - } -} diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 6ee83d6e5b9..9de298e0799 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -14,6 +14,7 @@ import { PanelEditor } from './PanelEditor'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types'; +import { PanelResizer } from './PanelResizer'; export interface Props { panel: PanelModel; @@ -158,10 +159,21 @@ export class DashboardPanel extends PureComponent { return (
-
- {plugin.exports.Panel && this.renderReactPanel()} - {plugin.exports.PanelCtrl && this.renderAngularPanel()} -
+ ( +
+ {plugin.exports.Panel && this.renderReactPanel()} + {plugin.exports.PanelCtrl && this.renderAngularPanel()} +
+ )} + /> {panel.isEditing && ( { this.state = { loading: LoadingState.NotStarted, + errorMessage: '', response: { data: [], }, @@ -89,7 +97,7 @@ export class DataPanel extends Component { return; } - this.setState({ loading: LoadingState.Loading }); + this.setState({ loading: LoadingState.Loading, errorMessage: '' }); try { const ds = await this.dataSourceSrv.get(datasource); @@ -127,10 +135,20 @@ export class DataPanel extends Component { }); } catch (err) { console.log('Loading error', err); - this.setState({ loading: LoadingState.Error, isFirstLoad: false }); + this.onError('Request Error'); } }; + onError = (errorMessage: string) => { + if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) { + this.setState({ + loading: LoadingState.Error, + isFirstLoad: false, + errorMessage: errorMessage + }); + } + } + render() { const { queries } = this.props; const { response, loading, isFirstLoad } = this.state; @@ -138,7 +156,7 @@ export class DataPanel extends Component { const timeSeries = response.data; if (isFirstLoad && loading === LoadingState.Loading) { - return this.renderLoadingSpinner(); + return this.renderLoadingStates(); } if (!queries.length) { @@ -151,24 +169,48 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingSpinner()} - {this.props.children({ - timeSeries, - loading, - })} + {this.renderLoadingStates()} + + {({error, errorInfo}) => { + if (errorInfo) { + this.onError(error.message || DEFAULT_PLUGIN_ERROR); + return null; + } + return ( + <> + {this.props.children({ + timeSeries, + loading, + })} + + ); + }} + ); } - private renderLoadingSpinner(): JSX.Element { - const { loading } = this.state; - + private renderLoadingStates(): JSX.Element { + const { loading, errorMessage } = this.state; if (loading === LoadingState.Loading) { return (
); + } else if (loading === LoadingState.Error) { + return ( + + + + + ); } return null; diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx index 7606d327405..b7da81a23f8 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx @@ -10,21 +10,22 @@ interface Props { children: JSX.Element; heading: string; renderToolbar?: () => JSX.Element; - toolbarItems?: EditorToolBarView[]; + toolbarItems?: EditorToolbarView[]; } -export interface EditorToolBarView { +export interface EditorToolbarView { title?: string; heading?: string; - imgSrc?: string; icon?: string; disabled?: boolean; onClick?: () => void; - render: (closeFunction?: any) => JSX.Element | JSX.Element[]; + render?: () => JSX.Element; + action?: () => void; + btnType?: 'danger'; } interface State { - openView?: EditorToolBarView; + openView?: EditorToolbarView; isOpen: boolean; fadeIn: boolean; } @@ -48,7 +49,7 @@ export class EditorTabBody extends PureComponent { this.setState({ fadeIn: true }); } - onToggleToolBarView = (item: EditorToolBarView) => { + onToggleToolBarView = (item: EditorToolbarView) => { this.setState({ openView: item, isOpen: !this.state.isOpen, @@ -74,12 +75,15 @@ export class EditorTabBody extends PureComponent { return state; } - renderButton(view: EditorToolBarView) { + renderButton(view: EditorToolbarView) { const onClick = () => { if (view.onClick) { view.onClick(); } - this.onToggleToolBarView(view); + + if (view.render) { + this.onToggleToolBarView(view); + } }; return ( @@ -91,7 +95,7 @@ export class EditorTabBody extends PureComponent { ); } - renderOpenView(view: EditorToolBarView) { + renderOpenView(view: EditorToolbarView) { return ( {view.render()} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 5df6f20fc23..84e11511453 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -16,7 +16,8 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { PanelPlugin, TimeRange } from 'app/types'; +import { PanelPlugin } from 'app/types'; +import { TimeRange } from '@grafana/ui'; export interface Props { panel: PanelModel; @@ -86,7 +87,6 @@ export class PanelChrome extends PureComponent { const { datasource, targets, transparent } = panel; const PanelComponent = plugin.exports.Panel; const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`; - return ( {({ width, height }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index a746d6c4b91..2b91e19d83c 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { QueriesTab } from './QueriesTab'; import { VisualizationTab } from './VisualizationTab'; import { GeneralTab } from './GeneralTab'; -import { AlertTab } from './AlertTab'; +import { AlertTab } from '../../alerting/AlertTab'; import config from 'app/core/config'; import { store } from 'app/store/store'; @@ -54,7 +54,7 @@ export class PanelEditor extends PureComponent { case 'queries': return ; case 'alert': - return ; + return ; case 'visualization': return ( JSX.Element; + panel: PanelModel; +} + +interface State { + editorHeight: number; +} + +export class PanelResizer extends PureComponent { + initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.4); + prevEditorHeight: number; + throttledChangeHeight: (height: number) => void; + throttledResizeDone: () => void; + noStyles: object = {}; + + constructor(props) { + super(props); + const { panel } = this.props; + + this.state = { + editorHeight: this.initialHeight, + }; + + this.throttledChangeHeight = throttle(this.changeHeight, 20, { trailing: true }); + this.throttledResizeDone = throttle(() => { + panel.resizeDone(); + }, 50); + } + + get largestHeight() { + return document.documentElement.scrollHeight * 0.9; + } + get smallestHeight() { + return 100; + } + + changeHeight = height => { + const sh = this.smallestHeight; + const lh = this.largestHeight; + height = height < sh ? sh : height; + height = height > lh ? lh : height; + + this.prevEditorHeight = this.state.editorHeight; + this.setState({ + editorHeight: height, + }); + }; + + onDrag = (evt, data) => { + const newHeight = this.state.editorHeight + data.y; + this.throttledChangeHeight(newHeight); + this.throttledResizeDone(); + }; + + render() { + const { render, isEditing } = this.props; + const { editorHeight } = this.state; + + return ( + <> + {render(isEditing ? {height: editorHeight} : this.noStyles)} + {isEditing && ( +
+ +
+
+
+ +
+ )} + + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 8513f061b74..77ab64b1dba 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,11 +1,10 @@ // Libraries -import React, { SFC, PureComponent } from 'react'; -import Remarkable from 'remarkable'; +import React, { PureComponent, SFC } from 'react'; import _ from 'lodash'; // Components -import './../../panel/metrics_tab'; -import { EditorTabBody } from './EditorTabBody'; +import 'app/features/panel/metrics_tab'; +import { EditorTabBody, EditorToolbarView} from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; @@ -14,14 +13,15 @@ import { PanelOptionSection } from './PanelOptionSection'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import config from 'app/core/config'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { DataSourceSelectItem, DataQuery } from 'app/types'; +import { DataQuery, DataSourceSelectItem } from 'app/types'; +import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; interface Props { panel: PanelModel; @@ -50,17 +50,21 @@ export class QueriesTab extends PureComponent { constructor(props) { super(props); - const { panel } = props; this.state = { - currentDS: this.datasources.find(datasource => datasource.value === panel.datasource), isLoadingHelp: false, + currentDS: this.findCurrentDataSource(), helpContent: null, isPickerOpen: false, isAddingMixed: false, }; } + findCurrentDataSource(): DataSourceSelectItem { + const { panel } = this.props; + return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; + } + getAngularQueryComponentScope(): AngularQueryComponentScope { const { panel, dashboard } = this.props; @@ -128,43 +132,13 @@ export class QueriesTab extends PureComponent { }); }; - loadHelp = () => { - const { currentDS } = this.state; - const hasHelp = currentDS.meta.hasQueryHelp; - - if (hasHelp) { - this.setState({ - helpContent:

Loading help...

, - isLoadingHelp: true, - }); - - this.backendSrv - .get(`/api/plugins/${currentDS.meta.id}/markdown/query_help`) - .then(res => { - const md = new Remarkable(); - const helpHtml = md.render(res); - this.setState({ - helpContent:
, - isLoadingHelp: false, - }); - }) - .catch(() => { - this.setState({ - helpContent:

'Error occured when loading help'

, - isLoadingHelp: false, - }); - }); - } - }; - renderQueryInspector = () => { const { panel } = this.props; return ; }; renderHelp = () => { - const { helpContent, isLoadingHelp } = this.state; - return isLoadingHelp ? : helpContent; + return ; }; onAddQuery = (query?: Partial) => { @@ -233,18 +207,15 @@ export class QueriesTab extends PureComponent { render() { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; - const { hasQueryHelp } = currentDS.meta; - const queryInspector = { + const queryInspector: EditorToolbarView = { title: 'Query Inspector', render: this.renderQueryInspector, }; - const dsHelp = { + const dsHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', - disabled: !hasQueryHelp, - onClick: this.loadHelp, render: this.renderHelp, }; diff --git a/public/app/features/dashboard/dashgrid/QueryOptions.tsx b/public/app/features/dashboard/dashgrid/QueryOptions.tsx index c6d5fecb6d4..fad70d92990 100644 --- a/public/app/features/dashboard/dashgrid/QueryOptions.tsx +++ b/public/app/features/dashboard/dashgrid/QueryOptions.tsx @@ -10,6 +10,7 @@ import { Input } from 'app/core/components/Form'; import { EventsWithValidation } from 'app/core/components/Form/Input'; import { InputStatus } from 'app/core/components/Form/Input'; import DataSourceOption from './DataSourceOption'; +import { GfFormLabel } from '@grafana/ui'; // Types import { PanelModel } from '../panel_model'; @@ -38,7 +39,33 @@ interface Props { datasource: DataSourceSelectItem; } -export class QueryOptions extends PureComponent { +interface State { + relativeTime: string; + timeShift: string; +} + +export class QueryOptions extends PureComponent { + constructor(props) { + super(props); + + this.state = { + relativeTime: props.panel.timeFrom || '', + timeShift: props.panel.timeShift || '', + }; + } + + onRelativeTimeChange = event => { + this.setState({ + relativeTime: event.target.value, + }); + }; + + onTimeShiftChange = event => { + this.setState({ + timeShift: event.target.value, + }); + }; + onOverrideTime = (evt, status: InputStatus) => { const { value } = evt.target; const { panel } = this.props; @@ -128,21 +155,25 @@ export class QueryOptions extends PureComponent { }); } - render = () => { + render() { const hideTimeOverride = this.props.panel.hideTimeOverride; + const { relativeTime, timeShift } = this.state; + return (
{this.renderOptions()}
- Relative time + Relative time
@@ -152,9 +183,11 @@ export class QueryOptions extends PureComponent { type="text" className="width-6" placeholder="1h" + onChange={this.onTimeShiftChange} onBlur={this.onTimeShift} validationEvents={timeRangeValidationEvents} hideErrorMessage={true} + value={timeShift} />
@@ -163,5 +196,5 @@ export class QueryOptions extends PureComponent {
); - }; + } } diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx index 0e31d7cdafc..bc7102f35dd 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx @@ -2,11 +2,12 @@ import React, { PureComponent } from 'react'; // Utils & Services -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; // Components -import { EditorTabBody } from './EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; +import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { PanelOptionSection } from './PanelOptionSection'; @@ -105,6 +106,7 @@ export class VisualizationTab extends PureComponent { } const panelCtrl = scope.$$childHead.ctrl; + panelCtrl.initEditMode(); let template = ''; for (let i = 0; i < panelCtrl.editorTabs.length; i++) { @@ -198,12 +200,20 @@ export class VisualizationTab extends PureComponent { } }; + renderHelp = () => ; + render() { const { plugin } = this.props; const { isVizPickerOpen, searchQuery } = this.state; + const pluginHelp: EditorToolbarView = { + heading: 'Help', + icon: 'fa fa-question', + render: this.renderHelp, + }; + return ( - + <> { const link = scope.link; + const dashboard = scope.dashboard; + let template = '
' + ' { backendSrv.get('/api/snapshot/shared-options').then(options => { - $scope.externalUrl = options['externalSnapshotURL']; $scope.sharingButtonText = options['externalSnapshotName']; $scope.externalEnabled = options['externalEnabled']; }); @@ -61,30 +60,14 @@ export class ShareSnapshotCtrl { dashboard: dash, name: dash.title, expires: $scope.snapshot.expires, + external: external, }; - const postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; - - backendSrv.post(postUrl, cmdData).then( + backendSrv.post($scope.apiUrl, cmdData).then( results => { $scope.loading = false; - - if (external) { - $scope.deleteUrl = results.deleteUrl; - $scope.snapshotUrl = results.url; - $scope.saveExternalSnapshotRef(cmdData, results); - } else { - const url = $location.url(); - let baseUrl = $location.absUrl(); - - if (url !== '/') { - baseUrl = baseUrl.replace(url, '') + '/'; - } - - $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key; - $scope.deleteUrl = baseUrl + 'api/snapshots-delete/' + results.deleteKey; - } - + $scope.deleteUrl = results.deleteUrl; + $scope.snapshotUrl = results.url; $scope.step = 2; }, () => { @@ -161,14 +144,6 @@ export class ShareSnapshotCtrl { $scope.step = 3; }); }; - - $scope.saveExternalSnapshotRef = (cmdData, results) => { - // save external in local instance as well - cmdData.external = true; - cmdData.key = results.key; - cmdData.deleteKey = results.deleteKey; - backendSrv.post('/api/snapshots/', cmdData); - }; } } diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index bc35ff31ff0..4dcf0a925b7 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -67,7 +67,7 @@ export function updateDashboardPermission( const updated = toUpdateItem(item); - // if this is the item we want to update, update it's permisssion + // if this is the item we want to update, update it's permission if (itemToUpdate === item) { updated.permission = level; } diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index d7cee33e6c3..5d0f200d862 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -20,7 +20,7 @@
- +
diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index ac717de15c9..b4d18e0279a 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -6,9 +6,9 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; import coreModule from 'app/core/core_module'; import * as dateMath from 'app/core/utils/datemath'; -// Types -import { TimeRange } from 'app/types'; +// Types +import { TimeRange } from '@grafana/ui'; export class TimeSrv { time: any; diff --git a/public/app/features/dashboard/timepicker/timepicker.html b/public/app/features/dashboard/timepicker/timepicker.html index 3e38881d480..168d2036a7f 100644 --- a/public/app/features/dashboard/timepicker/timepicker.html +++ b/public/app/features/dashboard/timepicker/timepicker.html @@ -24,60 +24,67 @@
-
-

Custom range

- - -
-
- -
-
- -
+
+
+ Quick ranges
- -
- - -
-
- -
-
- -
+
+
+ Custom range
- -
- -
- - -
-
- + + +
+
+ +
+
+ +
-
- -
-
- -
-

Quick ranges

-
+
+ +
+ + + +
+
+ +
+
+ +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 07ce54108f3..190451671ad 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -4,7 +4,7 @@ import { store } from 'app/store/store'; import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; import { PanelModel } from 'app/features/dashboard/panel_model'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelMenuItem } from 'app/types/panel'; +import { PanelMenuItem } from '@grafana/ui'; export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { const onViewPanel = () => { diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 0b0f127e2aa..f7ed0efd910 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -4,7 +4,7 @@ import store from 'app/core/store'; // Models import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelModel } from 'app/features/dashboard/panel_model'; -import { TimeRange } from 'app/types/series'; +import { TimeRange } from '@grafana/ui'; // Utils import { isString as _isString } from 'lodash'; diff --git a/public/app/features/datasources/DataSourcesList.tsx b/public/app/features/datasources/DataSourcesList.tsx index 904ed0cf679..0895b92461b 100644 --- a/public/app/features/datasources/DataSourcesList.tsx +++ b/public/app/features/datasources/DataSourcesList.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import classNames from 'classnames/bind'; +import classNames from 'classnames'; import DataSourcesListItem from './DataSourcesListItem'; import { DataSource } from 'app/types'; import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 569e0909c4d..120e002ac68 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -16,7 +16,7 @@ const BasicSettings: SFC = ({ dataSourceName, isDefault, onDefaultChange,
diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 10f3faa1267..5d64dde28ce 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -8,7 +8,7 @@ import 'vendor/flot/jquery.flot.time'; import 'vendor/flot/jquery.flot.selection'; import 'vendor/flot/jquery.flot.stack'; -import { RawTimeRange } from 'app/types/series'; +import { RawTimeRange } from '@grafana/ui'; import * as dateMath from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 8aa1789017e..7675fb13152 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,4 +1,3 @@ -import _ from 'lodash'; import React, { PureComponent } from 'react'; import classnames from 'classnames'; diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 2119f5a96b7..1a384cf011d 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -4,7 +4,7 @@ import Highlighter from 'react-highlight-words'; import classnames from 'classnames'; import * as rangeUtil from 'app/core/utils/rangeutil'; -import { RawTimeRange } from 'app/types/series'; +import { RawTimeRange } from '@grafana/ui'; import { LogsDedupDescription, LogsDedupStrategy, diff --git a/public/app/features/explore/PlaceholdersBuffer.ts b/public/app/features/explore/PlaceholdersBuffer.ts index 9a0db18ef04..461331daab4 100644 --- a/public/app/features/explore/PlaceholdersBuffer.ts +++ b/public/app/features/explore/PlaceholdersBuffer.ts @@ -88,7 +88,7 @@ export default class PlaceholdersBuffer { orders.push({ index: parts.length - 1, order }); textOffset += part.length + match.length; } - // Ensures string serialisation still works if no placeholders were parsed + // Ensures string serialization still works if no placeholders were parsed // and also accounts for the remainder of text with placeholders parts.push(text.slice(textOffset)); return { diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index 7ad659ec784..ce0a8a6e03e 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -3,7 +3,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa import { Emitter } from 'app/core/utils/emitter'; import { getIntervals } from 'app/core/utils/explore'; import { DataQuery } from 'app/types'; -import { RawTimeRange } from 'app/types/series'; +import { RawTimeRange } from '@grafana/ui'; import { getTimeSrv } from 'app/features/dashboard/time_srv'; import 'app/features/plugins/plugin_loader'; diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index d5cba981951..24b8b8f5b16 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom'; import { Change, Value } from 'slate'; import { Editor } from 'slate-react'; import Plain from 'slate-plain-serializer'; +import classnames from 'classnames'; import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore'; @@ -30,6 +31,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean { export interface QueryFieldProps { additionalPlugins?: any[]; cleanText?: (text: string) => string; + disabled?: boolean; initialQuery: string | null; onBlur?: () => void; onFocus?: () => void; @@ -78,7 +80,7 @@ export class QueryField extends React.PureComponent p); + this.plugins = [ClearPlugin(), NewlinePlugin(), ...(props.additionalPlugins || [])].filter(p => p); this.state = { suggestions: [], @@ -440,12 +442,17 @@ export class QueryField extends React.PureComponent +
{this.renderMenu()} qt.hints && qt.hints.length > 0); diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index e27909ff6f2..8476c6b2b27 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -3,7 +3,7 @@ import moment from 'moment'; import * as dateMath from 'app/core/utils/datemath'; import * as rangeUtil from 'app/core/utils/rangeutil'; -import { RawTimeRange, TimeRange } from 'app/types/series'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export const DEFAULT_RANGE = { @@ -232,60 +232,60 @@ export default class TimePicker extends PureComponent -
-

Custom range

- - -
-
- -
+
+
+ Quick ranges
- - -
-
- -
-
- - {/* -
-
- -
-
*/} -
- +
+ {Object.keys(timeOptions).map(section => { + const group = timeOptions[section]; + return ( + + ); + })}
-
-

Quick ranges

- {Object.keys(timeOptions).map(section => { - const group = timeOptions[section]; - return ( - - ); - })} +
+
+ Custom range +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ +
+
); diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 721527fbebe..6d6c60c0553 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -55,7 +55,7 @@ class TypeaheadItem extends React.PureComponent { interface TypeaheadGroupProps { items: CompletionItem[]; label: string; - onClickItem: (CompletionItem) => void; + onClickItem: (suggestion: CompletionItem) => void; selected: CompletionItem; prefix?: string; } diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index cd02915e586..a7adc71e2d8 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -112,7 +112,7 @@ export function updateFolderPermission(itemToUpdate: DashboardAcl, level: Permis const updated = toUpdateItem(item); - // if this is the item we want to update, update it's permisssion + // if this is the item we want to update, update it's permission if (itemToUpdate === item) { updated.permission = level; } diff --git a/public/app/features/manage-dashboards/SnapshotListCtrl.ts b/public/app/features/manage-dashboards/SnapshotListCtrl.ts index 2ff53e7aed5..4d6dc006d47 100644 --- a/public/app/features/manage-dashboards/SnapshotListCtrl.ts +++ b/public/app/features/manage-dashboards/SnapshotListCtrl.ts @@ -5,10 +5,14 @@ export class SnapshotListCtrl { snapshots: any; /** @ngInject */ - constructor(private $rootScope, private backendSrv, navModelSrv) { + constructor(private $rootScope, private backendSrv, navModelSrv, private $location) { this.navModel = navModelSrv.getNav('dashboards', 'snapshots', 0); this.backendSrv.get('/api/dashboard/snapshots').then(result => { - this.snapshots = result; + const baseUrl = this.$location.absUrl().replace($location.url(), ''); + this.snapshots = result.map(snapshot => ({ + ...snapshot, + url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`, + })); }); } diff --git a/public/app/features/manage-dashboards/partials/snapshot_list.html b/public/app/features/manage-dashboards/partials/snapshot_list.html index 8775b527ae1..f646194088d 100644 --- a/public/app/features/manage-dashboards/partials/snapshot_list.html +++ b/public/app/features/manage-dashboards/partials/snapshot_list.html @@ -6,17 +6,21 @@ Name Snapshot url + - {{snapshot.name}} + {{snapshot.name}} - dashboard/snapshot/{{snapshot.key}} + {{snapshot.url}} + + + External - + View diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts index aeec8297ea6..52793698a45 100644 --- a/public/app/features/org/state/actions.ts +++ b/public/app/features/org/state/actions.ts @@ -5,7 +5,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; type ThunkResult = ThunkAction; export enum ActionTypes { - LoadOrganization = 'LOAD_ORGANISATION', + LoadOrganization = 'LOAD_ORGANIZATION', SetOrganizationName = 'SET_ORGANIZATION_NAME', } @@ -19,9 +19,9 @@ interface SetOrganizationNameAction { payload: string; } -const organisationLoaded = (organisation: Organization) => ({ +const organizationLoaded = (organization: Organization) => ({ type: ActionTypes.LoadOrganization, - payload: organisation, + payload: organization, }); export const setOrganizationName = (orgName: string) => ({ @@ -33,10 +33,10 @@ export type Action = LoadOrganizationAction | SetOrganizationNameAction; export function loadOrganization(): ThunkResult { return async dispatch => { - const organisationResponse = await getBackendSrv().get('/api/org'); - dispatch(organisationLoaded(organisationResponse)); + const organizationResponse = await getBackendSrv().get('/api/org'); + dispatch(organizationLoaded(organizationResponse)); - return organisationResponse; + return organizationResponse; }; } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 6754d27bd64..5557b477b8f 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -30,8 +30,6 @@ class MetricsPanelCtrl extends PanelCtrl { constructor($scope, $injector) { super($scope, $injector); - // make metrics tab the default - this.editorTabIndex = 1; this.$q = $injector.get('$q'); this.contextSrv = $injector.get('contextSrv'); this.datasourceSrv = $injector.get('datasourceSrv'); @@ -90,7 +88,7 @@ class MetricsPanelCtrl extends PanelCtrl { .then(this.issueQueries.bind(this)) .then(this.handleQueryResult.bind(this)) .catch(err => { - // if cancelled keep loading set to true + // if canceled keep loading set to true if (err.cancelled) { console.log('Panel request cancelled', err); return; diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 70b3571e30d..74418484e3a 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,6 +1,3 @@ -// Libraries -import _ from 'lodash'; - // Services & utils import coreModule from 'app/core/core_module'; import { Emitter } from 'app/core/utils/emitter'; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 86f80b114e3..432d22fecdf 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -18,7 +18,6 @@ export class PanelCtrl { panel: any; error: any; dashboard: any; - editorTabIndex: number; pluginName: string; pluginId: string; editorTabs: any; @@ -39,7 +38,7 @@ export class PanelCtrl { this.$location = $injector.get('$location'); this.$scope = $scope; this.$timeout = $injector.get('$timeout'); - this.editorTabIndex = 0; + this.editorTabs = []; this.events = this.panel.events; this.timing = {}; @@ -90,10 +89,10 @@ export class PanelCtrl { } initEditMode() { - this.editorTabs = []; - - this.editModeInitiated = true; - this.events.emit('init-edit-mode', null); + if (!this.editModeInitiated) { + this.editModeInitiated = true; + this.events.emit('init-edit-mode', null); + } } addEditorTab(title, directiveFn, index?, icon?) { @@ -199,11 +198,10 @@ export class PanelCtrl { } calculatePanelHeight() { - if (this.panel.fullscreen) { - const docHeight = $('.react-grid-layout').height(); - const editHeight = Math.floor(docHeight * 0.35); - const fullscreenHeight = Math.floor(docHeight * 0.8); - this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight; + if (this.panel.isEditing) { + this.containerHeight = $('.panel-wrapper--edit').height(); + } else if (this.panel.fullscreen) { + this.containerHeight = $('.panel-wrapper--view').height(); } else { this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN; } @@ -212,11 +210,6 @@ export class PanelCtrl { this.containerHeight = $(window).height(); } - // hacky solution - if (this.panel.isEditing && !this.editModeInitiated) { - this.initEditMode(); - } - this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT); } diff --git a/public/app/features/panel/partials/general_tab.html b/public/app/features/panel/partials/general_tab.html index 7a4f1f7e17f..d6c2d4804a0 100644 --- a/public/app/features/panel/partials/general_tab.html +++ b/public/app/features/panel/partials/general_tab.html @@ -22,7 +22,7 @@
- Repat + Repeat
@@ -42,7 +42,7 @@
-
Drildown Links
+
Drilldown Links
diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx index 0074839e754..fd490fcfe1e 100644 --- a/public/app/features/plugins/PluginList.tsx +++ b/public/app/features/plugins/PluginList.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import classNames from 'classnames/bind'; +import classNames from 'classnames'; import PluginListItem from './PluginListItem'; import { Plugin } from 'app/types'; import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index 358adb112b3..b922840cb43 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -70,7 +70,7 @@ export const getMockPlugin = () => { url: 'url/to/GrafanaLabs', }, description: 'pretty decent plugin', - links: ['one link'], + links: [{ name: 'project', url: 'one link' }], logos: { small: 'small/logo', large: 'large/logo' }, screenshots: [{ path: `screenshot` }], updated: '2018-09-26', diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 8e0958f6c1b..3c4fa29382d 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -26,16 +26,10 @@ import * as ticks from 'app/core/utils/ticks'; import impressionSrv from 'app/core/services/impression_srv'; import builtInPlugins from './built_in_plugins'; import * as d3 from 'd3'; +import * as grafanaUI from '@grafana/ui'; // rxjs -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; - -// these imports add functions to Observable -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/combineAll'; +import { Observable, Subject } from 'rxjs'; // add cache busting const bust = `?_cache=${Date.now()}`; @@ -71,6 +65,7 @@ function exposeToPlugin(name: string, component: any) { }); } +exposeToPlugin('@grafana/ui', grafanaUI); exposeToPlugin('lodash', _); exposeToPlugin('moment', moment); exposeToPlugin('jquery', jquery); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index d8e12e338e9..d1551d6baa6 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { DeleteButton } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { NavModel, Team } from '../../types'; @@ -58,7 +58,7 @@ export class TeamList extends PureComponent { {team.memberCount} - this.deleteTeam(team)} /> + this.deleteTeam(team)} /> ); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 0e20f4be664..a25f1786a5b 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker } from 'app/core/components/Select/UserPicker'; -import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { DeleteButton } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TeamMember, User } from 'app/types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; @@ -76,7 +76,7 @@ export class TeamMembers extends PureComponent { {member.email} {syncEnabled && this.renderLabels(member.labels)} - this.onRemoveMember(member)} /> + this.onRemoveMember(member)} /> ); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index 73f081d496a..ae94691df0e 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -124,7 +124,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -174,7 +174,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -224,7 +224,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -274,7 +274,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -324,7 +324,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index d0a88bd97b0..5ebddb36d48 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -204,7 +204,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -229,7 +229,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -254,7 +254,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -279,7 +279,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -304,7 +304,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -441,7 +441,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -482,7 +482,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -523,7 +523,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -564,7 +564,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -605,7 +605,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index bda5b6aa577..0df6209bfdc 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -77,8 +77,8 @@ describe('VariableSrv init', function(this: any) { { name: 'apps', type: type, - current: { text: 'test', value: 'test' }, - options: [{ text: 'test', value: 'test' }], + current: { text: 'Test', value: 'test' }, + options: [{ text: 'Test', value: 'test' }], }, ]; scenario.urlParams['var-apps'] = 'new'; @@ -161,11 +161,11 @@ describe('VariableSrv init', function(this: any) { name: 'apps', type: 'query', multi: true, - current: { text: 'val1', value: 'val1' }, + current: { text: 'Val1', value: 'val1' }, options: [ - { text: 'val1', value: 'val1' }, - { text: 'val2', value: 'val2' }, - { text: 'val3', value: 'val3', selected: true }, + { text: 'Val1', value: 'val1' }, + { text: 'Val2', value: 'val2' }, + { text: 'Val3', value: 'val3', selected: true }, ], }, ]; @@ -177,7 +177,7 @@ describe('VariableSrv init', function(this: any) { expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); expect(variable.current.value[1]).toBe('val1'); - expect(variable.current.text).toBe('val2 + val1'); + expect(variable.current.text).toBe('Val2 + Val1'); expect(variable.options[0].selected).toBe(true); expect(variable.options[1].selected).toBe(true); }); @@ -188,6 +188,30 @@ describe('VariableSrv init', function(this: any) { }); }); + describeInitScenario( + 'when template variable is present in url multiple times and variables have no text', + scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: 'query', + multi: true, + }, + ]; + scenario.urlParams['var-apps'] = ['val1', 'val2']; + }); + + it('should display concatenated values in text', () => { + const variable = ctx.variableSrv.variables[0]; + expect(variable.current.value.length).toBe(2); + expect(variable.current.value[0]).toBe('val1'); + expect(variable.current.value[1]).toBe('val2'); + expect(variable.current.text).toBe('val1 + val2'); + }); + } + ); + describeInitScenario('when template variable is present in url multiple times using key/values', scenario => { scenario.setup(() => { scenario.variables = [ diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index a676f9c2848..bc0362f0678 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -237,8 +237,10 @@ export class VariableSrv { setOptionAsCurrent(variable, option) { variable.current = _.cloneDeep(option); - if (_.isArray(variable.current.text)) { + if (_.isArray(variable.current.text) && variable.current.text.length > 0) { variable.current.text = variable.current.text.join(' + '); + } else if (_.isArray(variable.current.value) && variable.current.value[0] !== '$__all') { + variable.current.text = variable.current.value.join(' + '); } this.selectOptionsForCurrentValue(variable); diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 82fae2ed059..28ed4754d01 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import classNames from 'classnames/bind'; +import classNames from 'classnames'; import { setUsersSearchQuery } from './state/actions'; import { getInviteesCount, getUsersSearchQuery } from './state/selectors'; diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index cf9b95882bc..8f1904dbc55 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -308,7 +308,7 @@ export default class InfluxDatasource { return 'now()'; } - const parts = /^now-(\d+)([d|h|m|s])$/.exec(date); + const parts = /^now-(\d+)([dhms])$/.exec(date); if (parts) { const amount = parseInt(parts[1], 10); const unit = parts[2]; diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 60eac1d3f2b..3ee9d703c54 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -256,6 +256,10 @@ export default class InfluxQuery { query += ' SLIMIT ' + target.slimit; } + if (target.tz) { + query += " tz('" + target.tz + "')"; + } + return query; } diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index e1c35d77115..658cf18daee 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -119,6 +119,16 @@
+
+
+ + +
+
+
+
+
+
diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index f531fe6c4d9..57460ec4d2a 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -100,6 +100,9 @@ export class InfluxQueryCtrl extends QueryCtrl { if (!this.target.slimit) { options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' })); } + if (!this.target.tz) { + options.push(this.uiSegmentSrv.newSegment({ value: 'tz' })); + } if (this.target.orderByTime === 'ASC') { options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' })); } @@ -124,6 +127,10 @@ export class InfluxQueryCtrl extends QueryCtrl { this.target.slimit = 10; break; } + case 'tz': { + this.target.tz = 'UTC'; + break; + } case 'ORDER BY time DESC': { this.target.orderByTime = 'DESC'; break; diff --git a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx index 01c6519916d..49f6b74e8b6 100644 --- a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx +++ b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx @@ -2,14 +2,23 @@ import React from 'react'; const CHEAT_SHEET_ITEMS = [ { - title: 'Logs From a Job', + title: 'See your logs', + label: 'Start by selecting a log stream from the Log labels selector.', + }, + { + title: 'Logs from a "job"', expression: '{job="default/prometheus"}', label: 'Returns all log lines emitted by instances of this job.', }, { - title: 'Search For Text', - expression: '{app="cassandra"} Maximum memory usage', - label: 'Returns all log lines for the selector and highlights the given text in the results.', + title: 'Combine stream selectors', + expression: '{app="cassandra",namespace="prod"}', + label: 'Returns all log lines from streams that have both labels.', + }, + { + title: 'Search for text', + expression: '{app="cassandra"} (duration|latency)\\s*(=|is|of)\\s*[\\d\\.]+', + label: 'Add a regular expression after the selector to filter for.', }, ]; @@ -19,12 +28,14 @@ export default (props: any) => ( {CHEAT_SHEET_ITEMS.map(item => (
{item.title}
-
props.onClickExample({ refId: '1', expr: item.expression })} - > - {item.expression} -
+ {item.expression && ( +
props.onClickExample({ refId: '1', expr: item.expression })} + > + {item.expression} +
+ )}
{item.label}
))} diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 005706bb8d1..e05d2a998b4 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -1,4 +1,3 @@ -import _ from 'lodash'; import React from 'react'; import Cascader from 'rc-cascader'; import PluginPrism from 'slate-prism'; @@ -15,6 +14,16 @@ import { DataQuery } from 'app/types'; const PRISM_SYNTAX = 'promql'; +function getChooserText(hasSytax, hasLogLabels) { + if (!hasSytax) { + return 'Loading labels...'; + } + if (!hasLogLabels) { + return '(No labels found)'; + } + return 'Log labels'; +} + export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string { // Modify suggestion based on context switch (typeaheadContext) { @@ -67,7 +76,10 @@ interface LokiQueryFieldState { class LokiQueryField extends React.PureComponent { plugins: any[]; + pluginsSearch: any[]; languageProvider: any; + modifiedSearch: string; + modifiedQuery: string; constructor(props: LokiQueryFieldProps, context) { super(props, context); @@ -85,6 +97,8 @@ class LokiQueryField extends React.PureComponent 0; + const chooserText = getChooserText(syntaxLoaded, hasLogLabels); return (
@@ -208,7 +223,7 @@ class LokiQueryField extends React.PureComponent diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index ddb4d6ed549..b7f67ffc0e7 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1,10 +1,39 @@ import LokiDatasource from './datasource'; describe('LokiDatasource', () => { - const instanceSettings = { + const instanceSettings: any = { url: 'myloggingurl', }; + describe('when querying', () => { + const backendSrvMock = { datasourceRequest: jest.fn() }; + + const templateSrvMock = { + getAdhocFilters: () => [], + replace: a => a, + }; + + const range = { from: 'now-6h', to: 'now' }; + + test('should use default max lines when no limit given', () => { + const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); + backendSrvMock.datasourceRequest = jest.fn(); + ds.query({ range, targets: [{ expr: 'foo' }] }); + expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); + expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000'); + }); + + test('should use custom max lines if limit is set', () => { + const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; + const customSettings = { ...instanceSettings, jsonData: customData }; + const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock); + backendSrvMock.datasourceRequest = jest.fn(); + ds.query({ range, targets: [{ expr: 'foo' }] }); + expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); + expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20'); + }); + }); + describe('when performing testDataSource', () => { let ds; let result; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index ebbe6bb4b56..eb33c7ef285 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -9,11 +9,11 @@ import LanguageProvider from './language_provider'; import { mergeStreamsToLogs } from './result_transformer'; import { formatQuery, parseQuery } from './query_utils'; -export const DEFAULT_LIMIT = 1000; +export const DEFAULT_MAX_LINES = 1000; const DEFAULT_QUERY_PARAMS = { direction: 'BACKWARD', - limit: DEFAULT_LIMIT, + limit: DEFAULT_MAX_LINES, regexp: '', query: '', }; @@ -29,10 +29,13 @@ function serializeParams(data: any) { export default class LokiDatasource { languageProvider: LanguageProvider; + maxLines: number; /** @ngInject */ constructor(private instanceSettings, private backendSrv, private templateSrv) { this.languageProvider = new LanguageProvider(this); + const settingsData = instanceSettings.jsonData || {}; + this.maxLines = parseInt(settingsData.maxLines, 10) || DEFAULT_MAX_LINES; } _request(apiUrl: string, data?, options?: any) { @@ -47,7 +50,7 @@ export default class LokiDatasource { } mergeStreams(streams: LogsStream[], intervalMs: number): LogsModel { - const logs = mergeStreamsToLogs(streams); + const logs = mergeStreamsToLogs(streams, this.maxLines); logs.series = makeSeriesForLogs(logs.rows, intervalMs); return logs; } @@ -61,6 +64,7 @@ export default class LokiDatasource { ...parseQuery(interpolated), start, end, + limit: this.maxLines, }; } @@ -77,6 +81,9 @@ export default class LokiDatasource { return Promise.all(queries).then((results: any[]) => { // Flatten streams from multiple queries const allStreams: LogsStream[] = results.reduce((acc, response, i) => { + if (!response) { + return acc; + } const streams: LogsStream[] = response.data.streams || []; // Inject search for match highlighting const search: string = queryTargets[i].regexp; diff --git a/public/app/plugins/datasource/loki/partials/config.html b/public/app/plugins/datasource/loki/partials/config.html index 8e79cc0adcc..d209b51730a 100644 --- a/public/app/plugins/datasource/loki/partials/config.html +++ b/public/app/plugins/datasource/loki/partials/config.html @@ -1,2 +1,16 @@ - \ No newline at end of file + + +
+
+
+ Maximum lines + + + Loki queries must contain a limit of the maximum number of lines returned (default: 1000). + Increase this limit to have a bigger result set for ad-hoc analysis. + Decrease this limit if your browser becomes sluggish when displaying the log results. + +
+
+
diff --git a/public/app/plugins/datasource/loki/query_utils.ts b/public/app/plugins/datasource/loki/query_utils.ts index 795d1e2ceeb..4f246ea8e28 100644 --- a/public/app/plugins/datasource/loki/query_utils.ts +++ b/public/app/plugins/datasource/loki/query_utils.ts @@ -1,5 +1,6 @@ const selectorRegexp = /(?:^|\s){[^{]*}/g; export function parseQuery(input: string) { + input = input || ''; const match = input.match(selectorRegexp); let query = ''; let regexp = input; diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index 182292261a8..bacc96c6f41 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -35,7 +35,7 @@ describe('getLoglevel()', () => { }); describe('parseLabels()', () => { - it('returns no labels on emtpy labels string', () => { + it('returns no labels on empty labels string', () => { expect(parseLabels('')).toEqual({}); expect(parseLabels('{}')).toEqual({}); }); @@ -46,7 +46,7 @@ describe('parseLabels()', () => { }); describe('formatLabels()', () => { - it('returns no labels on emtpy label set', () => { + it('returns no labels on empty label set', () => { expect(formatLabels({})).toEqual(''); expect(formatLabels({}, 'foo')).toEqual('foo'); }); @@ -99,7 +99,7 @@ describe('mergeStreamsToLogs()', () => { entries: [ { line: 'WARN boooo', - timestamp: '1970-01-01T00:00:00Z', + ts: '1970-01-01T00:00:00Z', }, ], }; @@ -120,7 +120,7 @@ describe('mergeStreamsToLogs()', () => { entries: [ { line: 'WARN boooo', - timestamp: '1970-01-01T00:00:01Z', + ts: '1970-01-01T00:00:01Z', }, ], }; @@ -129,11 +129,11 @@ describe('mergeStreamsToLogs()', () => { entries: [ { line: 'INFO 1', - timestamp: '1970-01-01T00:00:00Z', + ts: '1970-01-01T00:00:00Z', }, { line: 'INFO 2', - timestamp: '1970-01-01T00:00:02Z', + ts: '1970-01-01T00:00:02Z', }, ], }; diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index f922595e4b8..1f86f20d6fd 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -11,7 +11,7 @@ import { LogsStreamLabels, LogsMetaKind, } from 'app/core/logs_model'; -import { DEFAULT_LIMIT } from './datasource'; +import { DEFAULT_MAX_LINES } from './datasource'; /** * Returns the log level of a log line. @@ -116,10 +116,11 @@ export function processEntry( uniqueLabels: LogsStreamLabels, search: string ): LogRow { - const { line, timestamp } = entry; + const { line } = entry; + const ts = entry.ts || entry.timestamp; // Assumes unique-ness, needs nanosec precision for timestamp - const key = `EK${timestamp}${labels}`; - const time = moment(timestamp); + const key = `EK${ts}${labels}`; + const time = moment(ts); const timeEpochMs = time.valueOf(); const timeFromNow = time.fromNow(); const timeLocal = time.format('YYYY-MM-DD HH:mm:ss'); @@ -135,11 +136,11 @@ export function processEntry( entry: line, labels: parsedLabels, searchWords: search ? [search] : [], - timestamp: timestamp, + timestamp: ts, }; } -export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT): LogsModel { +export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LINES): LogsModel { // Unique model identifier const id = streams.map(stream => stream.labels).join(); diff --git a/public/app/plugins/datasource/stackdriver/constants.ts b/public/app/plugins/datasource/stackdriver/constants.ts index b11f4a1bcb1..49ac5131f0d 100644 --- a/public/app/plugins/datasource/stackdriver/constants.ts +++ b/public/app/plugins/datasource/stackdriver/constants.ts @@ -151,32 +151,32 @@ export const aggOptions = [ { text: 'mean', value: 'REDUCE_MEAN', - valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], }, { text: 'min', value: 'REDUCE_MIN', valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], - metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED], }, { text: 'max', value: 'REDUCE_MAX', valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], - metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED], }, { text: 'sum', value: 'REDUCE_SUM', valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], - metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED], }, { text: 'std. dev.', value: 'REDUCE_STDDEV', valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], - metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED], }, { text: 'count', diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index d1545655652..b6050ebf908 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -16,7 +16,6 @@ export default class StackdriverDatasource { constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) { this.baseUrl = `/stackdriver/`; this.url = instanceSettings.url; - this.doRequest = this.doRequest; this.id = instanceSettings.id; this.projectName = instanceSettings.jsonData.defaultProject || ''; this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt'; diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index c2607964456..22477cdf9fd 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -88,7 +88,6 @@ export class StackdriverQueryCtrl extends QueryCtrl { try { jsonBody = JSON.parse(queryRes.error); } catch { - this.lastQueryError = queryRes.error; } this.lastQueryError = jsonBody.error.message; diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 1b260107587..ba6f1a8b4f4 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -60,7 +60,6 @@ class DashListCtrl extends PanelCtrl { } onInitEditMode() { - this.editorTabIndex = 1; this.modes = ['starred', 'search', 'recently viewed']; this.addEditorTab('Options', 'public/app/plugins/panel/dashlist/editor.html'); } diff --git a/public/app/plugins/panel/gauge/Threshold.test.tsx b/public/app/plugins/panel/gauge/Threshold.test.tsx index 3b2becd9859..3fa508b98a9 100644 --- a/public/app/plugins/panel/gauge/Threshold.test.tsx +++ b/public/app/plugins/panel/gauge/Threshold.test.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import Thresholds from './Thresholds'; import { defaultProps, OptionsProps } from './module'; -import { BasicGaugeColor, PanelOptionsProps } from 'app/types'; +import { BasicGaugeColor } from 'app/types'; +import { PanelOptionsProps } from '@grafana/ui'; const setup = (propOverrides?: object) => { const props: PanelOptionsProps = { diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 152e7c20b5e..dccd424b416 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -5,15 +5,8 @@ import ValueOptions from './ValueOptions'; import GaugeOptions from './GaugeOptions'; import Thresholds from './Thresholds'; import ValueMappings from './ValueMappings'; -import { - BasicGaugeColor, - NullValueMode, - PanelOptionsProps, - PanelProps, - RangeMap, - Threshold, - ValueMap, -} from 'app/types'; +import { PanelOptionsProps, PanelProps, NullValueMode } from '@grafana/ui'; +import { BasicGaugeColor, RangeMap, Threshold, ValueMap } from 'app/types'; export interface OptionsProps { baseColor: string; @@ -45,8 +38,8 @@ export const defaultProps = { showThresholdLabels: false, suffix: '', decimals: 0, - stat: '', - unit: '', + stat: 'avg', + unit: 'none', mappings: [], thresholds: [], }, diff --git a/public/app/plugins/panel/graph/README.md b/public/app/plugins/panel/graph/README.md index 2dc8682f0e3..e1184beb8e7 100644 --- a/public/app/plugins/panel/graph/README.md +++ b/public/app/plugins/panel/graph/README.md @@ -1,7 +1,7 @@ -# Graph Panel - Native Plugin +# Graph Panel -The Graph is the main graph panel and is **included** with Grafana. It provides a very rich set of graphing options. +This is the main Graph panel and is **included** with Grafana. It provides a very rich set of graphing options. -Read more about it here: +For full reference documentation: -[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/) \ No newline at end of file +[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ff248d68201..86a6fd1dfd2 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -737,7 +737,7 @@ class GraphElement { if (min && max && ticks) { const range = max - min; const secPerTick = range / ticks / 1000; - // Need have 10 milisecond margin on the day range + // Need have 10 millisecond margin on the day range // As sometimes last 24 hour dashboard evaluates to more than 86400000 const oneDay = 86400010; const oneYear = 31536000000; diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts index ed2b2dab92a..a5d7f658ccf 100644 --- a/public/app/plugins/panel/graph/jquery.flot.events.ts +++ b/public/app/plugins/panel/graph/jquery.flot.events.ts @@ -54,7 +54,7 @@ export function createEditPopover(element, event, plot) { const eventManager = plot.getOptions().events.manager; if (eventManager.editorOpen) { // update marker element to attach to (needed in case of legend on the right - // when there is a double render pass and the inital marker element is removed) + // when there is a double render pass and the initial marker element is removed) markerElementToAttachTo = element; return; } diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index be243587820..58a35ea2a5f 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -114,6 +114,7 @@ describe('grafanaGraph', () => { {} ); + // @ts-ignore $.plot = ctrl.plot = jest.fn(); scope.ctrl = ctrl; diff --git a/public/app/plugins/panel/graph2/GraphOptions.tsx b/public/app/plugins/panel/graph2/GraphOptions.tsx index e87c03da634..6bb4b2c13d5 100644 --- a/public/app/plugins/panel/graph2/GraphOptions.tsx +++ b/public/app/plugins/panel/graph2/GraphOptions.tsx @@ -6,7 +6,7 @@ import React, { PureComponent } from 'react'; import { Switch } from 'app/core/components/Switch/Switch'; // Types -import { PanelOptionsProps } from 'app/types'; +import { PanelOptionsProps } from '@grafana/ui'; import { Options } from './types'; export class GraphOptions extends PureComponent> { diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index a7ef45e5428..020c33f7d38 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -1,31 +1,23 @@ // Libraries import _ from 'lodash'; import React, { PureComponent } from 'react'; +import colors from 'app/core/utils/colors'; -// Components -import Graph from 'app/viz/Graph'; - -// Services & Utils -import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; - -// Types -import { PanelProps, NullValueMode } from 'app/types'; +// Components & Types +import { Graph, PanelProps, NullValueMode, processTimeSeries } from '@grafana/ui'; import { Options } from './types'; interface Props extends PanelProps {} export class GraphPanel extends PureComponent { - constructor(props) { - super(props); - } - render() { const { timeSeries, timeRange, width, height } = this.props; const { showLines, showBars, showPoints } = this.props.options; - const vmSeries = getTimeSeriesVMs({ + const vmSeries = processTimeSeries({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, + colorPalette: colors, }); return ( diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 8092eaaeb9a..5d44231e362 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -251,7 +251,6 @@ export class HeatmapRenderer { if (tickInterval === 0) { yMax = max * this.dataRangeWidingFactor; yMin = min - min * (this.dataRangeWidingFactor - 1); - tickInterval = (yMax - yMin) / 2; } else { yMax = Math.ceil((max + yWiding) / tickInterval) * tickInterval; yMin = Math.floor((min - yWiding) / tickInterval) * tickInterval; @@ -389,9 +388,7 @@ export class HeatmapRenderer { // Adjust data range to log base adjustLogRange(min, max, logBase) { - let yMin, yMax; - - yMin = this.data.heatmapStats.minLog; + let yMin = this.data.heatmapStats.minLog; if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) { yMin = 1; } else { @@ -399,7 +396,7 @@ export class HeatmapRenderer { } // Adjust max Y value to log base - yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase); + const yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase); return { yMin, yMax }; } diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index eeac352f799..55ca160652d 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -29,7 +29,6 @@ class PluginListCtrl extends PanelCtrl { } onInitEditMode() { - this.editorTabIndex = 1; this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html'); } diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 874691fab97..08ab4cd2b96 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -43,7 +43,6 @@ export class TextPanelCtrl extends PanelCtrl { onInitEditMode() { this.addEditorTab('Options', 'public/app/plugins/panel/text/editor.html'); - this.editorTabIndex = 1; if (this.panel.mode === 'text') { this.panel.mode = 'markdown'; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index aaca23ccbf0..68523ff0880 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from 'app/types'; +import { PanelProps } from '@grafana/ui'; export class Text2 extends PureComponent { constructor(props) { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 80d55eedb60..c2c59d35f5b 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,6 +1,7 @@ import { Value } from 'slate'; -import { DataQuery, RawTimeRange } from './series'; +import { DataQuery } from './series'; +import { RawTimeRange } from '@grafana/ui'; import TableModel from 'app/core/table_model'; import { LogsModel } from 'app/core/logs_model'; import { DataSourceSelectItem } from 'app/types/datasources'; @@ -154,11 +155,11 @@ export interface ExploreState { datasourceError: any; datasourceLoading: boolean | null; datasourceMissing: boolean; - datasourceName?: string; exploreDatasources: DataSourceSelectItem[]; graphInterval: number; // in ms graphResult?: any[]; history: HistoryItem[]; + initialDatasource?: string; initialQueries: DataQuery[]; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index f2a5f807645..ab52b03ab17 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -8,20 +8,8 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState, UserState } from './user'; import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources'; -import { - TimeRange, - LoadingState, - TimeSeries, - TimeSeriesVM, - TimeSeriesVMs, - TimeSeriesStats, - NullValueMode, - DataQuery, - DataQueryResponse, - DataQueryOptions, - IntervalValues, -} from './series'; -import { BasicGaugeColor, MappingType, PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel'; +import { DataQuery, DataQueryResponse, DataQueryOptions } from './series'; +import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins'; import { Organization, OrganizationState } from './organization'; import { @@ -68,16 +56,7 @@ export { OrgUser, User, UsersState, - TimeRange, - LoadingState, PanelPlugin, - PanelProps, - PanelOptionsProps, - TimeSeries, - TimeSeriesVM, - TimeSeriesVMs, - NullValueMode, - TimeSeriesStats, DataQuery, DataQueryResponse, DataQueryOptions, @@ -95,7 +74,6 @@ export { ValidationRule, ValueMap, RangeMap, - IntervalValues, MappingType, BasicGaugeColor, }; diff --git a/public/app/types/jquery.d.ts b/public/app/types/jquery.d.ts new file mode 100644 index 00000000000..4a6f60b6029 --- /dev/null +++ b/public/app/types/jquery.d.ts @@ -0,0 +1,17 @@ +interface JQueryPlot { + (element: HTMLElement | JQuery, data: any, options: any): void; + plugins: any[]; +} + +interface JQueryStatic { + plot: JQueryPlot; +} + +interface JQuery { + place_tt: any; + modal: any; + tagsinput: any; + typeahead: any; + accessKey: any; + tooltip: any; +} diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index af371e16573..31674d20304 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -1,34 +1,3 @@ -import { LoadingState, TimeSeries, TimeRange } from './series'; - -export interface PanelProps { - timeSeries: TimeSeries[]; - timeRange: TimeRange; - loading: LoadingState; - options: T; - renderCounter: number; - width: number; - height: number; -} - -export interface PanelOptionsProps { - options: T; - onChange: (options: T) => void; -} - -export interface PanelSize { - width: number; - height: number; -} - -export interface PanelMenuItem { - type?: 'submenu' | 'divider'; - text?: string; - iconClassName?: string; - onClick?: () => void; - shortcut?: string; - subMenu?: PanelMenuItem[]; -} - export interface Threshold { index: number; value: number; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index bc33ec80409..a1403c7a71c 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,5 +1,5 @@ import { ComponentClass } from 'react'; -import { PanelProps, PanelOptionsProps } from './panel'; +import { PanelProps, PanelOptionsProps } from '@grafana/ui'; export interface PluginExports { Datasource?: any; @@ -57,13 +57,18 @@ export interface PluginInclude { path: string; } +interface PluginMetaInfoLink { + name: string; + url: string; +} + export interface PluginMetaInfo { author: { name: string; url?: string; }; description: string; - links: string[]; + links: PluginMetaInfoLink[]; logos: { large: string; small: string; diff --git a/public/app/types/series.ts b/public/app/types/series.ts index a9585a2c842..9fe68955da5 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -1,75 +1,5 @@ -import { Moment } from 'moment'; import { PluginMeta } from './plugins'; - -export enum LoadingState { - NotStarted = 'NotStarted', - Loading = 'Loading', - Done = 'Done', - Error = 'Error', -} - -export interface RawTimeRange { - from: Moment | string; - to: Moment | string; -} - -export interface TimeRange { - from: Moment; - to: Moment; - raw: RawTimeRange; -} - -export interface IntervalValues { - interval: string; // 10s,5m - intervalMs: number; -} - -export type TimeSeriesValue = string | number | null; - -export type TimeSeriesPoints = TimeSeriesValue[][]; - -export interface TimeSeries { - target: string; - datapoints: TimeSeriesPoints; - unit?: string; -} - -/** View model projection of a time series */ -export interface TimeSeriesVM { - label: string; - color: string; - data: TimeSeriesValue[][]; - stats: TimeSeriesStats; -} - -export interface TimeSeriesStats { - total: number; - max: number; - min: number; - logmin: number; - avg: number | null; - current: number | null; - first: number | null; - delta: number; - diff: number | null; - range: number | null; - timeStep: number; - count: number; - allIsNull: boolean; - allIsZero: boolean; -} - -export enum NullValueMode { - Null = 'null', - Ignore = 'connected', - AsZero = 'null as zero', -} - -/** View model projection of many time series */ -export interface TimeSeriesVMs { - [index: number]: TimeSeriesVM; - length: number; -} +import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui'; export interface DataQueryResponse { data: TimeSeries[]; diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx new file mode 100644 index 00000000000..91107a563e5 --- /dev/null +++ b/public/app/viz/Gauge.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Gauge, Props } from './Gauge'; +import { BasicGaugeColor } from '../types'; +import { TimeSeriesVMs } from '@grafana/ui'; + +jest.mock('jquery', () => ({ + plot: jest.fn(), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + baseColor: BasicGaugeColor.Green, + maxValue: 100, + mappings: [], + minValue: 0, + prefix: '', + showThresholdMarkers: true, + showThresholdLabels: false, + suffix: '', + thresholds: [], + unit: 'none', + stat: 'avg', + height: 300, + width: 300, + timeSeries: {} as TimeSeriesVMs, + decimals: 0, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as Gauge; + + return { + instance, + wrapper, + }; +}; + +describe('Get font color', () => { + it('should get base color if no threshold', () => { + const { instance } = setup(); + + expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green); + }); + + it('should be f2f2f2', () => { + const { instance } = setup({ + thresholds: [{ value: 59, color: '#f2f2f2' }], + }); + + expect(instance.getFontColor(58)).toEqual('#f2f2f2'); + }); +}); diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 9907ddf575f..defeaf8cc8f 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -1,10 +1,11 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { BasicGaugeColor, MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types'; +import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from 'app/types'; +import { TimeSeriesVMs } from '@grafana/ui'; import config from '../core/config'; import kbn from '../core/utils/kbn'; -interface Props { +export interface Props { baseColor: string; decimals: number; height: number; @@ -95,12 +96,14 @@ export class Gauge extends PureComponent { getFontColor(value) { const { baseColor, maxValue, thresholds } = this.props; - const atThreshold = thresholds.filter(threshold => value <= threshold.value); + if (thresholds.length > 0) { + const atThreshold = thresholds.filter(threshold => value <= threshold.value); - if (atThreshold.length > 0) { - return atThreshold[0].color; - } else if (value <= maxValue) { - return BasicGaugeColor.Red; + if (atThreshold.length > 0) { + return atThreshold[0].color; + } else if (value <= maxValue) { + return BasicGaugeColor.Red; + } } return baseColor; diff --git a/public/app/viz/state/timeSeries.ts b/public/app/viz/state/timeSeries.ts index e22cb4681b7..782383957bc 100644 --- a/public/app/viz/state/timeSeries.ts +++ b/public/app/viz/state/timeSeries.ts @@ -5,7 +5,7 @@ import _ from 'lodash'; import colors from 'app/core/utils/colors'; // Types -import { TimeSeries, TimeSeriesVMs, NullValueMode } from 'app/types'; +import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui'; interface Options { timeSeries: TimeSeries[]; diff --git a/public/dashboards/home.json b/public/dashboards/home.json index ff69bb6f856..55cf7242aa6 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -31,7 +31,7 @@ "folderId": 0, "headings": true, "id": 3, - "limit": 4, + "limit": 30, "links": [], "query": "", "recent": true, diff --git a/public/emails/invited_to_org.html b/public/emails/invited_to_org.html index 30f2b34ee9a..16705e7973b 100644 --- a/public/emails/invited_to_org.html +++ b/public/emails/invited_to_org.html @@ -1,5 +1,5 @@ - + diff --git a/public/emails/new_user_invite.html b/public/emails/new_user_invite.html index b925ced7267..cef3a210136 100644 --- a/public/emails/new_user_invite.html +++ b/public/emails/new_user_invite.html @@ -1,5 +1,5 @@ - + diff --git a/public/sass/.sass-lint.yml b/public/sass/.sass-lint.yml index 2e79bc584db..1d1e2fd9c73 100644 --- a/public/sass/.sass-lint.yml +++ b/public/sass/.sass-lint.yml @@ -1,23 +1,41 @@ options: formatter: stylish -files: - include: '**/*.s+(a|c)ss' - ignore: - - './utils/*.scss' - rules: + quotes: + - 0 + - + style: 'single' + + brace-style: + - 2 + - + style: '1tbs' + + space-before-brace: + - 2 + + no-duplicate-properties: + - 0 + - + exclude: + - 'font-size' + - 'word-break' + + empty-line-between-blocks: + - 0 + - + allow-single-line-rulesets: 1 + # Extends extends-before-mixins: 0 extends-before-declarations: 0 placeholder-in-extend: 0 - # Mixins mixins-before-declarations: 0 # Line Spacing one-declaration-per-line: 0 - empty-line-between-blocks: 0 single-line-per-selector: 0 # Disallows @@ -25,7 +43,6 @@ rules: no-color-literals: 0 no-css-comments: 0 no-debug: 0 - no-duplicate-properties: 0 no-empty-rulesets: 1 no-extends: 0 no-ids: 0 @@ -40,6 +57,7 @@ rules: no-vendor-prefixes: 0 no-warn: 0 property-units: 0 + pseudo-element: 0 # Nesting force-attribute-nesting: 0 @@ -57,7 +75,6 @@ rules: # Style Guide bem-depth: 0 border-zero: 0 - brace-style: 0 clean-import-paths: 0 empty-args: 0 hex-length: 0 @@ -66,7 +83,6 @@ rules: leading-zero: 0 nesting-depth: 0 property-sort-order: 0 - quotes: 0 shorthand-values: 0 url-quotes: 0 variable-for-property: 0 @@ -76,12 +92,10 @@ rules: space-after-comma: 0 space-before-colon: 0 space-after-colon: 0 - space-before-brace: 0 space-before-bang: 0 space-after-bang: 0 space-between-parens: 0 space-around-operator: 0 # Final Items - trailing-semicolon: 0 final-newline: 0 diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index bfb186c0b02..404a212241b 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -38,6 +38,9 @@ @import 'layout/lists'; @import 'layout/page'; +// LOAD @grafana/ui components +@import '../../packages/grafana-ui/src/index'; + // COMPONENTS @import 'components/scrollbar'; @import 'components/cards'; @@ -98,12 +101,12 @@ @import 'components/form_select_box'; @import 'components/panel_editor'; @import 'components/toolbar'; -@import 'components/delete_button'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; @import 'components/thresholds'; @import 'components/toggle_button_group'; @import 'components/value-mappings'; +@import 'components/popover-box'; // PAGES @import 'pages/login'; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index cab0eb76dde..ded17e6ecfe 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -103,6 +103,7 @@ $panel-bg: #212124; $panel-border-color: $dark-1; $panel-border: solid 1px $panel-border-color; $panel-header-hover-bg: $dark-4; +$panel-corner: $panel-bg; // page header $page-header-bg: linear-gradient(90deg, #292a2d, black); @@ -302,12 +303,14 @@ $popover-error-bg: $btn-danger-bg; // Tooltips and popovers // ------------------------- $tooltipColor: $popover-help-color; -$tooltipBackground: $popover-help-bg; $tooltipArrowWidth: 5px; -$tooltipArrowColor: $tooltipBackground; $tooltipLinkColor: $link-color; $graph-tooltip-bg: $dark-1; +$tooltipBackground: $popover-help-bg; +$tooltipArrowColor: $tooltipBackground; +$tooltipBackgroundError: $brand-danger; + // images $checkboxImageUrl: '../img/checkbox.png'; @@ -400,3 +403,7 @@ $logs-color-unkown: $gray-2; $button-toggle-group-btn-active-bg: linear-gradient(90deg, $orange, $red); $button-toggle-group-btn-active-shadow: inset 0 0 4px $black; $button-toggle-group-btn-seperator-border: 1px solid $page-bg; + +$vertical-resize-handle-bg: $dark-5; +$vertical-resize-handle-dots: $gray-1; +$vertical-resize-handle-dots-hover: $gray-2; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 6ad07011b68..ec25be52676 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -101,6 +101,7 @@ $panel-bg: $white; $panel-border-color: $gray-5; $panel-border: solid 1px $panel-border-color; $panel-header-hover-bg: $gray-6; +$panel-corner: $gray-4; // Page header $page-header-bg: linear-gradient(90deg, $white, $gray-7); @@ -306,12 +307,14 @@ $popover-error-bg: $btn-danger-bg; // Tooltips and popovers // ------------------------- $tooltipColor: $popover-help-color; -$tooltipBackground: $popover-help-bg; $tooltipArrowWidth: 5px; -$tooltipArrowColor: $tooltipBackground; $tooltipLinkColor: lighten($popover-help-color, 5%); $graph-tooltip-bg: $gray-5; +$tooltipBackground: $popover-help-bg; +$tooltipArrowColor: $tooltipBackground; // Used by Angular tooltip +$tooltipBackgroundError: $brand-danger; + // images $checkboxImageUrl: '../img/checkbox_white.png'; @@ -408,3 +411,7 @@ $logs-color-unkown: $gray-5; $button-toggle-group-btn-active-bg: $brand-primary; $button-toggle-group-btn-active-shadow: inset 0 0 4px $white; $button-toggle-group-btn-seperator-border: 1px solid $gray-6; + +$vertical-resize-handle-bg: $gray-4; +$vertical-resize-handle-dots: $gray-3; +$vertical-resize-handle-dots-hover: $gray-2; diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 6568414ed88..8d9d4fc6b7d 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -5,13 +5,13 @@ $useDropShadow: false; $attachmentOffset: 0%; $easing: cubic-bezier(0, 0, 0.265, 1); -@include drop-theme("error", $popover-error-bg, $popover-color); -@include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color); -@include drop-theme("help", $popover-help-bg, $popover-help-color); +@include drop-theme('error', $popover-error-bg, $popover-color); +@include drop-theme('popover', $popover-bg, $popover-color, $popover-border-color); +@include drop-theme('help', $popover-help-bg, $popover-help-color); -@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing); -@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing); -@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale('drop', 'help', $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale('drop', 'error', $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale('drop', 'popover', $attachmentOffset: $attachmentOffset, $easing: $easing); .drop-element { z-index: 10000; diff --git a/public/sass/components/_filter-list.scss b/public/sass/components/_filter-list.scss index 7713aa05ac2..90d0a21c539 100644 --- a/public/sass/components/_filter-list.scss +++ b/public/sass/components/_filter-list.scss @@ -67,17 +67,17 @@ text-transform: uppercase; &.online { - background-image: url("/img/online.svg"); + background-image: url('/img/online.svg'); color: $online; } &.warn { - background-image: url("/img/warn-tiny.svg"); + background-image: url('/img/warn-tiny.svg'); color: $warn; } &.critical { - background-image: url("/img/critical.svg"); + background-image: url('/img/critical.svg'); color: $critical; } } diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss index 665deda0f12..39804a9c240 100644 --- a/public/sass/components/_jsontree.scss +++ b/public/sass/components/_jsontree.scss @@ -3,16 +3,20 @@ json-tree { .json-tree-key { vertical-align: middle; } + .expandable { position: relative; + &::before { pointer-events: none; } + &::before, & > .json-tree-key { cursor: pointer; } } + .json-tree-branch-preview { display: inline-block; vertical-align: middle; @@ -24,36 +28,44 @@ json-tree { ul { padding-left: $spacer; } + li, ul { list-style: none; } + li { line-height: 1.3rem; } + .json-tree-key { color: $variable; padding: 5px 10px 5px 15px; + &::after { - content: ":"; + content: ':'; } } + json-node.expandable { &::before { - content: "\25b6"; + content: '\25b6'; position: absolute; left: 0px; font-size: 8px; transition: transform 0.1s ease; } + &.expanded::before { transform: rotate(90deg); } } + .json-tree-leaf-value, .json-tree-branch-preview { word-break: break-all; } + .json-tree-branch-preview { overflow: hidden; font-style: italic; diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index 9d0ad0703dc..bfc1c4bb9b5 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -14,7 +14,7 @@ &--view { flex: 1 1 0; - height: 80%; + height: 90%; margin: 0 $dashboard-padding; padding-top: $dashboard-padding; } @@ -26,6 +26,7 @@ flex-direction: row; flex: 1 1 0; position: relative; + min-height: 0; } .panel-editor__right { @@ -36,6 +37,7 @@ margin: 0 20px 0 84px; border-radius: 3px; box-shadow: $panel-editor-shadow; + min-height: 0; } .panel-editor__close { @@ -60,6 +62,7 @@ flex-grow: 1; min-width: 0; display: flex; + min-height: 0; } .panel-editor__content { @@ -84,46 +87,34 @@ } } -.panel-editor-resizer { - position: absolute; - height: 2px; - width: 100%; - top: -23px; - text-align: center; - border-bottom: 2px dashed transparent; - - &:hover { - transition: border-color 0.2s ease-in 0.4s; - transition-delay: 0.2s; - border-color: $text-color-faint; - } +.panel-editor-container__resizer { + position: relative; + margin-top: -3px; } .panel-editor-resizer__handle { - display: inline-block; - width: 180px; position: relative; - border-radius: 2px; - height: 7px; - cursor: grabbing; - background: $input-label-bg; - top: -9px; + display: block; + background: $vertical-resize-handle-bg; + width: 150px; + margin-left: -75px; + height: 6px; + cursor: ns-resize; + border-radius: 3px; + margin: 0 auto; - &:hover { - transition: background 0.2s ease-in 0.4s; - transition-delay: 0.2s; - background: linear-gradient(90deg, $orange, $red); - .panel-editor-resizer__handle-dots { - transition: opacity 0.2s ease-in; - opacity: 0; - } + &::before { + content: ' '; + position: absolute; + left: 10px; + right: 10px; + top: 2px; + border-top: 2px dotted $vertical-resize-handle-dots; } -} -.panel-editor-resizer__handle-dots { - border-top: 2px dashed $text-color-faint; - position: relative; - top: 4px; + &:hover::before { + border-color: $vertical-resize-handle-dots-hover; + } } .viz-picker { @@ -149,7 +140,6 @@ display: flex; margin-right: 10px; margin-bottom: 10px; - //border: 1px solid transparent; align-items: center; justify-content: center; padding-bottom: 6px; @@ -184,7 +174,6 @@ } .panel-editor-tabs { - position: relative; z-index: 2; display: flex; flex-direction: column; @@ -231,7 +220,6 @@ &:hover { filter: $panel-editor-side-menu-shadow; - transform: translate(-2px, -2px); transform: scale(1.1); } } diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 1fb3eda1834..5bbc4ba29ca 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -1,4 +1,4 @@ -// Colours +// Colors $progress-color-dark: $panel-bg !default; $progress-color: $panel-bg !default; $progress-color-light: $panel-bg !default; @@ -51,8 +51,7 @@ $path-position: $marker-size-half - ($path-height / 2); min-width: $marker-size; &::after { - right: -50%; - content: ""; + content: ''; display: block; position: absolute; z-index: 1; @@ -105,7 +104,7 @@ $path-position: $marker-size-half - ($path-height / 2); // change icon to check .icon-gf::before { - content: "\e604"; + content: '\e604'; } } .progress-text { diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 9f7a9575b61..b35a600b9e1 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -368,7 +368,6 @@ font-size: $font-size-sm; position: absolute; text-align: center; - font-size: 12px; } .alert-handle-wrapper { diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 7b4244902f1..dac962057bf 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -18,7 +18,7 @@ $column-horizontal-spacing: 10px; flex-wrap: wrap; > * { - margin-right: 1em; + margin-right: $spacer*2; } } @@ -31,20 +31,21 @@ $column-horizontal-spacing: 10px; .logs-panel-meta { flex: 1; color: $text-color-weak; - margin-bottom: 10px; + margin-bottom: $spacer; min-width: 30%; display: flex; } .logs-panel-meta__item { - margin-right: 1em; + margin-right: $spacer; display: flex; + align-items: baseline; } .logs-panel-meta__label { - margin-right: 0.5em; - font-size: 0.9em; - font-weight: 500; + margin-right: $spacer / 2; + font-size: $font-size-sm; + font-weight: $font-weight-semi-bold; } .logs-panel-meta__value { @@ -59,7 +60,7 @@ $column-horizontal-spacing: 10px; .logs-rows { font-family: $font-family-monospace; - font-size: 12px; + font-size: $font-size-sm; display: table; table-layout: fixed; } diff --git a/public/sass/components/_popover-box.scss b/public/sass/components/_popover-box.scss new file mode 100644 index 00000000000..6f0b3fb4310 --- /dev/null +++ b/public/sass/components/_popover-box.scss @@ -0,0 +1,31 @@ +.popover-box { + background-color: $popover-bg; + color: $popover-color; + border: 1px solid $popover-border-color; + border-radius: $border-radius; + max-width: 500px; +} + +.popover-box__header { + background-color: $popover-border-color; + padding: 6px 10px; + display: flex; +} + +.popover-box__title { + font-weight: $font-weight-semi-bold; + padding-right: $spacer; + overflow: hidden; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; +} + +.popover-box__body { + padding: 20px 10px 10px 10px; +} + +.popover-box__close { + cursor: pointer; +} diff --git a/public/sass/components/_popper.scss b/public/sass/components/_popper.scss index d869d52b92f..afa629d4043 100644 --- a/public/sass/components/_popper.scss +++ b/public/sass/components/_popper.scss @@ -8,7 +8,22 @@ $popper-margin-from-ref: 5px; text-align: center; } -.popper .popper__arrow { +.popper__background { + background: $tooltipBackground; + border-radius: $border-radius; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + padding: 10px; + + // Themes + &.popper__background--error { + background: $tooltipBackgroundError; + .popper__arrow { + border-color: $tooltipBackgroundError; + } + } +} + +.popper__arrow { width: 0; height: 0; border-style: solid; @@ -16,17 +31,10 @@ $popper-margin-from-ref: 5px; margin: 0px; } -.popper .popper__arrow { +.popper__arrow { border-color: $tooltipBackground; } -.popper__background { - background: $tooltipBackground; - border-radius: $border-radius; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); - padding: 10px; -} - // Top .popper[data-placement^='top'] { padding-bottom: $popper-margin-from-ref; diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index b5f61872585..447ca8c12a2 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -33,9 +33,8 @@ text-align: center; margin-right: 0.3rem; padding: 3px 5px; - font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; + font: 11px Consolas, 'Liberation Mono', Menlo, Courier, monospace; line-height: 10px; - color: #555; vertical-align: middle; background-color: $btn-inverse-bg; border: solid 1px $btn-inverse-bg-hl; diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 5fdb1a5e32e..237574b93bc 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -29,12 +29,12 @@ .sidemenu-open { .sidemenu { background: $side-menu-bg; - position: initial; height: auto; box-shadow: $side-menu-shadow; position: relative; z-index: $zindex-sidemenu; } + .sidemenu__top, .sidemenu__bottom { display: block; @@ -212,7 +212,6 @@ li.sidemenu-org-switcher { .sidemenu { width: 100%; background: $side-menu-bg-mobile; - position: initial; height: auto; box-shadow: $side-menu-shadow; position: relative; diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index 25b20f180ef..200f102d69a 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -5,7 +5,7 @@ word-break: break-word; } -.slate-query-field-wrapper { +.slate-query-field__wrapper { position: relative; display: inline-block; padding: 6px 7px 4px; @@ -20,6 +20,10 @@ transition: all 0.3s; } +.slate-query-field__wrapper--disabled { + background-color: inherit; +} + .slate-typeahead { .typeahead { position: absolute; @@ -28,7 +32,6 @@ left: -10000px; opacity: 0; border-radius: $border-radius; - transition: opacity 0.75s; border: $panel-border; max-height: calc(66vh); overflow-y: scroll; diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index a6844b9e66b..d4be5264d9d 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -22,10 +22,8 @@ } .submenu-item { - margin-right: 20px; display: inline-block; margin-right: 15px; - display: inline-block; float: left; .fa-caret-down { @@ -42,7 +40,6 @@ background-color: $input-bg; border: 1px solid $input-border-color; border-radius: $input-border-radius; - color: $input-color; box-sizing: content-box; display: inline-block; color: $text-color; diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index ab100a8c752..92756f81a76 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -19,15 +19,14 @@ gf-form-switch[disabled] { } .gf-form-switch { + display: flex; position: relative; - display: inline-block; width: 60px; height: $gf-form-input-height; background: $switch-bg; border: 1px solid $input-border-color; border-left: none; border-radius: $input-border-radius; - display: flex; align-items: center; justify-content: center; @@ -77,14 +76,13 @@ input:checked + .gf-form-switch__slider::before { .gf-form-checkbox { position: relative; - display: inline-block; + display: flex; width: 50px; height: $gf-form-input-height; background: $switch-bg; border: 1px solid $input-border-color; border-left: none; border-radius: $input-border-radius; - display: flex; align-items: center; justify-content: center; diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index ef21d54f71c..a47d9d65ceb 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -31,7 +31,6 @@ .tabbed-view-close-btn { float: right; - padding: 0; margin: 0; background-color: transparent; border: none; diff --git a/public/sass/components/_tags.scss b/public/sass/components/_tags.scss index 014d9f0be1e..692259facb3 100644 --- a/public/sass/components/_tags.scss +++ b/public/sass/components/_tags.scss @@ -25,6 +25,7 @@ border-width: 1px; border-style: solid; box-shadow: 0 0 1px rgba($white, 0.2); + .icon-tag { position: relative; top: 1px; diff --git a/public/sass/components/_tagsinput.scss b/public/sass/components/_tagsinput.scss index f4f0ed4c84d..e8cf9ea44e9 100644 --- a/public/sass/components/_tagsinput.scss +++ b/public/sass/components/_tagsinput.scss @@ -24,12 +24,15 @@ [data-role='remove'] { margin-left: 8px; cursor: pointer; + &::after { content: 'x'; padding: 0px 2px; } + &:hover { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + &:active { box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } diff --git a/public/sass/components/_thresholds.scss b/public/sass/components/_thresholds.scss index dbcc9951ff7..ff89a6b6ea6 100644 --- a/public/sass/components/_thresholds.scss +++ b/public/sass/components/_thresholds.scss @@ -66,9 +66,6 @@ padding: 5px 8px; } -.threshold-row-base { -} - .threshold-row-remove { display: flex; align-items: center; diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 51a730926ba..6f075c4d92e 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -13,23 +13,33 @@ } .gf-timepicker-dropdown { - position: absolute; - top: $navbarHeight; - right: 0; - padding: 10px 20px; background-color: $page-bg; border-radius: 0 0 0 4px; box-shadow: $search-shadow; z-index: $zindex-dropdown; -} + display: flex; + flex-direction: column; + position: absolute; + left: 20px; + right: 20px; + top: $navbarHeight; -.gf-timepicker-absolute-section { - width: 290px; - float: left; - padding: 0 10px; - select { - width: 183px; - margin-bottom: 0; + @include media-breakpoint-up(md) { + left: auto; + width: 550px; + } + + .popover-box { + max-width: 100%; + + &:first-child { + border-radius: $border-radius $border-radius 0 0; + border-bottom: 0; + } + + &:last-child { + border-radius: 0 0 $border-radius $border-radius; + } } } @@ -48,16 +58,18 @@ } .gf-timepicker-relative-section { - padding: 0 20px 0 30px; min-height: 237px; float: left; + ul { list-style: none; float: left; margin: 0 30px 10px 0px; + li { line-height: 22px; } + li.active { border-bottom: 1px solid $blue; margin: 3px 0; @@ -72,26 +84,32 @@ td { padding: 1px; } + button { @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); background-image: none; border: none; color: $text-color; + &.active span { color: $blue; font-weight: bold; } + .text-info { color: $orange; font-weight: bold; } + &.btn-sm { font-size: $font-size-sm; padding: 5px 11px; } + &:hover { color: $text-color-strong; } + &[disabled] { color: $text-color; } @@ -114,6 +132,7 @@ .fa-chevron-left::before { content: '\f053'; } + .fa-chevron-right::before { content: '\f054'; } diff --git a/public/sass/components/_tooltip.scss b/public/sass/components/_tooltip.scss index b5799e49301..650d8ba1b1a 100644 --- a/public/sass/components/_tooltip.scss +++ b/public/sass/components/_tooltip.scss @@ -11,21 +11,26 @@ font-size: 11px; line-height: 1.4; @include opacity(0); + &.in { @include opacity(100); } + &.top { margin-top: -3px; padding: 5px 0; } + &.right { margin-left: 3px; padding: 0 5px; } + &.bottom { margin-top: 3px; padding: 5px 0; } + &.left { margin-left: -3px; padding: 0 5px; @@ -60,6 +65,7 @@ border-width: $tooltipArrowWidth $tooltipArrowWidth 0; border-top-color: $tooltipArrowColor; } + &.right .tooltip-arrow { top: 50%; left: 0; @@ -67,6 +73,7 @@ border-width: $tooltipArrowWidth $tooltipArrowWidth $tooltipArrowWidth 0; border-right-color: $tooltipArrowColor; } + &.left .tooltip-arrow { top: 50%; right: 0; @@ -74,6 +81,7 @@ border-width: $tooltipArrowWidth 0 $tooltipArrowWidth $tooltipArrowWidth; border-left-color: $tooltipArrowColor; } + &.bottom .tooltip-arrow { top: 0; left: 50%; @@ -98,6 +106,7 @@ max-height: 600px; overflow: hidden; line-height: 14px; + a { color: $tooltipLinkColor; } diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index c0f8c72ab4f..b92bd596193 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -10,6 +10,7 @@ .navbar-page-btn { transform: translate3d(-36px, 0, 0); + i { opacity: 0; } @@ -40,9 +41,11 @@ .navbar { display: none; } + .scroll-canvas--dashboard { height: 100%; } + .submenu-controls { display: none; } diff --git a/public/sass/components/edit_sidemenu.scss b/public/sass/components/edit_sidemenu.scss index 4fc6795c068..8d60851b4d3 100644 --- a/public/sass/components/edit_sidemenu.scss +++ b/public/sass/components/edit_sidemenu.scss @@ -46,6 +46,7 @@ li { float: left; } + a { margin: 0.3rem 1rem; } diff --git a/public/sass/fonts.scss b/public/sass/fonts.scss index 8d50752f4e7..02fb498356a 100644 --- a/public/sass/fonts.scss +++ b/public/sass/fonts.scss @@ -1 +1 @@ -@import "base/fonts"; +@import 'base/fonts'; diff --git a/public/sass/grafana.dark.scss b/public/sass/grafana.dark.scss index 53193d213e6..f7f5163f36f 100644 --- a/public/sass/grafana.dark.scss +++ b/public/sass/grafana.dark.scss @@ -1,3 +1,3 @@ -@import "variables"; -@import "variables.dark"; -@import "grafana"; +@import 'variables'; +@import 'variables.dark'; +@import 'grafana'; diff --git a/public/sass/grafana.light.scss b/public/sass/grafana.light.scss index e03d24470e6..d7824edfc5d 100644 --- a/public/sass/grafana.light.scss +++ b/public/sass/grafana.light.scss @@ -1,3 +1,3 @@ -@import "variables"; -@import "variables.light"; -@import "grafana"; +@import 'variables'; +@import 'variables.light'; +@import 'grafana'; diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index a17c1c67d7b..818dc6c3e60 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -42,15 +42,13 @@ overflow: auto; height: 100%; -webkit-overflow-scrolling: touch; + display: flex; + flex-direction: column; &--dashboard { height: calc(100% - 56px); } - // Sticky footer - display: flex; - flex-direction: column; - > div { flex-grow: 1; } diff --git a/public/sass/mixins/_forms.scss b/public/sass/mixins/_forms.scss index ce488f0f636..5d2a4421353 100644 --- a/public/sass/mixins/_forms.scss +++ b/public/sass/mixins/_forms.scss @@ -13,16 +13,9 @@ .custom-control { color: $color; } - // Set the border and box shadow on specific inputs to match + .form-control { border-color: $color; - // @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - - &:focus { - // border-color: darken($border-color, 10%); - // $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%); - // @include box-shadow($shadow); - } } // Set validation states also for addons diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 77752be11bc..f285ab753ff 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -107,6 +107,7 @@ display: flex; flex-direction: column; flex-grow: 1; + justify-content: center; overflow: hidden; } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 589012bff3f..a0ff9fd877c 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -214,7 +214,7 @@ div.flot-text { &--info { display: block; - @include panel-corner-color(lighten($panel-bg, 4%)); + @include panel-corner-color(lighten($panel-corner, 4%)); .fa:before { content: '\f129'; } @@ -222,7 +222,7 @@ div.flot-text { &--links { display: block; - @include panel-corner-color(lighten($panel-bg, 4%)); + @include panel-corner-color(lighten($panel-corner, 4%)); .fa { left: 4px; } @@ -233,7 +233,7 @@ div.flot-text { &--error { display: block; - color: $text-color; + color: $white; @include panel-corner-color($popover-error-bg); .fa:before { content: '\f12a'; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 37ed0bcbc92..098dae1a4a2 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -135,7 +135,6 @@ position: absolute; animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67); animation-iteration-count: 100; - z-index: 2; background: $blue; } @@ -161,7 +160,7 @@ } .run-icon { - margin-left: 0.5em; + margin-left: 0.25em; transform: rotate(90deg); } diff --git a/public/sass/pages/_history.scss b/public/sass/pages/_history.scss index ea845b78445..42aa58f0bb7 100644 --- a/public/sass/pages/_history.scss +++ b/public/sass/pages/_history.scss @@ -29,10 +29,6 @@ white-space: nowrap; position: relative; - &:before, - &:after { - } - &:after { left: -40px; } diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 4baff47b2a8..091391da7ff 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -13,9 +13,11 @@ $login-border: #8daac5; background-image: url(../img/heatmap_bg_test.svg); background-size: cover; color: #d8d9da; + & a { color: #d8d9da !important; } + & .btn-primary { @include buttonBackground(#ff6600, #bc3e06); } @@ -198,6 +200,10 @@ select:-webkit-autofill:focus { border: none; font-size: 15px; padding: 10px 10px; + font-weight: bold; + display: inline-block; + width: 170px; + color: $text-color; &.active { background: darken($tight-form-bg, 5%); @@ -207,11 +213,6 @@ select:-webkit-autofill:focus { &:focus { outline: 0; } - - font-weight: bold; - display: inline-block; - width: 170px; - color: $text-color; } .password-strength { @@ -222,10 +223,12 @@ select:-webkit-autofill:focus { padding-top: 3px; color: darken($text-color, 20%); border-top: 3px solid $red; + &.password-strength-ok { width: 40%; border-top: 3px solid lighten($yellow, 10%); } + &.password-strength-good { width: 100%; border-top: 3px solid lighten($green, 10%); @@ -252,6 +255,7 @@ select:-webkit-autofill:focus { .password-recovery { background: $tight-form-bg; padding: 10px; + a { color: $gray-2; } diff --git a/public/sass/pages/_plugins.scss b/public/sass/pages/_plugins.scss index 15149dd0d35..2570eb6c791 100644 --- a/public/sass/pages/_plugins.scss +++ b/public/sass/pages/_plugins.scss @@ -3,9 +3,11 @@ font-size: $font-size-sm; position: relative; top: 1.2rem; + &:hover { color: $link-hover-color; } + img { vertical-align: top; } @@ -18,12 +20,12 @@ } .plugin-info-list-item { - img { - width: 16px; - } - white-space: nowrap; max-width: $page-sidebar-width; text-overflow: ellipsis; overflow: hidden; + + img { + width: 16px; + } } diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index 657d1f0414b..307afde42d1 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,6 +1,3 @@ -input[type='text'].ng-dirty.ng-invalid { -} - input.validation-error, input.ng-dirty.ng-invalid { box-shadow: inset 0 0px 5px $red; diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh index 68c044b395c..3af199d3ff2 100755 --- a/scripts/circle-test-frontend.sh +++ b/scripts/circle-test-frontend.sh @@ -11,4 +11,3 @@ function exit_if_fail { } exit_if_fail npm run test -exit_if_fail npm run build diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index 7b975aac977..8a71ea26627 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -10,15 +10,26 @@ module.exports = function (grunt) { grunt.registerTask('test', [ 'sasslint', - 'exec:tslint', + 'tslint', + 'typecheck', "exec:jest", 'no-only-tests' ]); + grunt.registerTask('tslint', [ + 'newer:exec:tslintPackages', + 'newer:exec:tslintRoot', + ]); + + grunt.registerTask('typecheck', [ + 'newer:exec:typecheckPackages', + 'newer:exec:typecheckRoot', + ]); + grunt.registerTask('precommit', [ - 'sasslint', - 'newer:exec:tslint', - 'newer:exec:tsc', + 'newer:sasslint', + 'typecheck', + 'tslint', 'no-only-tests' ]); diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index 3b60c5c3be6..27bfd7ae43d 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -2,12 +2,20 @@ module.exports = function (config, grunt) { 'use strict'; return { - tslint: { - command: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json', + tslintPackages: { + command: 'yarn workspaces run tslint', + src: ['packages/**/*.ts*'], + }, + tslintRoot: { + command: 'yarn run tslint', src: ['public/app/**/*.ts*'], }, - tsc: { - command: 'yarn tsc --noEmit', + typecheckPackages: { + command: 'yarn workspaces run typecheck', + src: ['packages/**/*.ts*'], + }, + typecheckRoot: { + command: 'yarn run typecheck', src: ['public/app/**/*.ts*'], }, jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2', diff --git a/scripts/grunt/options/sasslint.js b/scripts/grunt/options/sasslint.js index 8ba5ea3047f..e7651b5ece8 100644 --- a/scripts/grunt/options/sasslint.js +++ b/scripts/grunt/options/sasslint.js @@ -4,9 +4,9 @@ module.exports = function(config) { options: { configFile: 'public/sass/.sass-lint.yml', }, - target: [ - 'public/sass/*.scss', - 'public/sass/components/*.scss', - ] + src: [ + 'public/sass/**/*.scss', + 'packages/**/*.scss', + ], }; }; diff --git a/scripts/grunt/options/tslint.js b/scripts/grunt/options/tslint.js deleted file mode 100644 index d51c2062676..00000000000 --- a/scripts/grunt/options/tslint.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = function(config, grunt) { - 'use strict' - // dummy to avoid template compile error - return { - source: { - files: { - src: "" - } - } - }; -}; diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index ae7222e8374..cdc17b5ec00 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -3,9 +3,6 @@ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = { target: 'web', - stats: { - children: false - }, entry: { app: './public/app/index.ts', }, @@ -25,6 +22,7 @@ module.exports = { ], }, stats: { + children: false, warningsFilter: /export .* was not found in/ }, node: { diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js index d45c78352b0..880b7e08ed2 100644 --- a/scripts/webpack/webpack.prod.js +++ b/scripts/webpack/webpack.prod.js @@ -3,7 +3,6 @@ const merge = require('webpack-merge'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const common = require('./webpack.common.js'); -const webpack = require('webpack'); const path = require('path'); const ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); const HtmlWebpackPlugin = require("html-webpack-plugin"); diff --git a/tsconfig.json b/tsconfig.json index 58b29ba428e..3c8c41f34e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ "noUnusedLocals": true, "baseUrl": "public", "pretty": true, + "typeRoots": ["node_modules/@types", "types"], "paths": { "app": ["app"] } diff --git a/yarn.lock b/yarn.lock index 9bc6c7dcd63..78300be233e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -804,6 +804,11 @@ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b" integrity sha512-q6LuBI0t5u04f0Q4/R+cGBqIbZMtJkVvCSF+nTfFBBdQqQvJR/mNHeWjRkszyLl7oyf2rDoKUYMEjTw5AV0hiw== +"@types/classnames@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628" + integrity sha512-XHcYvVdbtAxVstjKxuULYqYaWIzHR15yr1pZj4fnGChuBVJlIAp9StJna0ZJNSgxPh4Nac2FL4JM3M11Tm6fqQ== + "@types/d3-array@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.3.tgz#dd141e3ba311485fffbf0792a1b01a7f2ec12dc1" @@ -1029,6 +1034,16 @@ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.4.tgz#cc43ae176a91dcb1504839b0b9d6659386cf0af5" integrity sha512-46jSw0QMerCRkhJZbOwPA0Eb9T1p74HtECsfa0GXdgjkenSGhgvK96w+e2PEPu4GF0/brUK5WQKq/rUQQFyAxA== +"@types/jquery@^1.10.35": + version "1.10.35" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-1.10.35.tgz#4e5c2b1e5b3bf0b863efb8c5e70081f52e6c9518" + integrity sha512-SVtqEcudm7yjkTwoRA1gC6CNMhGDdMx4Pg8BPdiqI7bXXdCn1BPmtxgeWYQOgDxrq53/5YTlhq5ULxBEAlWIBg== + +"@types/lodash@^4.14.119": + version "4.14.119" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39" + integrity sha512-Z3TNyBL8Vd/M9D9Ms2S3LmFq2sSMzahodD6rCS9V2N44HUMINb75jNkSuwAx7eo2ufqTdfOdtGQpNbieUjPQmw== + "@types/node@*": version "10.11.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.4.tgz#e8bd933c3f78795d580ae41d86590bfc1f4f389d" @@ -1397,7 +1412,7 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.0, ajv@^5.3.0: +ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= @@ -1847,7 +1862,7 @@ aws-sign2@~0.7.0: resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= -aws4@^1.6.0, aws4@^1.8.0: +aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== @@ -2878,13 +2893,6 @@ browserify-sign@^4.0.0: inherits "^2.0.1" parse-asn1 "^5.0.0" -browserify-zlib@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" - integrity sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0= - dependencies: - pako "~0.2.0" - browserify-zlib@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" @@ -3350,7 +3358,7 @@ classnames@2.x, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -clean-css@3.4.x, clean-css@~3.4.2: +clean-css@3.4.x: version "3.4.28" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff" integrity sha1-vxlF6C/ICPVWlebd6uwBQA79A/8= @@ -3655,7 +3663,7 @@ combined-stream@1.0.6: dependencies: delayed-stream "~1.0.0" -combined-stream@~1.0.5, combined-stream@~1.0.6: +combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== @@ -3753,7 +3761,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.4.1, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.5.2: +concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -5020,14 +5028,6 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -each-async@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/each-async/-/each-async-1.1.1.tgz#dee5229bdf0ab6ba2012a395e1b869abf8813473" - integrity sha1-3uUim98KtrogEqOV4bhpq/iBNHM= - dependencies: - onetime "^1.0.0" - set-immediate-shim "^1.0.0" - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -5700,7 +5700,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.1, extend@~3.0.2: +extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -5847,7 +5847,7 @@ figgy-pudding@^3.0.0, figgy-pudding@^3.1.0, figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== -figures@^1.0.1, figures@^1.3.5, figures@^1.7.0: +figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= @@ -6080,7 +6080,7 @@ fork-ts-checker-webpack-plugin@^0.4.9: resolve "^1.5.0" tapable "^1.0.0" -form-data@~2.3.1, form-data@~2.3.2: +form-data@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= @@ -6669,14 +6669,6 @@ grunt-contrib-compress@^1.3.0: optionalDependencies: iltorb "^1.0.13" -grunt-contrib-concat@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz#61509863084e871d7e86de48c015259ed97745bd" - integrity sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0= - dependencies: - chalk "^1.0.0" - source-map "^0.5.3" - grunt-contrib-copy@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz#7060c6581e904b8ab0d00f076e0a8f6e3e7c3573" @@ -6685,15 +6677,6 @@ grunt-contrib-copy@~1.0.0: chalk "^1.1.1" file-sync-cmp "^0.1.0" -grunt-contrib-cssmin@~1.0.2: - version "1.0.2" - resolved "http://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-1.0.2.tgz#1734cbd3d84ca7364758b7e58ff18e52aa60bb76" - integrity sha1-FzTL09hMpzZHWLflj/GOUqpgu3Y= - dependencies: - chalk "^1.0.0" - clean-css "~3.4.2" - maxmin "^1.1.0" - grunt-exec@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4" @@ -6761,22 +6744,13 @@ grunt-postcss@^0.8.0: diff "^2.0.2" postcss "^5.0.0" -grunt-sass-lint@^0.2.2: +grunt-sass-lint@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/grunt-sass-lint/-/grunt-sass-lint-0.2.4.tgz#06f77635ad8a5048968ea33c5584b40a18281e35" integrity sha512-jV88yXoxFFvr4R3WVBl0uz4YBzNxXTrCJ7ZBKrYby/SjRCw2sieKPkt5tpWDcQZIj9XrKsOpKuHQn08MaECVwg== dependencies: sass-lint "^1.12.0" -grunt-sass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/grunt-sass/-/grunt-sass-2.1.0.tgz#b7ba1d85ef4c2d9b7d8195fe65f664ac7554efa1" - integrity sha512-XkexnQt/9rhReNd+Y7T0n/2g5FqYOQKfi2iSlpwDqvgs7EgEaGTxNhnWzHnbW5oNRvzL9AHopBG3AgRxL0d+DA== - dependencies: - each-async "^1.0.0" - node-sass "^4.7.2" - object-assign "^4.0.1" - grunt-usemin@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/grunt-usemin/-/grunt-usemin-3.1.1.tgz#5ab679510d672cea566cc717abe8b8a009f641c2" @@ -6822,14 +6796,6 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== -gzip-size@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-1.0.0.tgz#66cf8b101047227b95bace6ea1da0c177ed5c22f" - integrity sha1-Zs+LEBBHInuVus5uodoMF37Vwi8= - dependencies: - browserify-zlib "^0.1.4" - concat-stream "^1.4.1" - gzip-size@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c" @@ -6859,14 +6825,6 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - har-validator@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" @@ -9372,16 +9330,6 @@ math-random@^1.0.1: resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" integrity sha1-izqsWIuKZuSXXjzepn97sylgH6w= -maxmin@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/maxmin/-/maxmin-1.1.0.tgz#71365e84a99dd8f8b3f7d5fde2f00d1e7f73be61" - integrity sha1-cTZehKmd2Piz99X94vANHn9zvmE= - dependencies: - chalk "^1.0.0" - figures "^1.0.1" - gzip-size "^1.0.0" - pretty-bytes "^1.0.0" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -9456,7 +9404,7 @@ memory-fs@^0.4.0, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.1.0, meow@^3.3.0, meow@^3.7.0: +meow@^3.3.0, meow@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= @@ -10082,10 +10030,10 @@ node-releases@^1.0.1: dependencies: semver "^5.3.0" -node-sass@^4.7.2: - version "4.9.3" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224" - integrity sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww== +node-sass@^4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" + integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -10102,7 +10050,7 @@ node-sass@^4.7.2: nan "^2.10.0" node-gyp "^3.8.0" npmlog "^4.0.0" - request "2.87.0" + request "^2.88.0" sass-graph "^2.2.4" stdout-stream "^1.4.0" "true-case-path" "^1.0.2" @@ -10483,11 +10431,6 @@ nwsapi@^2.0.7: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016" integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ== -oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= - oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -10910,11 +10853,6 @@ pacote@^8.1.6: unique-filename "^1.1.0" which "^1.3.0" -pako@~0.2.0: - version "0.2.9" - resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" - integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU= - pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" @@ -11659,14 +11597,6 @@ prettier@^1.12.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== -pretty-bytes@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" - integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ= - dependencies: - get-stdin "^4.0.1" - meow "^3.1.0" - pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" @@ -11881,7 +11811,7 @@ qs@6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== -qs@~6.5.1, qs@~6.5.2: +qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== @@ -12685,33 +12615,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" -request@2.87.0: - version "2.87.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" - integrity sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - -request@^2.74.0, request@^2.81.0, request@^2.85.0, request@^2.87.0: +request@^2.74.0, request@^2.81.0, request@^2.85.0, request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -12949,14 +12853,14 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@^5.4.2, rxjs@^5.4.3, rxjs@^5.5.2: +rxjs@^5.4.2, rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== dependencies: symbol-observable "1.0.1" -rxjs@^6.1.0: +rxjs@^6.1.0, rxjs@^6.3.3: version "6.3.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== @@ -13202,11 +13106,6 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-immediate-shim@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= - set-value@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" @@ -14368,13 +14267,6 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" -tough-cookie@~2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" - integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== - dependencies: - punycode "^1.4.1" - tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -14544,6 +14436,11 @@ typescript@^3.0.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.1.tgz#3362ba9dd1e482ebb2355b02dfe8bcd19a2c7c96" integrity sha512-Veu0w4dTc/9wlWNf2jeRInNodKlcdLgemvPsrNpfu5Pq39sgfFjvIIgTsvUHCoLBnMhPoUA+tFxsXjU6VexVRQ== +typescript@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5" + integrity sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg== + ua-parser-js@^0.7.18: version "0.7.19" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" @@ -14878,7 +14775,7 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: +uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==