diff --git a/.circleci/config.yml b/.circleci/config.yml
index f6d66daab84..7e89ffe7a1f 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.4
+ - image: circleci/golang:1.11.5
- 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.4
+ - image: circleci/golang:1.11.5
- image: circleci/postgres:9.3-ram
environment:
POSTGRES_USER: grafanatest
@@ -74,7 +74,7 @@ jobs:
gometalinter:
docker:
- - image: circleci/golang:1.11.4
+ - image: circleci/golang:1.11.5
environment:
# we need CGO because of go-sqlite3
CGO_ENABLED: 1
@@ -106,7 +106,7 @@ jobs:
test-backend:
docker:
- - image: circleci/golang:1.11.4
+ - image: circleci/golang:1.11.5
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -116,7 +116,7 @@ jobs:
build-all:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -147,9 +147,6 @@ jobs:
- run:
name: sha-sum packages
command: 'go run build.go sha-dist'
- - run:
- name: Build Grafana.com master publisher
- command: 'go build -o scripts/publish scripts/build/publish.go'
- run:
name: Test and build Grafana.com release publisher
command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
@@ -158,13 +155,12 @@ jobs:
paths:
- dist/grafana*
- scripts/*.sh
- - scripts/publish
- scripts/build/release_publisher/release_publisher
- scripts/build/publish.sh
build:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -233,7 +229,7 @@ jobs:
build-enterprise:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -265,7 +261,7 @@ jobs:
build-all-enterprise:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -393,7 +389,7 @@ jobs:
name: Publish to Grafana.com
command: |
rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
- ./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
+ cd dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -from-local
deploy-release:
docker:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9225d6545e4..67acea4e149 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,29 +1,44 @@
-# 5.5.0 (unreleased)
+# 6.0.0-beta1 (unreleased)
### New Features
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
+* **Influxdb**: Add support for time zone (`tz`) clause [#10322](https://github.com/grafana/grafana/issues/10322), thx [@cykl](https://github.com/cykl)
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
### Minor
+* **Alerting**: Use seperate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
* **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)
+* **Elasticsearch**: Add support for template variable interpolation in alias field [#4075](https://github.com/grafana/grafana/issues/4075), thx [@SamuelToh](https://github.com/SamuelToh)
+* **Influxdb**: Fix autocomplete of measurements does not escape search string properly [#11503](https://github.com/grafana/grafana/issues/11503), thx [@SamuelToh](https://github.com/SamuelToh)
+* **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)
+* **Cloudwatch**: Fix Assume Role Arn [#14722](https://github.com/grafana/grafana/issues/14722), thx [@jaken551](https://github.com/jaken551)
+* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
-* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
-* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK]req(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)
+* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
+* **Dashboard**: `Min width` changed to `Max per row` for repeating panels. This lets you specify the maximum number of panels to show per row and by that repeated panels will always take up full width of row [#12991](https://github.com/grafana/grafana/pull/12991), thx [@pgiraud](https://github.com/pgiraud)
+* **Dashboard**: Retain decimal precision when exporting CSV [#13929](https://github.com/grafana/grafana/issues/13929), thx [@cinaglia](https://github.com/cinaglia)
+* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
* **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)
-* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
+* **Units**: Add Floating Point Operations per Second units [#14558](https://github.com/grafana/grafana/pull/14558), thx [@hahnjo](https://github.com/hahnjo)
+* **Table**: Renders epoch string as date if date column style [#14484](https://github.com/grafana/grafana/issues/14484)
+* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
+* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
### Bug fixes
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
+* **Annotations**: Fix creating annotation when graph panel has no data points position the popup outside viewport [#13765](https://github.com/grafana/grafana/issues/13765), thx [@banjeremy](https://github.com/banjeremy)
+
+### Breaking changes
+* **Text Panel**: The text panel does no longer by default allow unsantizied HTML. [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable `GF_PANELS_DISABLE_SANITIZE_HTML=true`.
+* **Dashboard**: Panel property `minSpan` replaced by `maxPerRow`. Dashboard migration will automatically migrate all dashboard panels using the `minSpan` property to the new `maxPerRow` property [#12991](https://github.com/grafana/grafana/pull/12991)
# 5.4.3 (2019-01-14)
diff --git a/Dockerfile b/Dockerfile
index c3af89b6092..c3e59c8048e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Golang build container
-FROM golang:1.11.4
+FROM golang:1.11.5
WORKDIR $GOPATH/src/github.com/grafana/grafana
@@ -19,11 +19,13 @@ COPY package.json package.json
RUN go run build.go build
# Node build container
-FROM node:8
+FROM node:10.14.2
WORKDIR /usr/src/app/
COPY package.json yarn.lock ./
+COPY packages packages
+
RUN yarn install --pure-lockfile --no-progress
COPY Gruntfile.js tsconfig.json tslint.json ./
diff --git a/ROADMAP.md b/ROADMAP.md
index 891bc9f790b..b5e62578475 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -5,18 +5,22 @@ But it will give you an idea of our current vision and plan.
### Short term (1-2 months)
- PRs & Bugs
- - Multi-Stat panel
+ - React Panel Support
+ - React Query Editor Support
- Metrics & Log Explore UI
-
+ - Grafana UI library shared between grafana & plugins
+ - Seperate visualization from panels
+ - More reuse between Explore & dashboard
+ - Explore logging support for more data sources
+
### Mid term (2-4 months)
- - React Panels
- - Change visualization (panel type) on the fly.
- - Templating Query Editor UI Plugin hook
- - Backend plugins
+ - Drilldown links
+ - Dashboards as code workflows
+ - React migration
+ - New panels
### Long term (4 - 8 months)
- Alerting improvements (silence, per series tracking, etc)
- - Progress on React migration
### In a distant future far far away
- Meta queries
diff --git a/appveyor.yml b/appveyor.yml
index 5f97784dd38..ccf9b5a06e1 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.4
+ GOVERSION: 1.11.5
install:
- rmdir c:\go /s /q
diff --git a/build.go b/build.go
index 4486cd3deb9..ebe240d97ef 100644
--- a/build.go
+++ b/build.go
@@ -46,6 +46,8 @@ var (
binaries []string = []string{"grafana-server", "grafana-cli"}
isDev bool = false
enterprise bool = false
+ skipRpmGen bool = false
+ skipDebGen bool = false
)
func main() {
@@ -67,6 +69,8 @@ func main() {
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
+ flag.BoolVar(&skipRpmGen, "skipRpm", skipRpmGen, "skip rpm package generation (default: false)")
+ flag.BoolVar(&skipDebGen, "skipDeb", skipDebGen, "skip deb package generation (default: false)")
flag.Parse()
buildId = shortenBuildId(buildIdRaw)
@@ -165,6 +169,7 @@ func makeLatestDistCopies() {
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz",
+ ".linux-armv6.tar.gz": "dist/grafana-latest.linux-armv6.tar.gz",
".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz",
}
@@ -239,6 +244,8 @@ func createDebPackages() {
previousPkgArch := pkgArch
if pkgArch == "armv7" {
pkgArch = "armhf"
+ } else if pkgArch == "armv6" {
+ pkgArch = "armel"
}
createPackage(linuxPackageOptions{
packageType: "deb",
@@ -289,8 +296,13 @@ func createRpmPackages() {
}
func createLinuxPackages() {
- createDebPackages()
- createRpmPackages()
+ if !skipDebGen {
+ createDebPackages()
+ }
+
+ if !skipRpmGen {
+ createRpmPackages()
+ }
}
func createPackage(options linuxPackageOptions) {
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 7f61ac96870..4da3588bef2 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -106,6 +106,22 @@ path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+login_remember_days = 7
+
+# How often should the login token be rotated. default to '10m'
+rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+delete_expired_token_after_days = 30
+
#################################### Session #############################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@@ -175,11 +191,6 @@ admin_password = admin
# used for signing
secret_key = SW2YcwTIb9zpOOhoPsMm
-# Auto-login remember days
-login_remember_days = 7
-cookie_username = grafana_user
-cookie_remember_name = grafana_remember
-
# disable gravatar profile images
disable_gravatar = false
@@ -189,6 +200,9 @@ data_source_proxy_whitelist =
# disable protection against brute force login attempts
disable_brute_force_login_protection = false
+# set cookies as https only. default is false
+https_flag_cookies = false
+
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
@@ -490,7 +504,7 @@ concurrent_render_limit = 5
#################################### Explore #############################
[explore]
# Enable the Explore section
-enabled = false
+enabled = true
#################################### Internal Grafana Metrics ############
# Metrics available at HTTP API Url /metrics
@@ -570,6 +584,7 @@ callback_url =
[panels]
enable_alpha = false
+disable_sanitize_html = false
[enterprise]
license_path =
diff --git a/conf/sample.ini b/conf/sample.ini
index 014016d45bc..8b731e43bd9 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -102,6 +102,22 @@ log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+;cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+;login_remember_days = 7
+
+# How often should the login token be rotated. default to '10'
+;rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+;delete_expired_token_after_days = 30
+
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@@ -162,11 +178,6 @@ log_queries =
# used for signing
;secret_key = SW2YcwTIb9zpOOhoPsMm
-# Auto-login remember days
-;login_remember_days = 7
-;cookie_username = grafana_user
-;cookie_remember_name = grafana_remember
-
# disable gravatar profile images
;disable_gravatar = false
@@ -176,6 +187,9 @@ log_queries =
# disable protection against brute force login attempts
;disable_brute_force_login_protection = false
+# set cookies as https only. default is false
+;https_flag_cookies = false
+
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
@@ -415,7 +429,7 @@ log_queries =
#################################### Explore #############################
[explore]
# Enable the Explore section
-;enabled = false
+;enabled = true
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /metrics
@@ -495,3 +509,8 @@ log_queries =
# Path to a valid Grafana Enterprise license.jwt file
;license_path =
+[panels]
+;enable_alpha = false
+# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
+;disable_sanitize_html = false
+
diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml
index 1195e2a977c..504ee86404d 100644
--- a/devenv/docker/ha_test/docker-compose.yaml
+++ b/devenv/docker/ha_test/docker-compose.yaml
@@ -54,7 +54,8 @@ services:
# - GF_DATABASE_SSL_MODE=disable
# - GF_SESSION_PROVIDER=postgres
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
- - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
+ - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
+ - GF_LOGIN_ROTATE_TOKEN_MINUTES=2
ports:
- 3000
depends_on:
diff --git a/devenv/docker/loadtest/README.md b/devenv/docker/loadtest/README.md
new file mode 100644
index 00000000000..8e724637acb
--- /dev/null
+++ b/devenv/docker/loadtest/README.md
@@ -0,0 +1,69 @@
+# Grafana load test
+
+Runs load tests and checks using [k6](https://k6.io/).
+
+## Prerequisites
+
+Docker
+
+## Run
+
+Run load test for 15 minutes:
+
+```bash
+$ ./run.sh
+```
+
+Run load test for custom duration:
+
+```bash
+$ ./run.sh -d 10s
+```
+
+Example output:
+
+```bash
+
+ /\ |‾‾| /‾‾/ /‾/
+ /\ / \ | |_/ / / /
+ / \/ \ | | / ‾‾\
+ / \ | |‾\ \ | (_) |
+ / __________ \ |__| \__\ \___/ .io
+
+ execution: local
+ output: -
+ script: src/auth_token_test.js
+
+ duration: 15m0s, iterations: -
+ vus: 2, max: 2
+
+ done [==========================================================] 15m0s / 15m0s
+
+ █ user auth token test
+
+ █ user authenticates thru ui with username and password
+
+ ✓ response status is 200
+ ✓ response has cookie 'grafana_session' with 32 characters
+
+ █ batch tsdb requests
+
+ ✓ response status is 200
+
+ checks.....................: 100.00% ✓ 32844 ✗ 0
+ data_received..............: 411 MB 457 kB/s
+ data_sent..................: 12 MB 14 kB/s
+ group_duration.............: avg=95.64ms min=16.42ms med=94.35ms max=307.52ms p(90)=137.78ms p(95)=146.75ms
+ http_req_blocked...........: avg=1.27ms min=942ns med=610.08µs max=48.32ms p(90)=2.92ms p(95)=4.25ms
+ http_req_connecting........: avg=1.06ms min=0s med=456.79µs max=47.19ms p(90)=2.55ms p(95)=3.78ms
+ http_req_duration..........: avg=58.16ms min=1ms med=52.59ms max=293.35ms p(90)=109.53ms p(95)=120.19ms
+ http_req_receiving.........: avg=38.98µs min=6.43µs med=32.55µs max=16.2ms p(90)=64.63µs p(95)=78.8µs
+ http_req_sending...........: avg=328.66µs min=8.09µs med=110.77µs max=44.13ms p(90)=552.65µs p(95)=1.09ms
+ http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
+ http_req_waiting...........: avg=57.79ms min=935.02µs med=52.15ms max=293.06ms p(90)=109.04ms p(95)=119.71ms
+ http_reqs..................: 34486 38.317775/s
+ iteration_duration.........: avg=1.09s min=1.81µs med=1.09s max=1.3s p(90)=1.13s p(95)=1.14s
+ iterations.................: 1642 1.824444/s
+ vus........................: 2 min=2 max=2
+ vus_max....................: 2 min=2 max=2
+```
diff --git a/devenv/docker/loadtest/auth_token_test.js b/devenv/docker/loadtest/auth_token_test.js
new file mode 100644
index 00000000000..e1356fb6f9a
--- /dev/null
+++ b/devenv/docker/loadtest/auth_token_test.js
@@ -0,0 +1,71 @@
+import { sleep, check, group } from 'k6';
+import { createClient, createBasicAuthClient } from './modules/client.js';
+import { createTestOrgIfNotExists, createTestdataDatasourceIfNotExists } from './modules/util.js';
+
+export let options = {
+ noCookiesReset: true
+};
+
+let endpoint = __ENV.URL || 'http://localhost:3000';
+const client = createClient(endpoint);
+
+export const setup = () => {
+ const basicAuthClient = createBasicAuthClient(endpoint, 'admin', 'admin');
+ const orgId = createTestOrgIfNotExists(basicAuthClient);
+ const datasourceId = createTestdataDatasourceIfNotExists(basicAuthClient);
+ client.withOrgId(orgId);
+ return {
+ orgId: orgId,
+ datasourceId: datasourceId,
+ };
+}
+
+export default (data) => {
+ group("user auth token test", () => {
+ if (__ITER === 0) {
+ group("user authenticates thru ui with username and password", () => {
+ let res = client.ui.login('admin', 'admin');
+
+ check(res, {
+ 'response status is 200': (r) => r.status === 200,
+ 'response has cookie \'grafana_session\' with 32 characters': (r) => r.cookies.grafana_session[0].value.length === 32,
+ });
+ });
+ }
+
+ if (__ITER !== 0) {
+ group("batch tsdb requests", () => {
+ const batchCount = 20;
+ const requests = [];
+ const payload = {
+ from: '1547765247624',
+ to: '1547768847624',
+ queries: [{
+ refId: 'A',
+ scenarioId: 'random_walk',
+ intervalMs: 10000,
+ maxDataPoints: 433,
+ datasourceId: data.datasourceId,
+ }]
+ };
+
+ requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' });
+
+ for (let n = 0; n < batchCount; n++) {
+ requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload });
+ }
+
+ let responses = client.batch(requests);
+ for (let n = 0; n < batchCount; n++) {
+ check(responses[n], {
+ 'response status is 200': (r) => r.status === 200,
+ });
+ }
+ });
+ }
+ });
+
+ sleep(1)
+}
+
+export const teardown = (data) => {}
diff --git a/devenv/docker/loadtest/modules/client.js b/devenv/docker/loadtest/modules/client.js
new file mode 100644
index 00000000000..bda0da64564
--- /dev/null
+++ b/devenv/docker/loadtest/modules/client.js
@@ -0,0 +1,187 @@
+import http from "k6/http";
+import encoding from 'k6/encoding';
+
+export const UIEndpoint = class UIEndpoint {
+ constructor(httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ login(username, pwd) {
+ const payload = { user: username, password: pwd };
+ return this.httpClient.formPost('/login', payload);
+ }
+}
+
+export const DatasourcesEndpoint = class DatasourcesEndpoint {
+ constructor(httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ getById(id) {
+ return this.httpClient.get(`/datasources/${id}`);
+ }
+
+ getByName(name) {
+ return this.httpClient.get(`/datasources/name/${name}`);
+ }
+
+ create(payload) {
+ return this.httpClient.post(`/datasources`, JSON.stringify(payload));
+ }
+
+ delete(id) {
+ return this.httpClient.delete(`/datasources/${id}`);
+ }
+}
+
+export const OrganizationsEndpoint = class OrganizationsEndpoint {
+ constructor(httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ getById(id) {
+ return this.httpClient.get(`/orgs/${id}`);
+ }
+
+ getByName(name) {
+ return this.httpClient.get(`/orgs/name/${name}`);
+ }
+
+ create(name) {
+ let payload = {
+ name: name,
+ };
+ return this.httpClient.post(`/orgs`, JSON.stringify(payload));
+ }
+
+ delete(id) {
+ return this.httpClient.delete(`/orgs/${id}`);
+ }
+}
+
+export const GrafanaClient = class GrafanaClient {
+ constructor(httpClient) {
+ httpClient.onBeforeRequest = this.onBeforeRequest;
+ this.raw = httpClient;
+ this.ui = new UIEndpoint(httpClient);
+ this.orgs = new OrganizationsEndpoint(httpClient.withUrl('/api'));
+ this.datasources = new DatasourcesEndpoint(httpClient.withUrl('/api'));
+ }
+
+ batch(requests) {
+ return this.raw.batch(requests);
+ }
+
+ withOrgId(orgId) {
+ this.orgId = orgId;
+ }
+
+ onBeforeRequest(params) {
+ if (this.orgId && this.orgId > 0) {
+ params = params.headers || {};
+ params.headers["X-Grafana-Org-Id"] = this.orgId;
+ }
+ }
+}
+
+export const BaseClient = class BaseClient {
+ constructor(url, subUrl) {
+ if (url.endsWith('/')) {
+ url = url.substring(0, url.length - 1);
+ }
+
+ if (subUrl.endsWith('/')) {
+ subUrl = subUrl.substring(0, subUrl.length - 1);
+ }
+
+ this.url = url + subUrl;
+ this.onBeforeRequest = () => {};
+ }
+
+ withUrl(subUrl) {
+ let c = new BaseClient(this.url, subUrl);
+ c.onBeforeRequest = this.onBeforeRequest;
+ return c;
+ }
+
+ beforeRequest(params) {
+
+ }
+
+ get(url, params) {
+ params = params || {};
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.get(this.url + url, params);
+ }
+
+ formPost(url, body, params) {
+ params = params || {};
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.post(this.url + url, body, params);
+ }
+
+ post(url, body, params) {
+ params = params || {};
+ params.headers = params.headers || {};
+ params.headers['Content-Type'] = 'application/json';
+
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.post(this.url + url, body, params);
+ }
+
+ delete(url, params) {
+ params = params || {};
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.del(this.url + url, null, params);
+ }
+
+ batch(requests) {
+ for (let n = 0; n < requests.length; n++) {
+ let params = requests[n].params || {};
+ params.headers = params.headers || {};
+ params.headers['Content-Type'] = 'application/json';
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ requests[n].params = params;
+ requests[n].url = this.url + requests[n].url;
+ if (requests[n].body) {
+ requests[n].body = JSON.stringify(requests[n].body);
+ }
+ }
+
+ return http.batch(requests);
+ }
+}
+
+export class BasicAuthClient extends BaseClient {
+ constructor(url, subUrl, username, password) {
+ super(url, subUrl);
+ this.username = username;
+ this.password = password;
+ }
+
+ withUrl(subUrl) {
+ let c = new BasicAuthClient(this.url, subUrl, this.username, this.password);
+ c.onBeforeRequest = this.onBeforeRequest;
+ return c;
+ }
+
+ beforeRequest(params) {
+ params = params || {};
+ params.headers = params.headers || {};
+ let token = `${this.username}:${this.password}`;
+ params.headers['Authorization'] = `Basic ${encoding.b64encode(token)}`;
+ }
+}
+
+export const createClient = (url) => {
+ return new GrafanaClient(new BaseClient(url, ''));
+}
+
+export const createBasicAuthClient = (url, username, password) => {
+ return new GrafanaClient(new BasicAuthClient(url, '', username, password));
+}
diff --git a/devenv/docker/loadtest/modules/util.js b/devenv/docker/loadtest/modules/util.js
new file mode 100644
index 00000000000..af6d4cdac09
--- /dev/null
+++ b/devenv/docker/loadtest/modules/util.js
@@ -0,0 +1,35 @@
+export const createTestOrgIfNotExists = (client) => {
+ let orgId = 0;
+ let res = client.orgs.getByName('k6');
+ if (res.status === 404) {
+ res = client.orgs.create('k6');
+ if (res.status !== 200) {
+ throw new Error('Expected 200 response status when creating org');
+ }
+ orgId = res.json().orgId;
+ } else {
+ orgId = res.json().id;
+ }
+
+ client.withOrgId(orgId);
+ return orgId;
+}
+
+export const createTestdataDatasourceIfNotExists = (client) => {
+ const payload = {
+ access: 'proxy',
+ isDefault: false,
+ name: 'k6-testdata',
+ type: 'testdata',
+ };
+
+ let res = client.datasources.getByName(payload.name);
+ if (res.status === 404) {
+ res = client.datasources.create(payload);
+ if (res.status !== 200) {
+ throw new Error('Expected 200 response status when creating datasource');
+ }
+ }
+
+ return res.json().id;
+}
diff --git a/devenv/docker/loadtest/run.sh b/devenv/docker/loadtest/run.sh
new file mode 100755
index 00000000000..474d75383b6
--- /dev/null
+++ b/devenv/docker/loadtest/run.sh
@@ -0,0 +1,24 @@
+#/bin/bash
+
+PWD=$(pwd)
+
+run() {
+ duration='15m'
+ url='http://localhost:3000'
+
+ while getopts ":d:u:" o; do
+ case "${o}" in
+ d)
+ duration=${OPTARG}
+ ;;
+ u)
+ url=${OPTARG}
+ ;;
+ esac
+ done
+ shift $((OPTIND-1))
+
+ docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus 2 --duration $duration src/auth_token_test.js
+}
+
+run "$@"
diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md
index 9aaf29ec5f4..364b55b0cfc 100644
--- a/docs/sources/http_api/data_source.md
+++ b/docs/sources/http_api/data_source.md
@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"defaultRegion": "us-west-1"
},
"secureJsonData": {
- "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
- "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
+ "accessKey": "Ol4pIDpeKSA6XikgOl4p",
+ "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
}
}
```
diff --git a/docs/sources/http_api/other.md b/docs/sources/http_api/other.md
index 5bf0cde05fe..ea905bf88f0 100644
--- a/docs/sources/http_api/other.md
+++ b/docs/sources/http_api/other.md
@@ -82,4 +82,29 @@ HTTP/1.1 200
Content-Type: application/json
{"message": "Logged in"}
-```
\ No newline at end of file
+```
+
+# Health API
+
+## Returns health information about Grafana
+
+`GET /api/health`
+
+**Example Request**
+
+```http
+GET /api/health
+Accept: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200 OK
+
+{
+ "commit": "087143285",
+ "database": "ok",
+ "version": "5.1.3"
+}
+```
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 0e5a55b3c0e..46bab83654e 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -391,6 +391,12 @@ value is `true`.
If you want to track Grafana usage via Google analytics specify *your* Universal
Analytics ID here. By default this feature is disabled.
+### check_for_updates
+
+Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
+in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
+send any sensitive information.
+
## [dashboards]
@@ -589,3 +595,14 @@ Default setting for how Grafana handles nodata or null values in alerting. (aler
Alert notifications can include images, but rendering many images at the same time can overload the server.
This limit will protect the server from render overloading and make sure notifications are sent out quickly. Default
value is `5`.
+
+## [panels]
+
+### enable_alpha
+Set to true if you want to test panels that are not yet ready for general usage.
+
+### disable_sanitize_html
+If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities. Default
+is false. This settings was introduced in Grafana v6.0.
+
+
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 71ce6bdd2ae..3ef32b1b10f 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -52,6 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
+`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
diff --git a/package.json b/package.json
index 470101ff0c4..c794375793b 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
- "version": "5.5.0-pre1",
+ "version": "6.0.0-pre1",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -188,7 +188,8 @@
"slate-react": "^0.12.4",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
- "tinycolor2": "^1.4.1"
+ "tinycolor2": "^1.4.1",
+ "xss": "^1.0.3"
},
"resolutions": {
"caniuse-db": "1.0.30000772",
diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
index 434ee03c7a1..40f6c6c3c37 100644
--- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
+++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
@@ -7,12 +7,12 @@ interface Props {
autoHide?: boolean;
autoHideTimeout?: number;
autoHideDuration?: number;
- autoMaxHeight?: string;
+ autoHeightMax?: string;
hideTracksWhenNotNeeded?: boolean;
renderTrackHorizontal?: React.FunctionComponent;
renderTrackVertical?: React.FunctionComponent;
scrollTop?: number;
- setScrollTop: (value: React.MouseEvent) => void;
+ setScrollTop: (event: any) => void;
autoHeightMin?: number | string;
}
@@ -22,13 +22,13 @@ interface Props {
export class CustomScrollbar extends PureComponent {
static defaultProps: Partial = {
customClassName: 'custom-scrollbars',
- autoHide: true,
+ autoHide: false,
autoHideTimeout: 200,
autoHideDuration: 200,
- autoMaxHeight: '100%',
- hideTracksWhenNotNeeded: false,
setScrollTop: () => {},
+ hideTracksWhenNotNeeded: false,
autoHeightMin: '0',
+ autoHeightMax: '100%',
};
private ref: React.RefObject;
@@ -59,16 +59,32 @@ export class CustomScrollbar extends PureComponent {
}
render() {
- const { customClassName, children, autoMaxHeight, renderTrackHorizontal, renderTrackVertical } = this.props;
+ const {
+ customClassName,
+ children,
+ autoHeightMax,
+ autoHeightMin,
+ setScrollTop,
+ autoHide,
+ autoHideTimeout,
+ hideTracksWhenNotNeeded,
+ renderTrackHorizontal,
+ renderTrackVertical,
+ } = this.props;
return (
)}
renderTrackVertical={renderTrackVertical || (props =>
)}
renderThumbHorizontal={props =>
}
diff --git a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
index aabe3dd98c5..60b4a2e0aa5 100644
--- a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
+++ b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
@@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
Object {
"height": "auto",
"maxHeight": "100%",
- "minHeight": 0,
+ "minHeight": "0",
"overflow": "hidden",
"position": "relative",
"width": "100%",
@@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
"marginBottom": 0,
"marginRight": 0,
"maxHeight": "calc(100% + 0px)",
- "minHeight": 0,
+ "minHeight": "calc(0 + 0px)",
"overflow": "scroll",
"position": "relative",
"right": undefined,
diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
index b3396841d4d..396b7a03162 100644
--- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
@@ -98,83 +98,6 @@ describe('Get thresholds formatted', () => {
});
});
-describe('Format value with value mappings', () => {
- it('should return undefined with no valuemappings', () => {
- const valueMappings: ValueMapping[] = [];
- const value = '10';
- const { instance } = setup({ valueMappings });
-
- const result = instance.getFirstFormattedValueMapping(valueMappings, value);
-
- expect(result).toBeUndefined();
- });
-
- it('should return undefined with no matching valuemappings', () => {
- const valueMappings: ValueMapping[] = [
- { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
- { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
- ];
- const value = '10';
- const { instance } = setup({ valueMappings });
-
- const result = instance.getFirstFormattedValueMapping(valueMappings, value);
-
- expect(result).toBeUndefined();
- });
-
- it('should return first matching mapping with lowest id', () => {
- const valueMappings: ValueMapping[] = [
- { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
- { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
- ];
- const value = '10';
- const { instance } = setup({ valueMappings });
-
- const result = instance.getFirstFormattedValueMapping(valueMappings, value);
-
- expect(result.text).toEqual('1-20');
- });
-
- it('should return rangeToText mapping where value equals to', () => {
- const valueMappings: ValueMapping[] = [
- { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
- { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
- ];
- const value = '10';
- const { instance } = setup({ valueMappings });
-
- const result = instance.getFirstFormattedValueMapping(valueMappings, value);
-
- expect(result.text).toEqual('1-10');
- });
-
- it('should return rangeToText mapping where value equals from', () => {
- const valueMappings: ValueMapping[] = [
- { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
- { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
- ];
- const value = '10';
- const { instance } = setup({ valueMappings });
-
- const result = instance.getFirstFormattedValueMapping(valueMappings, value);
-
- expect(result.text).toEqual('10-20');
- });
-
- it('should return rangeToText mapping where value is between from and to', () => {
- const valueMappings: ValueMapping[] = [
- { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
- { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
- ];
- const value = '10';
- const { instance } = setup({ valueMappings });
-
- const result = instance.getFirstFormattedValueMapping(valueMappings, value);
-
- expect(result.text).toEqual('1-20');
- });
-});
-
describe('Format value', () => {
it('should return if value isNaN', () => {
const valueMappings: ValueMapping[] = [];
diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
index c0f23f17bc3..8d4d8c2169d 100644
--- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
@@ -1,10 +1,10 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
-import { ValueMapping, Threshold, MappingType, BasicGaugeColor, ValueMap, RangeMap } from '../../types/panel';
+import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
import { TimeSeriesVMs } from '../../types/series';
-import { getValueFormat } from '../../utils/valueFormats/valueFormats';
import { GrafanaTheme } from '../../types';
+import { getValueFormat } from '../../utils/valueFormats/valueFormats';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
type TimeSeriesValue = string | number | null;
@@ -52,70 +52,6 @@ export class Gauge extends PureComponent {
this.draw();
}
- addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) {
- if (!valueToTextMapping.value) {
- return allValueMappings;
- }
-
- const valueAsNumber = parseFloat(value as string);
- const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
-
- if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
- return allValueMappings;
- }
-
- if (valueAsNumber !== valueToTextMappingAsNumber) {
- return allValueMappings;
- }
-
- return allValueMappings.concat(valueToTextMapping);
- }
-
- addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) {
- if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) {
- return allValueMappings;
- }
-
- const valueAsNumber = parseFloat(value as string);
- const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
- const toAsNumber = parseFloat(rangeToTextMapping.to as string);
-
- if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
- return allValueMappings;
- }
-
- if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
- return allValueMappings.concat(rangeToTextMapping);
- }
-
- return allValueMappings;
- }
-
- getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) {
- const allFormattedValueMappings = valueMappings.reduce(
- (allValueMappings, valueMapping) => {
- if (valueMapping.type === MappingType.ValueToText) {
- allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
- } else if (valueMapping.type === MappingType.RangeToText) {
- allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
- }
-
- return allValueMappings;
- },
- [] as ValueMapping[]
- );
-
- allFormattedValueMappings.sort((t1, t2) => {
- return t1.id - t2.id;
- });
-
- return allFormattedValueMappings;
- }
-
- getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) {
- return this.getAllFormattedValueMappings(valueMappings, value)[0];
- }
-
formatValue(value: TimeSeriesValue) {
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
@@ -124,7 +60,7 @@ export class Gauge extends PureComponent {
}
if (valueMappings.length > 0) {
- const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value);
+ const valueMappedValue = getMappedValue(valueMappings, value);
if (valueMappedValue) {
return `${prefix} ${valueMappedValue.text} ${suffix}`;
}
@@ -132,8 +68,9 @@ export class Gauge extends PureComponent {
const formatFunc = getValueFormat(unit);
const formattedValue = formatFunc(value as number, decimals);
+ const handleNoValueValue = formattedValue || 'no value';
- return `${prefix} ${formattedValue} ${suffix}`;
+ return `${prefix} ${handleNoValueValue} ${suffix}`;
}
getFontColor(value: TimeSeriesValue) {
@@ -197,7 +134,7 @@ export class Gauge extends PureComponent {
if (timeSeries[0]) {
value = timeSeries[0].stats[stat];
} else {
- value = 'N/A';
+ value = null;
}
const dimension = Math.min(width, height * 1.3);
diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx
index 5246c7cbf15..6d83968d546 100644
--- a/packages/grafana-ui/src/components/Select/Select.tsx
+++ b/packages/grafana-ui/src/components/Select/Select.tsx
@@ -61,7 +61,7 @@ interface AsyncProps {
export const MenuList = (props: any) => {
return (
- {props.children}
+ {props.children}
);
};
diff --git a/packages/grafana-ui/src/utils/valueMappings.test.ts b/packages/grafana-ui/src/utils/valueMappings.test.ts
new file mode 100644
index 00000000000..d37e0beedab
--- /dev/null
+++ b/packages/grafana-ui/src/utils/valueMappings.test.ts
@@ -0,0 +1,81 @@
+import { getMappedValue } from './valueMappings';
+import { ValueMapping, MappingType } from '../types/panel';
+
+describe('Format value with value mappings', () => {
+ it('should return undefined with no valuemappings', () => {
+ const valueMappings: ValueMapping[] = [];
+ const value = '10';
+
+ expect(getMappedValue(valueMappings, value)).toBeUndefined();
+ });
+
+ it('should return undefined with no matching valuemappings', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
+ ];
+ const value = '10';
+
+ expect(getMappedValue(valueMappings, value)).toBeUndefined();
+ });
+
+ it('should return first matching mapping with lowest id', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+ { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
+ ];
+ const value = '10';
+
+ expect(getMappedValue(valueMappings, value).text).toEqual('1-20');
+ });
+
+ it('should return if value is null and value to text mapping value is null', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+ { id: 1, operator: '', text: '', type: MappingType.ValueToText, value: 'null' },
+ ];
+ const value = null;
+
+ expect(getMappedValue(valueMappings, value).text).toEqual('');
+ });
+
+ it('should return if value is null and range to text mapping from and to is null', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: 'null', to: 'null' },
+ { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ ];
+ const value = null;
+
+ expect(getMappedValue(valueMappings, value).text).toEqual('');
+ });
+
+ it('should return rangeToText mapping where value equals to', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
+ { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ ];
+ const value = '10';
+
+ expect(getMappedValue(valueMappings, value).text).toEqual('1-10');
+ });
+
+ it('should return rangeToText mapping where value equals from', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
+ { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ ];
+ const value = '10';
+
+ expect(getMappedValue(valueMappings, value).text).toEqual('10-20');
+ });
+
+ it('should return rangeToText mapping where value is between from and to', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+ { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ ];
+ const value = '10';
+
+ expect(getMappedValue(valueMappings, value).text).toEqual('1-20');
+ });
+});
diff --git a/packages/grafana-ui/src/utils/valueMappings.ts b/packages/grafana-ui/src/utils/valueMappings.ts
new file mode 100644
index 00000000000..c9b926ea0a4
--- /dev/null
+++ b/packages/grafana-ui/src/utils/valueMappings.ts
@@ -0,0 +1,89 @@
+import { ValueMapping, MappingType, ValueMap, RangeMap } from '../types';
+
+export type TimeSeriesValue = string | number | null;
+
+const addValueToTextMappingText = (
+ allValueMappings: ValueMapping[],
+ valueToTextMapping: ValueMap,
+ value: TimeSeriesValue
+) => {
+ if (valueToTextMapping.value === undefined) {
+ return allValueMappings;
+ }
+
+ if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') {
+ return allValueMappings.concat(valueToTextMapping);
+ }
+
+ const valueAsNumber = parseFloat(value as string);
+ const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
+
+ if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
+ return allValueMappings;
+ }
+
+ if (valueAsNumber !== valueToTextMappingAsNumber) {
+ return allValueMappings;
+ }
+
+ return allValueMappings.concat(valueToTextMapping);
+};
+
+const addRangeToTextMappingText = (
+ allValueMappings: ValueMapping[],
+ rangeToTextMapping: RangeMap,
+ value: TimeSeriesValue
+) => {
+ if (rangeToTextMapping.from === undefined || rangeToTextMapping.to === undefined || value === undefined) {
+ return allValueMappings;
+ }
+
+ if (
+ value === null &&
+ rangeToTextMapping.from &&
+ rangeToTextMapping.to &&
+ rangeToTextMapping.from.toLowerCase() === 'null' &&
+ rangeToTextMapping.to.toLowerCase() === 'null'
+ ) {
+ return allValueMappings.concat(rangeToTextMapping);
+ }
+
+ const valueAsNumber = parseFloat(value as string);
+ const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
+ const toAsNumber = parseFloat(rangeToTextMapping.to as string);
+
+ if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
+ return allValueMappings;
+ }
+
+ if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
+ return allValueMappings.concat(rangeToTextMapping);
+ }
+
+ return allValueMappings;
+};
+
+const getAllFormattedValueMappings = (valueMappings: ValueMapping[], value: TimeSeriesValue) => {
+ const allFormattedValueMappings = valueMappings.reduce(
+ (allValueMappings, valueMapping) => {
+ if (valueMapping.type === MappingType.ValueToText) {
+ allValueMappings = addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
+ } else if (valueMapping.type === MappingType.RangeToText) {
+ allValueMappings = addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
+ }
+
+ return allValueMappings;
+ },
+ [] as ValueMapping[]
+ );
+
+ allFormattedValueMappings.sort((t1, t2) => {
+ return t1.id - t2.id;
+ });
+
+ return allFormattedValueMappings;
+};
+
+export const getMappedValue = (valueMappings: ValueMapping[], value: TimeSeriesValue): ValueMapping => {
+ return getAllFormattedValueMappings(valueMappings, value)[0];
+};
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 0526ee80afe..07cb712f794 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -23,9 +23,9 @@ func (hs *HTTPServer) registerRoutes() {
// not logged in views
r.Get("/", reqSignedIn, hs.Index)
- r.Get("/logout", Logout)
- r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
- r.Get("/login/:name", quota("session"), OAuthLogin)
+ r.Get("/logout", hs.Logout)
+ r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(hs.LoginPost))
+ r.Get("/login/:name", quota("session"), hs.OAuthLogin)
r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index)
@@ -84,11 +84,11 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/signup", hs.Index)
r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
- r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
+ r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(hs.SignUpStep2))
// invited
r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
- r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
+ r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(hs.CompleteInvite))
// reset password
r.Get("/user/password/send-reset-email", hs.Index)
@@ -109,7 +109,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
// api renew session based on remember cookie
- r.Get("/api/login/ping", quota("session"), LoginAPIPing)
+ r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
// authed api
r.Group("/api", func(apiRoute routing.RouteRegister) {
diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go
index 8b66a7a468b..eb1f89e3f22 100644
--- a/pkg/api/common_test.go
+++ b/pkg/api/common_test.go
@@ -5,7 +5,6 @@ import (
"net/http/httptest"
"path/filepath"
- "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@@ -95,13 +94,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
}
type scenarioContext struct {
- m *macaron.Macaron
- context *m.ReqContext
- resp *httptest.ResponseRecorder
- handlerFunc handlerFunc
- defaultHandler macaron.Handler
- req *http.Request
- url string
+ m *macaron.Macaron
+ context *m.ReqContext
+ resp *httptest.ResponseRecorder
+ handlerFunc handlerFunc
+ defaultHandler macaron.Handler
+ req *http.Request
+ url string
+ userAuthTokenService *fakeUserAuthTokenService
}
func (sc *scenarioContext) exec() {
@@ -123,8 +123,30 @@ func setupScenarioContext(url string) *scenarioContext {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
- sc.m.Use(middleware.GetContextHandler())
- sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
+ sc.userAuthTokenService = newFakeUserAuthTokenService()
+ sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService))
return sc
}
+
+type fakeUserAuthTokenService struct {
+ initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
+}
+
+func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
+ return &fakeUserAuthTokenService{
+ initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
+ return false
+ },
+ }
+}
+
+func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
+ return s.initContextWithTokenProvider(ctx, orgID)
+}
+
+func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
+ return nil
+}
+
+func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 6d6cc708496..ed7054050e4 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -166,6 +166,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"viewersCanEdit": setting.ViewersCanEdit,
+ "disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index d4d7b41bec5..7b7c1478a4c 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -11,14 +11,8 @@ import (
"path"
"time"
- "github.com/grafana/grafana/pkg/api/routing"
- "github.com/prometheus/client_golang/prometheus"
-
- "github.com/prometheus/client_golang/prometheus/promhttp"
-
- macaron "gopkg.in/macaron.v1"
-
"github.com/grafana/grafana/pkg/api/live"
+ "github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -27,11 +21,16 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
+ "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/rendering"
+ "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ macaron "gopkg.in/macaron.v1"
)
func init() {
@@ -49,13 +48,14 @@ type HTTPServer struct {
streamManager *live.StreamManager
httpSrv *http.Server
- RouteRegister routing.RouteRegister `inject:""`
- Bus bus.Bus `inject:""`
- RenderService rendering.Service `inject:""`
- Cfg *setting.Cfg `inject:""`
- HooksService *hooks.HooksService `inject:""`
- CacheService *cache.CacheService `inject:""`
- DatasourceCache datasources.CacheService `inject:""`
+ RouteRegister routing.RouteRegister `inject:""`
+ Bus bus.Bus `inject:""`
+ RenderService rendering.Service `inject:""`
+ Cfg *setting.Cfg `inject:""`
+ HooksService *hooks.HooksService `inject:""`
+ CacheService *cache.CacheService `inject:""`
+ DatasourceCache datasources.CacheService `inject:""`
+ AuthTokenService auth.UserAuthTokenService `inject:""`
}
func (hs *HTTPServer) Init() error {
@@ -65,6 +65,8 @@ func (hs *HTTPServer) Init() error {
hs.macaron = hs.newMacaron()
hs.registerRoutes()
+ session.Init(&setting.SessionOptions, setting.SessionConnMaxLifetime)
+
return nil
}
@@ -223,8 +225,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.healthHandler)
m.Use(hs.metricsEndpoint)
- m.Use(middleware.GetContextHandler())
- m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
+ m.Use(middleware.GetContextHandler(hs.AuthTokenService))
m.Use(middleware.OrgRedirect())
// needs to be after context handler
diff --git a/pkg/api/login.go b/pkg/api/login.go
index 05afc40e59a..50c62e0835a 100644
--- a/pkg/api/login.go
+++ b/pkg/api/login.go
@@ -1,6 +1,8 @@
package api
import (
+ "encoding/hex"
+ "net/http"
"net/url"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -9,12 +11,13 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
+ "github.com/grafana/grafana/pkg/util"
)
const (
- ViewIndex = "index"
+ ViewIndex = "index"
+ LoginErrorCookieName = "login_error"
)
func (hs *HTTPServer) LoginView(c *m.ReqContext) {
@@ -34,8 +37,8 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
- if loginError, ok := c.Session.Get("loginError").(string); ok {
- c.Session.Delete("loginError")
+ if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
+ deleteCookie(c, LoginErrorCookieName)
viewData.Settings["loginError"] = loginError
}
@@ -43,7 +46,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
return
}
- if !tryLoginUsingRememberCookie(c) {
+ if !c.IsSignedIn {
c.HTML(200, ViewIndex, viewData)
return
}
@@ -75,56 +78,15 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool {
return false
}
-func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
- // Check auto-login.
- uname := c.GetCookie(setting.CookieUserName)
- if len(uname) == 0 {
- return false
+func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response {
+ if c.IsSignedIn || c.IsAnonymous {
+ return JSON(200, "Logged in")
}
- isSucceed := false
- defer func() {
- if !isSucceed {
- log.Trace("auto-login cookie cleared: %s", uname)
- c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
- c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
- return
- }
- }()
-
- userQuery := m.GetUserByLoginQuery{LoginOrEmail: uname}
- if err := bus.Dispatch(&userQuery); err != nil {
- return false
- }
-
- user := userQuery.Result
-
- // validate remember me cookie
- signingKey := user.Rands + user.Password
- if len(signingKey) < 10 {
- c.Logger.Error("Invalid user signingKey")
- return false
- }
-
- if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
- return false
- }
-
- isSucceed = true
- loginUserWithUser(user, c)
- return true
+ return Error(401, "Unauthorized", nil)
}
-func LoginAPIPing(c *m.ReqContext) {
- if !tryLoginUsingRememberCookie(c) {
- c.JsonApiErr(401, "Unauthorized", nil)
- return
- }
-
- c.JsonOK("Logged in")
-}
-
-func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
+func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
if setting.DisableLoginForm {
return Error(401, "Login is disabled", nil)
}
@@ -146,7 +108,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
user := authQuery.User
- loginUserWithUser(user, c)
+ hs.loginUserWithUser(user, c)
result := map[string]interface{}{
"message": "Logged in",
@@ -162,30 +124,60 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
return JSON(200, result)
}
-func loginUserWithUser(user *m.User, c *m.ReqContext) {
+func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
if user == nil {
- log.Error(3, "User login with nil user")
+ hs.log.Error("User login with nil user")
}
- c.Resp.Header().Del("Set-Cookie")
-
- days := 86400 * setting.LogInRememberDays
- if days > 0 {
- c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/")
- c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
+ err := hs.AuthTokenService.UserAuthenticatedHook(user, c)
+ if err != nil {
+ hs.log.Error("User auth hook failed", "error", err)
}
-
- c.Session.RegenerateId(c.Context)
- c.Session.Set(session.SESS_KEY_USERID, user.Id)
}
-func Logout(c *m.ReqContext) {
- c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
- c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
- c.Session.Destory(c.Context)
+func (hs *HTTPServer) Logout(c *m.ReqContext) {
+ hs.AuthTokenService.UserSignedOutHook(c)
+
if setting.SignoutRedirectUrl != "" {
c.Redirect(setting.SignoutRedirectUrl)
} else {
c.Redirect(setting.AppSubUrl + "/login")
}
}
+
+func tryGetEncryptedCookie(ctx *m.ReqContext, cookieName string) (string, bool) {
+ cookie := ctx.GetCookie(cookieName)
+ if cookie == "" {
+ return "", false
+ }
+
+ decoded, err := hex.DecodeString(cookie)
+ if err != nil {
+ return "", false
+ }
+
+ decryptedError, err := util.Decrypt([]byte(decoded), setting.SecretKey)
+ return string(decryptedError), err == nil
+}
+
+func deleteCookie(ctx *m.ReqContext, cookieName string) {
+ ctx.SetCookie(cookieName, "", -1, setting.AppSubUrl+"/")
+}
+
+func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string, value string, maxAge int) error {
+ encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey)
+ if err != nil {
+ return err
+ }
+
+ http.SetCookie(ctx.Resp, &http.Cookie{
+ Name: cookieName,
+ MaxAge: 60,
+ Value: hex.EncodeToString(encryptedError),
+ HttpOnly: true,
+ Path: setting.AppSubUrl + "/",
+ Secure: hs.Cfg.SecurityHTTPSCookies,
+ })
+
+ return nil
+}
diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go
index fe4fa93b621..4160d48733e 100644
--- a/pkg/api/login_oauth.go
+++ b/pkg/api/login_oauth.go
@@ -3,9 +3,11 @@ package api
import (
"context"
"crypto/rand"
+ "crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
+ "encoding/hex"
"fmt"
"io/ioutil"
"net/http"
@@ -18,12 +20,14 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
-var oauthLogger = log.New("oauth")
+var (
+ oauthLogger = log.New("oauth")
+ OauthStateCookieName = "oauth_state"
+)
func GenStateString() string {
rnd := make([]byte, 32)
@@ -31,7 +35,7 @@ func GenStateString() string {
return base64.URLEncoding.EncodeToString(rnd)
}
-func OAuthLogin(ctx *m.ReqContext) {
+func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
if setting.OAuthService == nil {
ctx.Handle(404, "OAuth not enabled", nil)
return
@@ -48,14 +52,15 @@ func OAuthLogin(ctx *m.ReqContext) {
if errorParam != "" {
errorDesc := ctx.Query("error_description")
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
- redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
+ hs.redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
return
}
code := ctx.Query("code")
if code == "" {
state := GenStateString()
- ctx.Session.Set(session.SESS_KEY_OAUTH_STATE, state)
+ hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
+ hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60)
if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
} else {
@@ -64,14 +69,20 @@ func OAuthLogin(ctx *m.ReqContext) {
return
}
- savedState, ok := ctx.Session.Get(session.SESS_KEY_OAUTH_STATE).(string)
- if !ok {
+ cookieState := ctx.GetCookie(OauthStateCookieName)
+
+ // delete cookie
+ ctx.Resp.Header().Del("Set-Cookie")
+ hs.deleteCookie(ctx.Resp, OauthStateCookieName)
+
+ if cookieState == "" {
ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
return
}
- queryState := ctx.Query("state")
- if savedState != queryState {
+ queryState := hashStatecode(ctx.Query("state"), setting.OAuthService.OAuthInfos[name].ClientSecret)
+ oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState)
+ if cookieState != queryState {
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
return
}
@@ -131,7 +142,7 @@ func OAuthLogin(ctx *m.ReqContext) {
userInfo, err := connect.UserInfo(client, token)
if err != nil {
if sErr, ok := err.(*social.Error); ok {
- redirectWithError(ctx, sErr)
+ hs.redirectWithError(ctx, sErr)
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
@@ -142,13 +153,13 @@ func OAuthLogin(ctx *m.ReqContext) {
// validate that we got at least an email address
if userInfo.Email == "" {
- redirectWithError(ctx, login.ErrNoEmail)
+ hs.redirectWithError(ctx, login.ErrNoEmail)
return
}
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
- redirectWithError(ctx, login.ErrEmailNotAllowed)
+ hs.redirectWithError(ctx, login.ErrEmailNotAllowed)
return
}
@@ -171,14 +182,15 @@ func OAuthLogin(ctx *m.ReqContext) {
ExternalUser: extUser,
SignupAllowed: connect.IsSignupAllowed(),
}
+
err = bus.Dispatch(cmd)
if err != nil {
- redirectWithError(ctx, err)
+ hs.redirectWithError(ctx, err)
return
}
// login
- loginUserWithUser(cmd.Result, ctx)
+ hs.loginUserWithUser(cmd.Result, ctx)
metrics.M_Api_Login_OAuth.Inc()
@@ -191,8 +203,29 @@ func OAuthLogin(ctx *m.ReqContext) {
ctx.Redirect(setting.AppSubUrl + "/")
}
-func redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
+func (hs *HTTPServer) deleteCookie(w http.ResponseWriter, name string) {
+ hs.writeCookie(w, name, "", -1)
+}
+
+func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value string, maxAge int) {
+ http.SetCookie(w, &http.Cookie{
+ Name: name,
+ MaxAge: maxAge,
+ Value: value,
+ HttpOnly: true,
+ Path: setting.AppSubUrl + "/",
+ Secure: hs.Cfg.SecurityHTTPSCookies,
+ })
+}
+
+func hashStatecode(code, seed string) string {
+ hashBytes := sha256.Sum256([]byte(code + setting.SecretKey + seed))
+ return hex.EncodeToString(hashBytes[:])
+}
+
+func (hs *HTTPServer) redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
ctx.Logger.Error(err.Error(), v...)
- ctx.Session.Set("loginError", err.Error())
+ hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60)
+
ctx.Redirect(setting.AppSubUrl + "/login")
}
diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go
index dfb2cf045ed..835b03a2cc9 100644
--- a/pkg/api/org_invite.go
+++ b/pkg/api/org_invite.go
@@ -148,7 +148,7 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
})
}
-func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
+func (hs *HTTPServer) CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
if err := bus.Dispatch(&query); err != nil {
@@ -186,7 +186,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
return rsp
}
- loginUserWithUser(user, c)
+ hs.loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc()
metrics.M_Api_User_SignUpInvite.Inc()
diff --git a/pkg/api/signup.go b/pkg/api/signup.go
index 200a3ebc9d1..fe577dd9ef9 100644
--- a/pkg/api/signup.go
+++ b/pkg/api/signup.go
@@ -51,7 +51,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
return JSON(200, util.DynMap{"status": "SignUpCreated"})
}
-func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
+func (hs *HTTPServer) SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
if !setting.AllowUserSignUp {
return Error(401, "User signup is disabled", nil)
}
@@ -109,7 +109,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
apiResponse["code"] = "redirect-to-select-org"
}
- loginUserWithUser(user, c)
+ hs.loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc()
return JSON(200, apiResponse)
diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go
index 5faee1e3fa7..27248342c8d 100644
--- a/pkg/middleware/auth.go
+++ b/pkg/middleware/auth.go
@@ -7,7 +7,6 @@ import (
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -17,16 +16,6 @@ type AuthOptions struct {
ReqSignedIn bool
}
-func getRequestUserId(c *m.ReqContext) int64 {
- userID := c.Session.Get(session.SESS_KEY_USERID)
-
- if userID != nil {
- return userID.(int64)
- }
-
- return 0
-}
-
func getApiKey(c *m.ReqContext) string {
header := c.Req.Header.Get("Authorization")
parts := strings.SplitN(header, " ", 2)
diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go
index fc109ac707f..93ee577e3c6 100644
--- a/pkg/middleware/auth_proxy.go
+++ b/pkg/middleware/auth_proxy.go
@@ -16,7 +16,9 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
-var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+var (
+ AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+)
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
if !setting.AuthProxyEnabled {
@@ -40,6 +42,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
return false
}
+ defer func() {
+ if err := ctx.Session.Release(); err != nil {
+ ctx.Logger.Error("failed to save session data", "error", err)
+ }
+ }()
+
query := &m.GetSignedInUserQuery{OrgId: orgID}
// if this session has already been authenticated by authProxy just load the user
@@ -192,6 +200,16 @@ var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
return nil
}
+func getRequestUserId(c *m.ReqContext) int64 {
+ userID := c.Session.Get(session.SESS_KEY_USERID)
+
+ if userID != nil {
+ return userID.(int64)
+ }
+
+ return 0
+}
+
func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
return nil
diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go
index ace72d998eb..3722ac3058f 100644
--- a/pkg/middleware/middleware.go
+++ b/pkg/middleware/middleware.go
@@ -3,15 +3,15 @@ package middleware
import (
"strconv"
- "gopkg.in/macaron.v1"
-
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
+ macaron "gopkg.in/macaron.v1"
)
var (
@@ -21,12 +21,12 @@ var (
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
)
-func GetContextHandler() macaron.Handler {
+func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
return func(c *macaron.Context) {
ctx := &m.ReqContext{
Context: c,
SignedInUser: &m.SignedInUser{},
- Session: session.GetSession(),
+ Session: session.GetSession(), // should only be used by auth_proxy
IsSignedIn: false,
AllowAnonymous: false,
SkipCache: false,
@@ -49,7 +49,7 @@ func GetContextHandler() macaron.Handler {
case initContextWithApiKey(ctx):
case initContextWithBasicAuth(ctx, orgId):
case initContextWithAuthProxy(ctx, orgId):
- case initContextWithUserSessionCookie(ctx, orgId):
+ case ats.InitContextWithToken(ctx, orgId):
case initContextWithAnonymousUser(ctx):
}
@@ -88,29 +88,6 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
return true
}
-func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool {
- // initialize session
- if err := ctx.Session.Start(ctx.Context); err != nil {
- ctx.Logger.Error("Failed to start session", "error", err)
- return false
- }
-
- var userId int64
- if userId = getRequestUserId(ctx); userId == 0 {
- return false
- }
-
- query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
- if err := bus.Dispatch(&query); err != nil {
- ctx.Logger.Error("Failed to get user with id", "userId", userId, "error", err)
- return false
- }
-
- ctx.SignedInUser = query.Result
- ctx.IsSignedIn = true
- return true
-}
-
func initContextWithApiKey(ctx *m.ReqContext) bool {
var keyString string
if keyString = getApiKey(ctx); keyString == "" {
diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
index b9a8afce6c6..11740574d0b 100644
--- a/pkg/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"
- ms "github.com/go-macaron/session"
+ msession "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
@@ -43,11 +43,6 @@ func TestMiddlewareContext(t *testing.T) {
So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
})
- middlewareScenario("Non api request should init session", func(sc *scenarioContext) {
- sc.fakeReq("GET", "/").exec()
- So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess")
- })
-
middlewareScenario("Invalid api key", func(sc *scenarioContext) {
sc.apiKey = "invalid_key_test"
sc.fakeReq("GET", "/").exec()
@@ -151,22 +146,17 @@ func TestMiddlewareContext(t *testing.T) {
})
})
- middlewareScenario("UserId in session", func(sc *scenarioContext) {
-
- sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
- c.Session.Set(session.SESS_KEY_USERID, int64(12))
- }).exec()
-
- bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
- query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
- return nil
- })
+ middlewareScenario("Auth token service", func(sc *scenarioContext) {
+ var wasCalled bool
+ sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+ wasCalled = true
+ return false
+ }
sc.fakeReq("GET", "/").exec()
- Convey("should init context with user info", func() {
- So(sc.context.IsSignedIn, ShouldBeTrue)
- So(sc.context.UserId, ShouldEqual, 12)
+ Convey("should call middleware", func() {
+ So(wasCalled, ShouldBeTrue)
})
})
@@ -211,6 +201,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
+ setting.SessionOptions = msession.Options{}
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.exec()
@@ -479,6 +470,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
defer bus.ClearBusHandlers()
sc := &scenarioContext{}
+
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
@@ -487,10 +479,13 @@ func middlewareScenario(desc string, fn scenarioFunc) {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
- sc.m.Use(GetContextHandler())
+ session.Init(&msession.Options{}, 0)
+ sc.userAuthTokenService = newFakeUserAuthTokenService()
+ sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
- sc.m.Use(Sessioner(&ms.Options{}, 0))
+ setting.SessionOptions = msession.Options{}
+
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
@@ -508,15 +503,16 @@ func middlewareScenario(desc string, fn scenarioFunc) {
}
type scenarioContext struct {
- m *macaron.Macaron
- context *m.ReqContext
- resp *httptest.ResponseRecorder
- apiKey string
- authHeader string
- respJson map[string]interface{}
- handlerFunc handlerFunc
- defaultHandler macaron.Handler
- url string
+ m *macaron.Macaron
+ context *m.ReqContext
+ resp *httptest.ResponseRecorder
+ apiKey string
+ authHeader string
+ respJson map[string]interface{}
+ handlerFunc handlerFunc
+ defaultHandler macaron.Handler
+ url string
+ userAuthTokenService *fakeUserAuthTokenService
req *http.Request
}
@@ -585,3 +581,25 @@ func (sc *scenarioContext) exec() {
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext)
+
+type fakeUserAuthTokenService struct {
+ initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
+}
+
+func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
+ return &fakeUserAuthTokenService{
+ initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
+ return false
+ },
+ }
+}
+
+func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
+ return s.initContextWithTokenProvider(ctx, orgID)
+}
+
+func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
+ return nil
+}
+
+func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go
index db263c2a17a..ca63733946c 100644
--- a/pkg/middleware/org_redirect.go
+++ b/pkg/middleware/org_redirect.go
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
-
"gopkg.in/macaron.v1"
)
diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go
index fa08154b250..46b8776fdcc 100644
--- a/pkg/middleware/org_redirect_test.go
+++ b/pkg/middleware/org_redirect_test.go
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/session"
. "github.com/smartystreets/goconvey/convey"
)
@@ -15,18 +14,15 @@ func TestOrgRedirectMiddleware(t *testing.T) {
Convey("Can redirect to correct org", t, func() {
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
- sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
- c.Session.Set(session.SESS_KEY_USERID, int64(12))
- }).exec()
-
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return nil
})
- bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
- query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
- return nil
- })
+ sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+ ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
+ ctx.IsSignedIn = true
+ return true
+ }
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
@@ -37,14 +33,16 @@ func TestOrgRedirectMiddleware(t *testing.T) {
})
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
- sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
- c.Session.Set(session.SESS_KEY_USERID, int64(12))
- }).exec()
-
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
+ sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+ ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
+ ctx.IsSignedIn = true
+ return true
+ }
+
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
return nil
diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go
index 92c3d62674d..4f2203a5d3d 100644
--- a/pkg/middleware/quota_test.go
+++ b/pkg/middleware/quota_test.go
@@ -74,15 +74,12 @@ func TestMiddlewareQuota(t *testing.T) {
})
middlewareScenario("with user logged in", func(sc *scenarioContext) {
- // log us in, so we have a user_id and org_id in the context
- sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
- c.Session.Set(session.SESS_KEY_USERID, int64(12))
- }).exec()
+ sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+ ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12}
+ ctx.IsSignedIn = true
+ return true
+ }
- bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
- query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
- return nil
- })
bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
query.Result = &m.GlobalQuotaDTO{
Target: query.Target,
diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go
index c92150f3b7d..e041d42e56b 100644
--- a/pkg/middleware/recovery_test.go
+++ b/pkg/middleware/recovery_test.go
@@ -4,13 +4,12 @@ import (
"path/filepath"
"testing"
- ms "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
- "gopkg.in/macaron.v1"
+ macaron "gopkg.in/macaron.v1"
)
func TestRecoveryMiddleware(t *testing.T) {
@@ -64,10 +63,10 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
- sc.m.Use(GetContextHandler())
+ sc.userAuthTokenService = newFakeUserAuthTokenService()
+ sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
- sc.m.Use(Sessioner(&ms.Options{}, 0))
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go
deleted file mode 100644
index 19cfa368b49..00000000000
--- a/pkg/middleware/session.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package middleware
-
-import (
- ms "github.com/go-macaron/session"
- "gopkg.in/macaron.v1"
-
- m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/session"
-)
-
-func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler {
- session.Init(options, sessionConnMaxLifetime)
-
- return func(ctx *m.ReqContext) {
- ctx.Next()
-
- if err := ctx.Session.Release(); err != nil {
- panic("session(release): " + err.Error())
- }
- }
-}
diff --git a/pkg/models/context.go b/pkg/models/context.go
index 7cb80a957c3..df970451304 100644
--- a/pkg/models/context.go
+++ b/pkg/models/context.go
@@ -3,18 +3,18 @@ package models
import (
"strings"
- "github.com/prometheus/client_golang/prometheus"
- "gopkg.in/macaron.v1"
-
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
+ "github.com/prometheus/client_golang/prometheus"
+ "gopkg.in/macaron.v1"
)
type ReqContext struct {
*macaron.Context
*SignedInUser
+ // This should only be used by the auth_proxy
Session session.SessionStore
IsSignedIn bool
diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go
index 0f8e24bcef5..22cbe2456b7 100644
--- a/pkg/services/alerting/engine.go
+++ b/pkg/services/alerting/engine.go
@@ -105,8 +105,9 @@ func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
var (
unfinishedWorkTimeout = time.Second * 5
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
- alertTimeout = time.Second * 30
- alertMaxAttempts = 3
+ alertTimeout = time.Second * 30
+ resultHandleTimeout = time.Second * 30
+ alertMaxAttempts = 3
)
func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
@@ -116,7 +117,7 @@ func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *J
}
}()
- cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
+ cancelChan := make(chan context.CancelFunc, alertMaxAttempts*2)
attemptChan := make(chan int, 1)
// Initialize with first attemptID=1
@@ -204,6 +205,15 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel
}
}
+ // create new context with timeout for notifications
+ resultHandleCtx, resultHandleCancelFn := context.WithTimeout(context.Background(), resultHandleTimeout)
+ cancelChan <- resultHandleCancelFn
+
+ // override the context used for evaluation with a new context for notifications.
+ // This makes it possible for notifiers to execute when datasources
+ // dont respond within the timeout limit. We should rewrite this so notifications
+ // dont reuse the evalContext and get its own context.
+ evalContext.Ctx = resultHandleCtx
evalContext.Rule.State = evalContext.GetNewState()
e.resultHandler.Handle(evalContext)
span.Finish()
diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go
new file mode 100644
index 00000000000..aa518baae24
--- /dev/null
+++ b/pkg/services/alerting/engine_integration_test.go
@@ -0,0 +1,148 @@
+// +build integration
+
+package alerting
+
+import (
+ "context"
+ "errors"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestEngineTimeouts(t *testing.T) {
+ Convey("Alerting engine timeout tests", t, func() {
+ engine := NewEngine()
+ engine.resultHandler = &FakeResultHandler{}
+ job := &Job{Running: true, Rule: &Rule{}}
+
+ Convey("Should trigger as many retries as needed", func() {
+ Convey("pended alert for datasource -> result handler should be worked", func() {
+ // reduce alert timeout to test quickly
+ originAlertTimeout := alertTimeout
+ alertTimeout = 2 * time.Second
+ transportTimeoutInterval := 2 * time.Second
+ serverBusySleepDuration := 1 * time.Second
+
+ evalHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
+ resultHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
+ engine.evalHandler = evalHandler
+ engine.resultHandler = resultHandler
+
+ engine.processJobWithRetry(context.TODO(), job)
+
+ So(evalHandler.EvalSucceed, ShouldEqual, true)
+ So(resultHandler.ResultHandleSucceed, ShouldEqual, true)
+
+ // initialize for other tests.
+ alertTimeout = originAlertTimeout
+ engine.resultHandler = &FakeResultHandler{}
+ })
+ })
+ })
+}
+
+type FakeCommonTimeoutHandler struct {
+ TransportTimeoutDuration time.Duration
+ ServerBusySleepDuration time.Duration
+ EvalSucceed bool
+ ResultHandleSucceed bool
+}
+
+func NewFakeCommonTimeoutHandler(transportTimeoutDuration time.Duration, serverBusySleepDuration time.Duration) *FakeCommonTimeoutHandler {
+ return &FakeCommonTimeoutHandler{
+ TransportTimeoutDuration: transportTimeoutDuration,
+ ServerBusySleepDuration: serverBusySleepDuration,
+ EvalSucceed: false,
+ ResultHandleSucceed: false,
+ }
+}
+
+func (handler *FakeCommonTimeoutHandler) Eval(evalContext *EvalContext) {
+ // 1. prepare mock server
+ path := "/evaltimeout"
+ srv := runBusyServer(path, handler.ServerBusySleepDuration)
+ defer srv.Close()
+
+ // 2. send requests
+ url := srv.URL + path
+ res, err := sendRequest(evalContext.Ctx, url, handler.TransportTimeoutDuration)
+ if res != nil {
+ defer res.Body.Close()
+ }
+
+ if err != nil {
+ evalContext.Error = errors.New("Fake evaluation timeout test failure")
+ return
+ }
+
+ if res.StatusCode == 200 {
+ handler.EvalSucceed = true
+ }
+
+ evalContext.Error = errors.New("Fake evaluation timeout test failure; wrong response")
+}
+
+func (handler *FakeCommonTimeoutHandler) Handle(evalContext *EvalContext) error {
+ // 1. prepare mock server
+ path := "/resulthandle"
+ srv := runBusyServer(path, handler.ServerBusySleepDuration)
+ defer srv.Close()
+
+ // 2. send requests
+ url := srv.URL + path
+ res, err := sendRequest(evalContext.Ctx, url, handler.TransportTimeoutDuration)
+ if res != nil {
+ defer res.Body.Close()
+ }
+
+ if err != nil {
+ evalContext.Error = errors.New("Fake result handle timeout test failure")
+ return evalContext.Error
+ }
+
+ if res.StatusCode == 200 {
+ handler.ResultHandleSucceed = true
+ return nil
+ }
+
+ evalContext.Error = errors.New("Fake result handle timeout test failure; wrong response")
+
+ return evalContext.Error
+}
+
+func runBusyServer(path string, serverBusySleepDuration time.Duration) *httptest.Server {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+
+ mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(serverBusySleepDuration)
+ })
+
+ return server
+}
+
+func sendRequest(context context.Context, url string, transportTimeoutInterval time.Duration) (resp *http.Response, err error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req = req.WithContext(context)
+
+ transport := http.Transport{
+ Dial: (&net.Dialer{
+ Timeout: transportTimeoutInterval,
+ KeepAlive: transportTimeoutInterval,
+ }).Dial,
+ }
+ client := http.Client{
+ Transport: &transport,
+ }
+
+ return client.Do(req)
+}
diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go
new file mode 100644
index 00000000000..7e9433c2d70
--- /dev/null
+++ b/pkg/services/auth/auth_token.go
@@ -0,0 +1,266 @@
+package auth
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/infra/serverlock"
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/registry"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/setting"
+ "github.com/grafana/grafana/pkg/util"
+)
+
+func init() {
+ registry.RegisterService(&UserAuthTokenServiceImpl{})
+}
+
+var (
+ getTime = time.Now
+ UrgentRotateTime = 1 * time.Minute
+ oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often.
+)
+
+// UserAuthTokenService are used for generating and validating user auth tokens
+type UserAuthTokenService interface {
+ InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
+ UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
+ UserSignedOutHook(c *models.ReqContext)
+}
+
+type UserAuthTokenServiceImpl struct {
+ SQLStore *sqlstore.SqlStore `inject:""`
+ ServerLockService *serverlock.ServerLockService `inject:""`
+ Cfg *setting.Cfg `inject:""`
+ log log.Logger
+}
+
+// Init this service
+func (s *UserAuthTokenServiceImpl) Init() error {
+ s.log = log.New("auth")
+ return nil
+}
+
+func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool {
+ //auth User
+ unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName)
+ if unhashedToken == "" {
+ return false
+ }
+
+ userToken, err := s.LookupToken(unhashedToken)
+ if err != nil {
+ ctx.Logger.Info("failed to look up user based on cookie", "error", err)
+ return false
+ }
+
+ query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID}
+ if err := bus.Dispatch(&query); err != nil {
+ ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err)
+ return false
+ }
+
+ ctx.SignedInUser = query.Result
+ ctx.IsSignedIn = true
+
+ //rotate session token if needed.
+ rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent())
+ if err != nil {
+ ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id)
+ return true
+ }
+
+ if rotated {
+ s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds)
+ }
+
+ return true
+}
+
+func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
+ if setting.Env == setting.DEV {
+ ctx.Logger.Info("new token", "unhashed token", value)
+ }
+
+ ctx.Resp.Header().Del("Set-Cookie")
+ cookie := http.Cookie{
+ Name: s.Cfg.LoginCookieName,
+ Value: url.QueryEscape(value),
+ HttpOnly: true,
+ Path: setting.AppSubUrl + "/",
+ Secure: s.Cfg.SecurityHTTPSCookies,
+ MaxAge: maxAge,
+ }
+
+ http.SetCookie(ctx.Resp, &cookie)
+}
+
+func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error {
+ userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
+ if err != nil {
+ return err
+ }
+
+ s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds)
+ return nil
+}
+
+func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) {
+ s.writeSessionCookie(c, "", -1)
+}
+
+func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
+ clientIP = util.ParseIPAddress(clientIP)
+ token, err := util.RandomHex(16)
+ if err != nil {
+ return nil, err
+ }
+
+ hashedToken := hashToken(token)
+
+ now := getTime().Unix()
+
+ userToken := userAuthToken{
+ UserId: userId,
+ AuthToken: hashedToken,
+ PrevAuthToken: hashedToken,
+ ClientIp: clientIP,
+ UserAgent: userAgent,
+ RotatedAt: now,
+ CreatedAt: now,
+ UpdatedAt: now,
+ SeenAt: 0,
+ AuthTokenSeen: false,
+ }
+ _, err = s.SQLStore.NewSession().Insert(&userToken)
+ if err != nil {
+ return nil, err
+ }
+
+ userToken.UnhashedToken = token
+
+ return &userToken, nil
+}
+
+func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
+ hashedToken := hashToken(unhashedToken)
+ if setting.Env == setting.DEV {
+ s.log.Info("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
+ }
+
+ expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
+
+ var userToken userAuthToken
+ exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken)
+ if err != nil {
+ return nil, err
+ }
+
+ if !exists {
+ return nil, ErrAuthTokenNotFound
+ }
+
+ if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen {
+ userTokenCopy := userToken
+ userTokenCopy.AuthTokenSeen = false
+ expireBefore := getTime().Add(-UrgentRotateTime).Unix()
+ affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy)
+ if err != nil {
+ return nil, err
+ }
+
+ if affectedRows == 0 {
+ s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+ } else {
+ s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+ }
+ }
+
+ if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken {
+ userTokenCopy := userToken
+ userTokenCopy.AuthTokenSeen = true
+ userTokenCopy.SeenAt = getTime().Unix()
+ affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy)
+ if err != nil {
+ return nil, err
+ }
+
+ if affectedRows == 1 {
+ userToken = userTokenCopy
+ }
+
+ if affectedRows == 0 {
+ s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+ } else {
+ s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+ }
+ }
+
+ userToken.UnhashedToken = unhashedToken
+
+ return &userToken, nil
+}
+
+func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) {
+ if token == nil {
+ return false, nil
+ }
+
+ now := getTime()
+
+ needsRotation := false
+ rotatedAt := time.Unix(token.RotatedAt, 0)
+ if token.AuthTokenSeen {
+ needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute))
+ } else {
+ needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime))
+ }
+
+ if !needsRotation {
+ return false, nil
+ }
+
+ s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id)
+
+ clientIP = util.ParseIPAddress(clientIP)
+ newToken, _ := util.RandomHex(16)
+ hashedToken := hashToken(newToken)
+
+ // very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
+ sql := `
+ UPDATE user_auth_token
+ SET
+ seen_at = 0,
+ user_agent = ?,
+ client_ip = ?,
+ prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
+ auth_token = ?,
+ auth_token_seen = ?,
+ rotated_at = ?
+ WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
+
+ res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
+ if err != nil {
+ return false, err
+ }
+
+ affected, _ := res.RowsAffected()
+ s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId)
+ if affected > 0 {
+ token.UnhashedToken = newToken
+ return true, nil
+ }
+
+ return false, nil
+}
+
+func hashToken(token string) string {
+ hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
+ return hex.EncodeToString(hashBytes[:])
+}
diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go
new file mode 100644
index 00000000000..2f75c660d9d
--- /dev/null
+++ b/pkg/services/auth/auth_token_test.go
@@ -0,0 +1,339 @@
+package auth
+
+import (
+ "testing"
+ "time"
+
+ "github.com/grafana/grafana/pkg/setting"
+
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserAuthToken(t *testing.T) {
+ Convey("Test user auth token", t, func() {
+ ctx := createTestContext(t)
+ userAuthTokenService := ctx.tokenService
+ userID := int64(10)
+
+ t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
+ getTime = func() time.Time {
+ return t
+ }
+
+ Convey("When creating token", func() {
+ token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+ So(token.AuthTokenSeen, ShouldBeFalse)
+
+ Convey("When lookup unhashed token should return user auth token", func() {
+ LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(LookupToken, ShouldNotBeNil)
+ So(LookupToken.UserId, ShouldEqual, userID)
+ So(LookupToken.AuthTokenSeen, ShouldBeTrue)
+
+ storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id)
+ So(err, ShouldBeNil)
+ So(storedAuthToken, ShouldNotBeNil)
+ So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
+ })
+
+ Convey("When lookup hashed token should return user auth token not found error", func() {
+ LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken)
+ So(err, ShouldEqual, ErrAuthTokenNotFound)
+ So(LookupToken, ShouldBeNil)
+ })
+ })
+
+ Convey("expires correctly", func() {
+ token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+
+ _, err = userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+
+ token, err = ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+
+ getTime = func() time.Time {
+ return t.Add(time.Hour)
+ }
+
+ refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ _, err = userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+
+ stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(stillGood, ShouldNotBeNil)
+
+ getTime = func() time.Time {
+ return t.Add(24 * 7 * time.Hour)
+ }
+ notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldEqual, ErrAuthTokenNotFound)
+ So(notGood, ShouldBeNil)
+ })
+
+ Convey("can properly rotate tokens", func() {
+ token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+
+ prevToken := token.AuthToken
+ unhashedPrev := token.UnhashedToken
+
+ refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeFalse)
+
+ updated, err := ctx.markAuthTokenAsSeen(token.Id)
+ So(err, ShouldBeNil)
+ So(updated, ShouldBeTrue)
+
+ token, err = ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+
+ getTime = func() time.Time {
+ return t.Add(time.Hour)
+ }
+
+ refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ unhashedToken := token.UnhashedToken
+
+ token, err = ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+ token.UnhashedToken = unhashedToken
+
+ So(token.RotatedAt, ShouldEqual, getTime().Unix())
+ So(token.ClientIp, ShouldEqual, "192.168.10.12")
+ So(token.UserAgent, ShouldEqual, "a new user agent")
+ So(token.AuthTokenSeen, ShouldBeFalse)
+ So(token.SeenAt, ShouldEqual, 0)
+ So(token.PrevAuthToken, ShouldEqual, prevToken)
+
+ // ability to auth using an old token
+
+ lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+ So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+ So(lookedUp.SeenAt, ShouldEqual, getTime().Unix())
+
+ lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+ So(lookedUp.Id, ShouldEqual, token.Id)
+ So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+
+ getTime = func() time.Time {
+ return t.Add(time.Hour + (2 * time.Minute))
+ }
+
+ lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+ So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+
+ lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+ So(lookedUp.AuthTokenSeen, ShouldBeFalse)
+
+ refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ token, err = ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+ So(token.SeenAt, ShouldEqual, 0)
+ })
+
+ Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
+ token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+
+ lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+
+ getTime = func() time.Time {
+ return t.Add(10 * time.Minute)
+ }
+
+ prevToken := token.UnhashedToken
+ refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ getTime = func() time.Time {
+ return t.Add(20 * time.Minute)
+ }
+
+ current, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(current, ShouldNotBeNil)
+
+ prev, err := userAuthTokenService.LookupToken(prevToken)
+ So(err, ShouldBeNil)
+ So(prev, ShouldNotBeNil)
+ })
+
+ Convey("will not mark token unseen when prev and current are the same", func() {
+ token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+
+ lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+
+ lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+
+ lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
+ So(err, ShouldBeNil)
+ So(lookedUp, ShouldNotBeNil)
+ So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+ })
+
+ Convey("Rotate token", func() {
+ token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+
+ prevToken := token.AuthToken
+
+ Convey("Should rotate current token and previous token when auth token seen", func() {
+ updated, err := ctx.markAuthTokenAsSeen(token.Id)
+ So(err, ShouldBeNil)
+ So(updated, ShouldBeTrue)
+
+ getTime = func() time.Time {
+ return t.Add(10 * time.Minute)
+ }
+
+ refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ storedToken, err := ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+ So(storedToken, ShouldNotBeNil)
+ So(storedToken.AuthTokenSeen, ShouldBeFalse)
+ So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
+ So(storedToken.AuthToken, ShouldNotEqual, prevToken)
+
+ prevToken = storedToken.AuthToken
+
+ updated, err = ctx.markAuthTokenAsSeen(token.Id)
+ So(err, ShouldBeNil)
+ So(updated, ShouldBeTrue)
+
+ getTime = func() time.Time {
+ return t.Add(20 * time.Minute)
+ }
+
+ refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ storedToken, err = ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+ So(storedToken, ShouldNotBeNil)
+ So(storedToken.AuthTokenSeen, ShouldBeFalse)
+ So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
+ So(storedToken.AuthToken, ShouldNotEqual, prevToken)
+ })
+
+ Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
+ token.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
+
+ getTime = func() time.Time {
+ return t.Add(2 * time.Minute)
+ }
+
+ refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+ So(err, ShouldBeNil)
+ So(refreshed, ShouldBeTrue)
+
+ storedToken, err := ctx.getAuthTokenByID(token.Id)
+ So(err, ShouldBeNil)
+ So(storedToken, ShouldNotBeNil)
+ So(storedToken.AuthTokenSeen, ShouldBeFalse)
+ So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
+ So(storedToken.AuthToken, ShouldNotEqual, prevToken)
+ })
+ })
+
+ Reset(func() {
+ getTime = time.Now
+ })
+ })
+}
+
+func createTestContext(t *testing.T) *testContext {
+ t.Helper()
+
+ sqlstore := sqlstore.InitTestDB(t)
+ tokenService := &UserAuthTokenServiceImpl{
+ SQLStore: sqlstore,
+ Cfg: &setting.Cfg{
+ LoginCookieName: "grafana_session",
+ LoginCookieMaxDays: 7,
+ LoginDeleteExpiredTokensAfterDays: 30,
+ LoginCookieRotation: 10,
+ },
+ log: log.New("test-logger"),
+ }
+
+ UrgentRotateTime = time.Minute
+
+ return &testContext{
+ sqlstore: sqlstore,
+ tokenService: tokenService,
+ }
+}
+
+type testContext struct {
+ sqlstore *sqlstore.SqlStore
+ tokenService *UserAuthTokenServiceImpl
+}
+
+func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
+ sess := c.sqlstore.NewSession()
+ var t userAuthToken
+ found, err := sess.ID(id).Get(&t)
+ if err != nil || !found {
+ return nil, err
+ }
+
+ return &t, nil
+}
+
+func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
+ sess := c.sqlstore.NewSession()
+ res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id)
+ if err != nil {
+ return false, err
+ }
+
+ rowsAffected, err := res.RowsAffected()
+ if err != nil {
+ return false, err
+ }
+ return rowsAffected == 1, nil
+}
diff --git a/pkg/services/auth/model.go b/pkg/services/auth/model.go
new file mode 100644
index 00000000000..7a0f49539f2
--- /dev/null
+++ b/pkg/services/auth/model.go
@@ -0,0 +1,25 @@
+package auth
+
+import (
+ "errors"
+)
+
+// Typed errors
+var (
+ ErrAuthTokenNotFound = errors.New("User auth token not found")
+)
+
+type userAuthToken struct {
+ Id int64
+ UserId int64
+ AuthToken string
+ PrevAuthToken string
+ UserAgent string
+ ClientIp string
+ AuthTokenSeen bool
+ SeenAt int64
+ RotatedAt int64
+ CreatedAt int64
+ UpdatedAt int64
+ UnhashedToken string `xorm:"-"`
+}
diff --git a/pkg/services/auth/session_cleanup.go b/pkg/services/auth/session_cleanup.go
new file mode 100644
index 00000000000..7e523181a7b
--- /dev/null
+++ b/pkg/services/auth/session_cleanup.go
@@ -0,0 +1,38 @@
+package auth
+
+import (
+ "context"
+ "time"
+)
+
+func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error {
+ ticker := time.NewTicker(time.Hour * 12)
+ deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.LoginDeleteExpiredTokensAfterDays)
+
+ for {
+ select {
+ case <-ticker.C:
+ srv.ServerLockService.LockAndExecute(ctx, "delete old sessions", time.Hour*12, func() {
+ srv.deleteOldSession(deleteSessionAfter)
+ })
+
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+func (srv *UserAuthTokenServiceImpl) deleteOldSession(deleteSessionAfter time.Duration) (int64, error) {
+ sql := `DELETE from user_auth_token WHERE rotated_at < ?`
+
+ deleteBefore := getTime().Add(-deleteSessionAfter)
+ res, err := srv.SQLStore.NewSession().Exec(sql, deleteBefore.Unix())
+ if err != nil {
+ return 0, err
+ }
+
+ affected, err := res.RowsAffected()
+ srv.log.Info("deleted old sessions", "count", affected)
+
+ return affected, err
+}
diff --git a/pkg/services/auth/session_cleanup_test.go b/pkg/services/auth/session_cleanup_test.go
new file mode 100644
index 00000000000..eef2cd74d04
--- /dev/null
+++ b/pkg/services/auth/session_cleanup_test.go
@@ -0,0 +1,36 @@
+package auth
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserAuthTokenCleanup(t *testing.T) {
+
+ Convey("Test user auth token cleanup", t, func() {
+ ctx := createTestContext(t)
+
+ insertToken := func(token string, prev string, rotatedAt int64) {
+ ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
+ _, err := ctx.sqlstore.NewSession().Insert(&ut)
+ So(err, ShouldBeNil)
+ }
+
+ // insert three old tokens that should be deleted
+ for i := 0; i < 3; i++ {
+ insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), int64(i))
+ }
+
+ // insert three active tokens that should not be deleted
+ for i := 0; i < 3; i++ {
+ insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), getTime().Unix())
+ }
+
+ affected, err := ctx.tokenService.deleteOldSession(time.Hour)
+ So(err, ShouldBeNil)
+ So(affected, ShouldEqual, 3)
+ })
+}
diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go
index 59ceefa0be5..33f418cbee3 100644
--- a/pkg/services/dashboards/dashboard_service.go
+++ b/pkg/services/dashboards/dashboard_service.go
@@ -164,11 +164,7 @@ func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand,
User: dto.User,
}
- if err := bus.Dispatch(&alertCmd); err != nil {
- return err
- }
-
- return nil
+ return bus.Dispatch(&alertCmd)
}
func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
diff --git a/pkg/services/session/session.go b/pkg/services/session/session.go
index 5873a6a5b72..2e60b8a25d7 100644
--- a/pkg/services/session/session.go
+++ b/pkg/services/session/session.go
@@ -14,8 +14,6 @@ import (
const (
SESS_KEY_USERID = "uid"
- SESS_KEY_OAUTH_STATE = "state"
- SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys
SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
)
diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go
index 36cd8e5ed62..931259ec3ed 100644
--- a/pkg/services/sqlstore/migrations/migrations.go
+++ b/pkg/services/sqlstore/migrations/migrations.go
@@ -32,6 +32,7 @@ func AddMigrations(mg *Migrator) {
addLoginAttemptMigrations(mg)
addUserAuthMigrations(mg)
addServerlockMigrations(mg)
+ addUserAuthTokenMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
diff --git a/pkg/services/sqlstore/migrations/user_auth_token_mig.go b/pkg/services/sqlstore/migrations/user_auth_token_mig.go
new file mode 100644
index 00000000000..9794b7a78c7
--- /dev/null
+++ b/pkg/services/sqlstore/migrations/user_auth_token_mig.go
@@ -0,0 +1,32 @@
+package migrations
+
+import (
+ . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+)
+
+func addUserAuthTokenMigrations(mg *Migrator) {
+ userAuthTokenV1 := Table{
+ Name: "user_auth_token",
+ Columns: []*Column{
+ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+ {Name: "user_id", Type: DB_BigInt, Nullable: false},
+ {Name: "auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
+ {Name: "prev_auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
+ {Name: "user_agent", Type: DB_NVarchar, Length: 255, Nullable: false},
+ {Name: "client_ip", Type: DB_NVarchar, Length: 255, Nullable: false},
+ {Name: "auth_token_seen", Type: DB_Bool, Nullable: false},
+ {Name: "seen_at", Type: DB_Int, Nullable: true},
+ {Name: "rotated_at", Type: DB_Int, Nullable: false},
+ {Name: "created_at", Type: DB_Int, Nullable: false},
+ {Name: "updated_at", Type: DB_Int, Nullable: false},
+ },
+ Indices: []*Index{
+ {Cols: []string{"auth_token"}, Type: UniqueIndex},
+ {Cols: []string{"prev_auth_token"}, Type: UniqueIndex},
+ },
+ }
+
+ mg.AddMigration("create user auth token table", NewAddTableMigration(userAuthTokenV1))
+ mg.AddMigration("add unique index user_auth_token.auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[0]))
+ mg.AddMigration("add unique index user_auth_token.prev_auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[1]))
+}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index 1417392fdf8..d1eca777004 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -18,7 +18,7 @@ import (
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
- "gopkg.in/ini.v1"
+ ini "gopkg.in/ini.v1"
)
type Scheme string
@@ -83,9 +83,6 @@ var (
// Security settings.
SecretKey string
- LogInRememberDays int
- CookieUserName string
- CookieRememberName string
DisableGravatar bool
EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool
@@ -222,7 +219,15 @@ type Cfg struct {
MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string
EnableAlphaPanels bool
+ DisableSanitizeHtml bool
EnterpriseLicensePath string
+
+ LoginCookieName string
+ LoginCookieMaxDays int
+ LoginCookieRotation int
+ LoginDeleteExpiredTokensAfterDays int
+
+ SecurityHTTPSCookies bool
}
type CommandLineArgs struct {
@@ -546,6 +551,16 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
ApplicationName = APP_NAME_ENTERPRISE
}
+ //login
+ login := iniFile.Section("login")
+ cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
+ cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
+ cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30)
+ cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10)
+ if cfg.LoginCookieRotation < 2 {
+ cfg.LoginCookieRotation = 2
+ }
+
Env = iniFile.Section("").Key("app_mode").MustString("development")
InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
@@ -586,11 +601,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// read security settings
security := iniFile.Section("security")
SecretKey = security.Key("secret_key").String()
- LogInRememberDays = security.Key("login_remember_days").MustInt()
- CookieUserName = security.Key("cookie_username").String()
- CookieRememberName = security.Key("cookie_remember_name").String()
DisableGravatar = security.Key("disable_gravatar").MustBool(true)
cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
+ cfg.SecurityHTTPSCookies = security.Key("https_flag_cookies").MustBool(false)
DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection
// read snapshots settings
@@ -705,10 +718,11 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data")
explore := iniFile.Section("explore")
- ExploreEnabled = explore.Key("enabled").MustBool(false)
+ ExploreEnabled = explore.Key("enabled").MustBool(true)
panels := iniFile.Section("panels")
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
+ cfg.DisableSanitizeHtml = panels.Key("disable_sanitize_html").MustBool(false)
cfg.readSessionConfig()
cfg.readSmtpSettings()
diff --git a/pkg/util/encoding.go b/pkg/util/encoding.go
index 0edb721e422..e82344d73f9 100644
--- a/pkg/util/encoding.go
+++ b/pkg/util/encoding.go
@@ -101,3 +101,11 @@ func DecodeBasicAuthHeader(header string) (string, string, error) {
return userAndPass[0], userAndPass[1], nil
}
+
+func RandomHex(n int) (string, error) {
+ bytes := make([]byte, n)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(bytes), nil
+}
diff --git a/pkg/util/ip_address.go b/pkg/util/ip_address.go
new file mode 100644
index 00000000000..d8d95ef3acd
--- /dev/null
+++ b/pkg/util/ip_address.go
@@ -0,0 +1,29 @@
+package util
+
+import (
+ "net"
+ "strings"
+)
+
+// ParseIPAddress parses an IP address and removes port and/or IPV6 format
+func ParseIPAddress(input string) string {
+ s := input
+ lastIndex := strings.LastIndex(input, ":")
+
+ if lastIndex != -1 {
+ if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
+ s = input[:lastIndex]
+ }
+ }
+
+ s = strings.Replace(s, "[", "", -1)
+ s = strings.Replace(s, "]", "", -1)
+
+ ip := net.ParseIP(s)
+
+ if ip.IsLoopback() {
+ return "127.0.0.1"
+ }
+
+ return ip.String()
+}
diff --git a/pkg/util/ip_address_test.go b/pkg/util/ip_address_test.go
new file mode 100644
index 00000000000..fd3e3ea8587
--- /dev/null
+++ b/pkg/util/ip_address_test.go
@@ -0,0 +1,16 @@
+package util
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestParseIPAddress(t *testing.T) {
+ Convey("Test parse ip address", t, func() {
+ So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
+ So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
+ So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
+ So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
+ })
+}
diff --git a/public/app/core/actions/location.ts b/public/app/core/actions/location.ts
index 6f7ac67363e..8669788fa16 100644
--- a/public/app/core/actions/location.ts
+++ b/public/app/core/actions/location.ts
@@ -1,13 +1,17 @@
import { LocationUpdate } from 'app/types';
+export enum CoreActionTypes {
+ UpdateLocation = 'UPDATE_LOCATION',
+}
+
export type Action = UpdateLocationAction;
export interface UpdateLocationAction {
- type: 'UPDATE_LOCATION';
+ type: CoreActionTypes.UpdateLocation;
payload: LocationUpdate;
}
export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({
- type: 'UPDATE_LOCATION',
+ type: CoreActionTypes.UpdateLocation,
payload: location,
});
diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.tsx
index 4231e992b19..db2172039c6 100644
--- a/public/app/core/components/sidemenu/SideMenuDropDown.tsx
+++ b/public/app/core/components/sidemenu/SideMenuDropDown.tsx
@@ -10,7 +10,9 @@ const SideMenuDropDown: FC = props => {
return (
- {link.text}
+
+ {link.text}
+
{link.children &&
link.children.map((child, index) => {
diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap
index 861168c1cc3..20d0a3ef3a4 100644
--- a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap
+++ b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap
@@ -8,11 +8,15 @@ exports[`Render should render children 1`] = `
-
- link
-
+
+ link
+
+
-
- link
-
+
+ link
+
+
`;
diff --git a/public/app/core/config.ts b/public/app/core/config.ts
index 26f31ffcf54..395e40e914b 100644
--- a/public/app/core/config.ts
+++ b/public/app/core/config.ts
@@ -35,8 +35,9 @@ export class Settings {
loginHint: any;
loginError: any;
viewersCanEdit: boolean;
+ disableSanitizeHtml: boolean;
- constructor(options) {
+ constructor(options: Settings) {
const defaults = {
datasources: {},
windowTitlePrefix: 'Grafana - ',
@@ -52,6 +53,7 @@ export class Settings {
isEnterprise: false,
},
viewersCanEdit: false,
+ disableSanitizeHtml: false
};
_.extend(this, defaults, options);
diff --git a/public/app/core/controllers/all.ts b/public/app/core/controllers/all.ts
index 0dbcdf4cb28..f6a4e51bad4 100644
--- a/public/app/core/controllers/all.ts
+++ b/public/app/core/controllers/all.ts
@@ -1,4 +1,3 @@
-import './inspect_ctrl';
import './json_editor_ctrl';
import './login_ctrl';
import './invited_ctrl';
diff --git a/public/app/core/controllers/inspect_ctrl.ts b/public/app/core/controllers/inspect_ctrl.ts
deleted file mode 100644
index d106b42da16..00000000000
--- a/public/app/core/controllers/inspect_ctrl.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import angular from 'angular';
-import _ from 'lodash';
-import $ from 'jquery';
-import coreModule from '../core_module';
-
-export class InspectCtrl {
- /** @ngInject */
- constructor($scope, $sanitize) {
- const model = $scope.inspector;
-
- $scope.init = function() {
- $scope.editor = { index: 0 };
-
- if (!model.error) {
- return;
- }
-
- if (_.isString(model.error.data)) {
- $scope.response = $('' + model.error.data + '
').text();
- } else if (model.error.data) {
- if (model.error.data.response) {
- $scope.response = $sanitize(model.error.data.response);
- } else {
- $scope.response = angular.toJson(model.error.data, true);
- }
- } else if (model.error.message) {
- $scope.message = model.error.message;
- }
-
- if (model.error.config && model.error.config.params) {
- $scope.request_parameters = _.map(model.error.config.params, (value, key) => {
- return { key: key, value: value };
- });
- }
-
- if (model.error.stack) {
- $scope.editor.index = 3;
- $scope.stack_trace = model.error.stack;
- $scope.message = model.error.message;
- }
-
- if (model.error.config && model.error.config.data) {
- $scope.editor.index = 2;
-
- if (_.isString(model.error.config.data)) {
- $scope.request_parameters = this.getParametersFromQueryString(model.error.config.data);
- } else {
- $scope.request_parameters = _.map(model.error.config.data, (value, key) => {
- return { key: key, value: angular.toJson(value, true) };
- });
- }
- }
- };
- }
- getParametersFromQueryString(queryString) {
- const result = [];
- const parameters = queryString.split('&');
- for (let i = 0; i < parameters.length; i++) {
- const keyValue = parameters[i].split('=');
- if (keyValue[1].length > 0) {
- result.push({
- key: keyValue[0],
- value: (window as any).unescape(keyValue[1]),
- });
- }
- }
- return result;
- }
-}
-
-coreModule.controller('InspectCtrl', InspectCtrl);
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
index 4cf9a029a2a..a3f78e7152a 100644
--- a/public/app/core/logs_model.ts
+++ b/public/app/core/logs_model.ts
@@ -42,7 +42,7 @@ export interface LogSearchMatch {
text: string;
}
-export interface LogRow {
+export interface LogRowModel {
duplicates?: number;
entry: string;
key: string; // timestamp + labels
@@ -56,7 +56,7 @@ export interface LogRow {
uniqueLabels?: LogsStreamLabels;
}
-export interface LogsLabelStat {
+export interface LogLabelStatsModel {
active?: boolean;
count: number;
proportion: number;
@@ -78,7 +78,7 @@ export interface LogsMetaItem {
export interface LogsModel {
id: string; // Identify one logs result from another
meta?: LogsMetaItem[];
- rows: LogRow[];
+ rows: LogRowModel[];
series?: TimeSeries[];
}
@@ -188,13 +188,13 @@ export const LogsParsers: { [name: string]: LogsParser } = {
},
};
-export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] {
+export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogLabelStatsModel[] {
// Consider only rows that satisfy the matcher
const rowsWithField = rows.filter(row => extractor.test(row.entry));
const rowCount = rowsWithField.length;
// Get field value counts for eligible rows
- const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]);
+ const countsByValue = _.countBy(rowsWithField, row => (row as LogRowModel).entry.match(extractor)[1]);
const sortedCounts = _.chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count')
@@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe
return sortedCounts;
}
-export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] {
+export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] {
// Consider only rows that have the given label
const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
const rowCount = rowsWithLabel.length;
// Get label value counts for eligible rows
- const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRow).labels[label]);
+ const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]);
const sortedCounts = _.chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count')
@@ -221,7 +221,7 @@ export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabe
}
const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g;
-function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean {
+function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedupStrategy): boolean {
switch (strategy) {
case LogsDedupStrategy.exact:
// Exact still strips dates
@@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
return logs;
}
- const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
+ const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
const previous = result[result.length - 1];
if (index > 0 && isDuplicateRow(row, previous, strategy)) {
previous.duplicates++;
@@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set)
return logs;
}
- const filteredRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
+ const filteredRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
if (!hiddenLogLevels.has(row.logLevel)) {
result.push(row);
}
@@ -291,7 +291,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set)
};
}
-export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
+export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] {
// currently interval is rangeMs / resolution, which is too low for showing series as bars.
// need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain
// when executing queries & interval calculated and not here but this is a temporary fix.
diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts
index a42bd813782..6b39710dcca 100644
--- a/public/app/core/reducers/location.ts
+++ b/public/app/core/reducers/location.ts
@@ -1,4 +1,4 @@
-import { Action } from 'app/core/actions/location';
+import { Action, CoreActionTypes } from 'app/core/actions/location';
import { LocationState } from 'app/types';
import { renderUrl } from 'app/core/utils/url';
import _ from 'lodash';
@@ -12,7 +12,7 @@ export const initialState: LocationState = {
export const locationReducer = (state = initialState, action: Action): LocationState => {
switch (action.type) {
- case 'UPDATE_LOCATION': {
+ case CoreActionTypes.UpdateLocation: {
const { path, routeParams } = action.payload;
let query = action.payload.query || state.query;
@@ -24,9 +24,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
return {
url: renderUrl(path || state.path, query),
path: path || state.path,
- query: {
- ...query,
- },
+ query: { ...query },
routeParams: routeParams || state.routeParams,
};
}
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 9e128c449a6..989746fd067 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -236,7 +236,7 @@ export class KeybindingSrv {
shareScope.dashboard = dashboard;
appEvents.emit('show-modal', {
- src: 'public/app/features/dashboard/partials/shareModal.html',
+ src: 'public/app/features/dashboard/components/ShareModal/template.html',
scope: shareScope,
});
}
diff --git a/public/app/core/specs/url.test.ts b/public/app/core/specs/url.test.ts
index b5994488128..3b7f81494f9 100644
--- a/public/app/core/specs/url.test.ts
+++ b/public/app/core/specs/url.test.ts
@@ -14,3 +14,12 @@ describe('toUrlParams', () => {
expect(url).toBe('server=backend-01&hasSpace=has%20space&many=1&many=2&many=3&true&number=20&isNull=&isUndefined=');
});
});
+
+describe('toUrlParams', () => {
+ it('should encode the same way as angularjs', () => {
+ const url = toUrlParams({
+ server: ':@',
+ });
+ expect(url).toBe('server=:@');
+ });
+});
diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts
index 45b70672bc6..7a9f54a0cae 100644
--- a/public/app/core/utils/explore.ts
+++ b/public/app/core/utils/explore.ts
@@ -84,7 +84,7 @@ export async function getExploreUrl(
}
const exploreState = JSON.stringify(state);
- url = renderUrl('/explore', { state: exploreState });
+ url = renderUrl('/explore', { left: exploreState });
}
return url;
}
diff --git a/public/app/core/utils/text.ts b/public/app/core/utils/text.ts
index 4e948116dba..427b0102c95 100644
--- a/public/app/core/utils/text.ts
+++ b/public/app/core/utils/text.ts
@@ -1,4 +1,5 @@
import { TextMatch } from 'app/types/explore';
+import xss from 'xss';
/**
* Adapt findMatchesInText for react-highlight-words findChunks handler.
@@ -22,7 +23,7 @@ export function findMatchesInText(haystack: string, needle: string): TextMatch[]
}
const matches = [];
const cleaned = cleanNeedle(needle);
- let regexp;
+ let regexp: RegExp;
try {
regexp = new RegExp(`(?:${cleaned})`, 'g');
} catch (error) {
@@ -42,3 +43,28 @@ export function findMatchesInText(haystack: string, needle: string): TextMatch[]
});
return matches;
}
+
+const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
+ acc[element] = xss.whiteList[element].concat(['class', 'style']);
+ return acc;
+}, {});
+
+const sanitizeXSS = new xss.FilterXSS({
+ whiteList: XSSWL
+});
+
+/**
+ * Returns string safe from XSS attacks.
+ *
+ * Even though we allow the style-attribute, there's still default filtering applied to it
+ * Info: https://github.com/leizongmin/js-xss#customize-css-filter
+ * Whitelist: https://github.com/leizongmin/js-css-filter/blob/master/lib/default.js
+ */
+export function sanitize (unsanitizedString: string): string {
+ try {
+ return sanitizeXSS.process(unsanitizedString);
+ } catch (error) {
+ console.log('String could not be sanitized', unsanitizedString);
+ return unsanitizedString;
+ }
+}
diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts
index ab8be8ad222..824e0e4e9c9 100644
--- a/public/app/core/utils/url.ts
+++ b/public/app/core/utils/url.ts
@@ -11,6 +11,16 @@ export function renderUrl(path: string, query: UrlQueryMap | undefined): string
return path;
}
+export function encodeURIComponentAsAngularJS(val, pctEncodeSpaces) {
+ return encodeURIComponent(val).
+ replace(/%40/gi, '@').
+ replace(/%3A/gi, ':').
+ replace(/%24/g, '$').
+ replace(/%2C/gi, ',').
+ replace(/%3B/gi, ';').
+ replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
+}
+
export function toUrlParams(a) {
const s = [];
const rbracket = /\[\]$/;
@@ -22,9 +32,9 @@ export function toUrlParams(a) {
const add = (k, v) => {
v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v;
if (typeof v !== 'boolean') {
- s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v);
+ s[s.length] = encodeURIComponentAsAngularJS(k, true) + '=' + encodeURIComponentAsAngularJS(v, true);
} else {
- s[s.length] = encodeURIComponent(k);
+ s[s.length] = encodeURIComponentAsAngularJS(k, true);
}
};
diff --git a/public/app/features/all.ts b/public/app/features/all.ts
index 1ba6a85899c..83146596ea0 100644
--- a/public/app/features/all.ts
+++ b/public/app/features/all.ts
@@ -1,7 +1,7 @@
import './annotations/all';
import './templating/all';
import './plugins/all';
-import './dashboard/all';
+import './dashboard';
import './playlist/all';
import './panel/all';
import './org/all';
diff --git a/public/app/features/dashboard/alerting_srv.ts b/public/app/features/dashboard/alerting_srv.ts
deleted file mode 100644
index 446c3218f79..00000000000
--- a/public/app/features/dashboard/alerting_srv.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import coreModule from 'app/core/core_module';
-
-export class AlertingSrv {
- dashboard: any;
- alerts: any[];
-
- init(dashboard, alerts) {
- this.dashboard = dashboard;
- this.alerts = alerts || [];
- }
-}
-
-coreModule.service('alertingSrv', AlertingSrv);
diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts
deleted file mode 100644
index 5ec4e5e3929..00000000000
--- a/public/app/features/dashboard/all.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import './dashboard_ctrl';
-import './alerting_srv';
-import './history/history';
-import './dashboard_loader_srv';
-import './dashnav/dashnav';
-import './submenu/submenu';
-import './save_as_modal';
-import './save_modal';
-import './save_provisioned_modal';
-import './shareModalCtrl';
-import './share_snapshot_ctrl';
-import './dashboard_srv';
-import './view_state_srv';
-import './validation_srv';
-import './time_srv';
-import './unsaved_changes_srv';
-import './unsaved_changes_modal';
-import './timepicker/timepicker';
-import './upload';
-import './export/export_modal';
-import './export_data/export_data_modal';
-import './ad_hoc_filters';
-import './repeat_option/repeat_option';
-import './dashgrid/DashboardGridDirective';
-import './dashgrid/RowOptions';
-import './folder_picker/folder_picker';
-import './move_to_folder_modal/move_to_folder';
-import './settings/settings';
-import './panellinks/module';
-import './dashlinks/module';
-
-// angular wrappers
-import { react2AngularDirective } from 'app/core/utils/react2angular';
-import DashboardPermissions from './permissions/DashboardPermissions';
-
-react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
-
-import coreModule from 'app/core/core_module';
-import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
-import { DashboardImportCtrl } from './dashboard_import_ctrl';
-import { CreateFolderCtrl } from './create_folder_ctrl';
-
-coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
-coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
-coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);
diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/ad_hoc_filters.ts
rename to public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts
diff --git a/public/app/features/dashboard/components/AdHocFilters/index.ts b/public/app/features/dashboard/components/AdHocFilters/index.ts
new file mode 100644
index 00000000000..522b564d004
--- /dev/null
+++ b/public/app/features/dashboard/components/AdHocFilters/index.ts
@@ -0,0 +1 @@
+export { AdHocFiltersCtrl } from './AdHocFiltersCtrl';
diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
similarity index 86%
rename from public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
rename to public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
index 95d03152b14..4d46d88a1d2 100644
--- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
+++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
@@ -1,23 +1,23 @@
import React from 'react';
import _ from 'lodash';
import config from 'app/core/config';
-import { PanelModel } from '../panel_model';
-import { DashboardModel } from '../dashboard_model';
+import { PanelModel } from '../../panel_model';
+import { DashboardModel } from '../../dashboard_model';
import store from 'app/core/store';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { updateLocation } from 'app/core/actions';
import { store as reduxStore } from 'app/store/store';
-export interface AddPanelPanelProps {
+export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
}
-export interface AddPanelPanelState {
+export interface State {
copiedPanelPlugins: any[];
}
-export class AddPanelPanel extends React.Component {
+export class AddPanelWidget extends React.Component {
constructor(props) {
super(props);
this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
@@ -133,15 +133,15 @@ export class AddPanelPanel extends React.Component
-
-
+
+
+
-
+
-
+
Edit Panel
diff --git a/public/sass/components/_panel_add_panel.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss
similarity index 81%
rename from public/sass/components/_panel_add_panel.scss
rename to public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss
index 86921fb43f3..5a1cbee4b44 100644
--- a/public/sass/components/_panel_add_panel.scss
+++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss
@@ -1,12 +1,12 @@
-.add-panel-container {
+.add-panel-widget-container {
height: 100%;
}
-.add-panel {
+.add-panel-widget {
height: 100%;
}
-.add-panel__header {
+.add-panel-widget__header {
top: 0;
position: absolute;
padding: 0 15px;
@@ -26,7 +26,7 @@
}
}
-.add-panel__close {
+.add-panel-widget__close {
margin-left: auto;
background-color: transparent;
border: 0;
@@ -34,7 +34,7 @@
margin-right: -10px;
}
-.add-panel-btn-container {
+.add-panel-widget__btn-container {
display: flex;
justify-content: center;
align-items: center;
diff --git a/public/app/features/dashboard/components/AddPanelWidget/index.ts b/public/app/features/dashboard/components/AddPanelWidget/index.ts
new file mode 100644
index 00000000000..b96948ab1c0
--- /dev/null
+++ b/public/app/features/dashboard/components/AddPanelWidget/index.ts
@@ -0,0 +1 @@
+export { AddPanelWidget } from './AddPanelWidget';
diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/components/DashExportModal/DashExportCtrl.ts
similarity index 92%
rename from public/app/features/dashboard/export/export_modal.ts
rename to public/app/features/dashboard/components/DashExportModal/DashExportCtrl.ts
index 8136c77cd8f..7769bdf114a 100644
--- a/public/app/features/dashboard/export/export_modal.ts
+++ b/public/app/features/dashboard/components/DashExportModal/DashExportCtrl.ts
@@ -2,7 +2,7 @@ import angular from 'angular';
import { saveAs } from 'file-saver';
import coreModule from 'app/core/core_module';
-import { DashboardExporter } from './exporter';
+import { DashboardExporter } from './DashboardExporter';
export class DashExportCtrl {
dash: any;
@@ -66,7 +66,7 @@ export class DashExportCtrl {
export function dashExportDirective() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/export/export_modal.html',
+ templateUrl: 'public/app/features/dashboard/components/DashExportModal/template.html',
controller: DashExportCtrl,
bindToController: true,
controllerAs: 'ctrl',
diff --git a/public/app/features/dashboard/specs/exporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
similarity index 98%
rename from public/app/features/dashboard/specs/exporter.test.ts
rename to public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
index eac6b0b272a..20ab21541a5 100644
--- a/public/app/features/dashboard/specs/exporter.test.ts
+++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
@@ -6,8 +6,8 @@ jest.mock('app/core/store', () => {
import _ from 'lodash';
import config from 'app/core/config';
-import { DashboardExporter } from '../export/exporter';
-import { DashboardModel } from '../dashboard_model';
+import { DashboardExporter } from './DashboardExporter';
+import { DashboardModel } from '../../dashboard_model';
describe('given dashboard with repeated panels', () => {
let dash, exported;
diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
similarity index 98%
rename from public/app/features/dashboard/export/exporter.ts
rename to public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
index 7aecb5c384f..22b93b767d6 100644
--- a/public/app/features/dashboard/export/exporter.ts
+++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
@@ -1,6 +1,6 @@
import config from 'app/core/config';
import _ from 'lodash';
-import { DashboardModel } from '../dashboard_model';
+import { DashboardModel } from '../../dashboard_model';
export class DashboardExporter {
constructor(private datasourceSrv) {}
diff --git a/public/app/features/dashboard/components/DashExportModal/index.ts b/public/app/features/dashboard/components/DashExportModal/index.ts
new file mode 100644
index 00000000000..6529cf07ad9
--- /dev/null
+++ b/public/app/features/dashboard/components/DashExportModal/index.ts
@@ -0,0 +1,2 @@
+export { DashboardExporter } from './DashboardExporter';
+export { DashExportCtrl } from './DashExportCtrl';
diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/components/DashExportModal/template.html
similarity index 100%
rename from public/app/features/dashboard/export/export_modal.html
rename to public/app/features/dashboard/components/DashExportModal/template.html
diff --git a/public/app/features/dashboard/dashlinks/module.ts b/public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts
similarity index 99%
rename from public/app/features/dashboard/dashlinks/module.ts
rename to public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts
index c951538d45d..a08e438a46c 100644
--- a/public/app/features/dashboard/dashlinks/module.ts
+++ b/public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts
@@ -1,6 +1,6 @@
import angular from 'angular';
import _ from 'lodash';
-import { iconMap } from './editor';
+import { iconMap } from './DashLinksEditorCtrl';
function dashLinksContainer() {
return {
diff --git a/public/app/features/dashboard/dashlinks/editor.ts b/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts
similarity index 90%
rename from public/app/features/dashboard/dashlinks/editor.ts
rename to public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts
index 482052469db..398ad757bf3 100644
--- a/public/app/features/dashboard/dashlinks/editor.ts
+++ b/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts
@@ -11,7 +11,7 @@ export let iconMap = {
cloud: 'fa-cloud',
};
-export class DashLinkEditorCtrl {
+export class DashLinksEditorCtrl {
dashboard: any;
iconMap: any;
mode: any;
@@ -65,8 +65,8 @@ export class DashLinkEditorCtrl {
function dashLinksEditor() {
return {
restrict: 'E',
- controller: DashLinkEditorCtrl,
- templateUrl: 'public/app/features/dashboard/dashlinks/editor.html',
+ controller: DashLinksEditorCtrl,
+ templateUrl: 'public/app/features/dashboard/components/DashLinks/editor.html',
bindToController: true,
controllerAs: 'ctrl',
scope: {
diff --git a/public/app/features/dashboard/dashlinks/editor.html b/public/app/features/dashboard/components/DashLinks/editor.html
similarity index 100%
rename from public/app/features/dashboard/dashlinks/editor.html
rename to public/app/features/dashboard/components/DashLinks/editor.html
diff --git a/public/app/features/dashboard/components/DashLinks/index.ts b/public/app/features/dashboard/components/DashLinks/index.ts
new file mode 100644
index 00000000000..ef118d4a84c
--- /dev/null
+++ b/public/app/features/dashboard/components/DashLinks/index.ts
@@ -0,0 +1,2 @@
+export { DashLinksContainerCtrl } from './DashLinksContainerCtrl';
+export { DashLinksEditorCtrl } from './DashLinksEditorCtrl';
diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts
similarity index 92%
rename from public/app/features/dashboard/dashnav/dashnav.ts
rename to public/app/features/dashboard/components/DashNav/DashNavCtrl.ts
index 1c83b2d0bdb..d7305b948dc 100644
--- a/public/app/features/dashboard/dashnav/dashnav.ts
+++ b/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts
@@ -1,7 +1,7 @@
import moment from 'moment';
import angular from 'angular';
import { appEvents, NavModel } from 'app/core/core';
-import { DashboardModel } from '../dashboard_model';
+import { DashboardModel } from '../../dashboard_model';
export class DashNavCtrl {
dashboard: DashboardModel;
@@ -60,7 +60,7 @@ export class DashNavCtrl {
modalScope.dashboard = this.dashboard;
appEvents.emit('show-modal', {
- src: 'public/app/features/dashboard/partials/shareModal.html',
+ src: 'public/app/features/dashboard/components/ShareModal/template.html',
scope: modalScope,
});
}
@@ -107,7 +107,7 @@ export class DashNavCtrl {
export function dashNavDirective() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/dashnav/dashnav.html',
+ templateUrl: 'public/app/features/dashboard/components/DashNav/template.html',
controller: DashNavCtrl,
bindToController: true,
controllerAs: 'ctrl',
diff --git a/public/app/features/dashboard/components/DashNav/index.ts b/public/app/features/dashboard/components/DashNav/index.ts
new file mode 100644
index 00000000000..854e32b24d2
--- /dev/null
+++ b/public/app/features/dashboard/components/DashNav/index.ts
@@ -0,0 +1 @@
+export { DashNavCtrl } from './DashNavCtrl';
diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/components/DashNav/template.html
similarity index 100%
rename from public/app/features/dashboard/dashnav/dashnav.html
rename to public/app/features/dashboard/components/DashNav/template.html
diff --git a/public/app/features/dashboard/permissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
similarity index 97%
rename from public/app/features/dashboard/permissions/DashboardPermissions.tsx
rename to public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
index 265d11161ea..ce6a866ce57 100644
--- a/public/app/features/dashboard/permissions/DashboardPermissions.tsx
+++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
@@ -8,11 +8,11 @@ import {
addDashboardPermission,
removeDashboardPermission,
updateDashboardPermission,
-} from '../state/actions';
+} from '../../state/actions';
import PermissionList from 'app/core/components/PermissionList/PermissionList';
import AddPermission from 'app/core/components/PermissionList/AddPermission';
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
-import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
+import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
export interface Props {
dashboardId: number;
diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
similarity index 97%
rename from public/app/features/dashboard/settings/settings.ts
rename to public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
index 1e8d96a54cb..a0eb5c8c6b3 100755
--- a/public/app/features/dashboard/settings/settings.ts
+++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
@@ -1,5 +1,5 @@
import { coreModule, appEvents, contextSrv } from 'app/core/core';
-import { DashboardModel } from '../dashboard_model';
+import { DashboardModel } from '../../dashboard_model';
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';
@@ -230,7 +230,7 @@ export class SettingsCtrl {
export function dashboardSettings() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/settings/settings.html',
+ templateUrl: 'public/app/features/dashboard/components/DashboardSettings/template.html',
controller: SettingsCtrl,
bindToController: true,
controllerAs: 'ctrl',
diff --git a/public/app/features/dashboard/components/DashboardSettings/index.ts b/public/app/features/dashboard/components/DashboardSettings/index.ts
new file mode 100644
index 00000000000..f81b8cdbc67
--- /dev/null
+++ b/public/app/features/dashboard/components/DashboardSettings/index.ts
@@ -0,0 +1 @@
+export { SettingsCtrl } from './SettingsCtrl';
diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/components/DashboardSettings/template.html
similarity index 98%
rename from public/app/features/dashboard/settings/settings.html
rename to public/app/features/dashboard/components/DashboardSettings/template.html
index 46d84a7a2fd..97002f7bf92 100644
--- a/public/app/features/dashboard/settings/settings.html
+++ b/public/app/features/dashboard/components/DashboardSettings/template.html
@@ -51,7 +51,8 @@
on-change="ctrl.onFolderChange($folder)"
enable-create-new="true"
is-valid-selection="true"
- label-class="width-7">
+ label-class="width-7"
+ dashboard-id="ctrl.dashboard.id">
diff --git a/public/app/features/dashboard/export_data/export_data_modal.ts b/public/app/features/dashboard/components/ExportDataModal/ExportDataModalCtrl.ts
similarity index 92%
rename from public/app/features/dashboard/export_data/export_data_modal.ts
rename to public/app/features/dashboard/components/ExportDataModal/ExportDataModalCtrl.ts
index 460f80079d9..f87daa94ee7 100644
--- a/public/app/features/dashboard/export_data/export_data_modal.ts
+++ b/public/app/features/dashboard/components/ExportDataModal/ExportDataModalCtrl.ts
@@ -31,7 +31,7 @@ export class ExportDataModalCtrl {
export function exportDataModal() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/export_data/export_data_modal.html',
+ templateUrl: 'public/app/features/dashboard/components/ExportDataModal/template.html',
controller: ExportDataModalCtrl,
controllerAs: 'ctrl',
scope: {
diff --git a/public/app/features/dashboard/components/ExportDataModal/index.ts b/public/app/features/dashboard/components/ExportDataModal/index.ts
new file mode 100644
index 00000000000..6df4fd00434
--- /dev/null
+++ b/public/app/features/dashboard/components/ExportDataModal/index.ts
@@ -0,0 +1 @@
+export { ExportDataModalCtrl } from './ExportDataModalCtrl';
diff --git a/public/app/features/dashboard/export_data/export_data_modal.html b/public/app/features/dashboard/components/ExportDataModal/template.html
similarity index 100%
rename from public/app/features/dashboard/export_data/export_data_modal.html
rename to public/app/features/dashboard/components/ExportDataModal/template.html
diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
similarity index 90%
rename from public/app/features/dashboard/folder_picker/folder_picker.ts
rename to public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
index 80651fecb7e..93d43d36038 100644
--- a/public/app/features/dashboard/folder_picker/folder_picker.ts
+++ b/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
@@ -21,6 +21,7 @@ export class FolderPickerCtrl {
hasValidationError: boolean;
validationError: any;
isEditor: boolean;
+ dashboardId?: number;
/** @ngInject */
constructor(private backendSrv, private validationSrv, private contextSrv) {
@@ -144,7 +145,13 @@ export class FolderPickerCtrl {
if (this.isEditor) {
folder = rootFolder;
} else {
- folder = result.length > 0 ? result[0] : resetFolder;
+ // We shouldn't assign a random folder without the user actively choosing it on a persisted dashboard
+ const isPersistedDashBoard = this.dashboardId ? true : false;
+ if (isPersistedDashBoard) {
+ folder = resetFolder;
+ } else {
+ folder = result.length > 0 ? result[0] : resetFolder;
+ }
}
}
@@ -161,7 +168,7 @@ export class FolderPickerCtrl {
export function folderPicker() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/folder_picker/folder_picker.html',
+ templateUrl: 'public/app/features/dashboard/components/FolderPicker/template.html',
controller: FolderPickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
@@ -176,6 +183,7 @@ export function folderPicker() {
exitFolderCreation: '&',
enableCreateNew: '@',
enableReset: '@',
+ dashboardId: '',
},
};
}
diff --git a/public/app/features/dashboard/components/FolderPicker/index.ts b/public/app/features/dashboard/components/FolderPicker/index.ts
new file mode 100644
index 00000000000..7550f7fd573
--- /dev/null
+++ b/public/app/features/dashboard/components/FolderPicker/index.ts
@@ -0,0 +1 @@
+export { FolderPickerCtrl } from './FolderPickerCtrl';
diff --git a/public/app/features/dashboard/folder_picker/folder_picker.html b/public/app/features/dashboard/components/FolderPicker/template.html
similarity index 100%
rename from public/app/features/dashboard/folder_picker/folder_picker.html
rename to public/app/features/dashboard/components/FolderPicker/template.html
diff --git a/public/app/features/dashboard/dashgrid/RowOptions.ts b/public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/dashgrid/RowOptions.ts
rename to public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts
diff --git a/public/app/features/dashboard/components/RowOptions/index.ts b/public/app/features/dashboard/components/RowOptions/index.ts
new file mode 100644
index 00000000000..626e4cd65b3
--- /dev/null
+++ b/public/app/features/dashboard/components/RowOptions/index.ts
@@ -0,0 +1 @@
+export { RowOptionsCtrl } from './RowOptionsCtrl';
diff --git a/public/app/features/dashboard/partials/row_options.html b/public/app/features/dashboard/components/RowOptions/template.html
similarity index 100%
rename from public/app/features/dashboard/partials/row_options.html
rename to public/app/features/dashboard/components/RowOptions/template.html
diff --git a/public/app/features/dashboard/specs/save_as_modal.test.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.test.ts
similarity index 95%
rename from public/app/features/dashboard/specs/save_as_modal.test.ts
rename to public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.test.ts
index ceb7e49c550..91b9097b626 100644
--- a/public/app/features/dashboard/specs/save_as_modal.test.ts
+++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.test.ts
@@ -1,4 +1,4 @@
-import { SaveDashboardAsModalCtrl } from '../save_as_modal';
+import { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
import { describe, it, expect } from 'test/lib/common';
describe('saving dashboard as', () => {
diff --git a/public/app/features/dashboard/save_as_modal.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts
similarity index 96%
rename from public/app/features/dashboard/save_as_modal.ts
rename to public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts
index 4649bc18f9f..6a470785fdb 100644
--- a/public/app/features/dashboard/save_as_modal.ts
+++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts
@@ -25,7 +25,8 @@ const template = `
enter-folder-creation="ctrl.onEnterFolderCreation()"
exit-folder-creation="ctrl.onExitFolderCreation()"
enable-create-new="true"
- label-class="width-7">
+ label-class="width-7"
+ dashboard-id="ctrl.clone.id">
diff --git a/public/app/features/dashboard/specs/save_modal.test.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.test.ts
similarity index 97%
rename from public/app/features/dashboard/specs/save_modal.test.ts
rename to public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.test.ts
index 669ae43a0ff..f973c1b8e63 100644
--- a/public/app/features/dashboard/specs/save_modal.test.ts
+++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.test.ts
@@ -1,4 +1,4 @@
-import { SaveDashboardModalCtrl } from '../save_modal';
+import { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
const setup = (timeChanged, variableValuesChanged, cb) => {
const dash = {
diff --git a/public/app/features/dashboard/save_modal.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/save_modal.ts
rename to public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts
diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.test.ts
similarity index 87%
rename from public/app/features/dashboard/specs/save_provisioned_modal.test.ts
rename to public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.test.ts
index a3ab27a984f..86048e861bd 100644
--- a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts
+++ b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.test.ts
@@ -1,4 +1,4 @@
-import { SaveProvisionedDashboardModalCtrl } from '../save_provisioned_modal';
+import { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';
describe('SaveProvisionedDashboardModalCtrl', () => {
const json = {
diff --git a/public/app/features/dashboard/save_provisioned_modal.ts b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/save_provisioned_modal.ts
rename to public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts
diff --git a/public/app/features/dashboard/components/SaveModals/index.ts b/public/app/features/dashboard/components/SaveModals/index.ts
new file mode 100644
index 00000000000..afab0796d28
--- /dev/null
+++ b/public/app/features/dashboard/components/SaveModals/index.ts
@@ -0,0 +1,2 @@
+export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
+export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts b/public/app/features/dashboard/components/ShareModal/ShareModalCtrl.test.ts
similarity index 96%
rename from public/app/features/dashboard/specs/share_modal_ctrl.test.ts
rename to public/app/features/dashboard/components/ShareModal/ShareModalCtrl.test.ts
index 70d301ed5ff..3181231cb53 100644
--- a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts
+++ b/public/app/features/dashboard/components/ShareModal/ShareModalCtrl.test.ts
@@ -1,7 +1,6 @@
-import '../shareModalCtrl';
-import { ShareModalCtrl } from '../shareModalCtrl';
import config from 'app/core/config';
-import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv';
+import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
+import { ShareModalCtrl } from './ShareModalCtrl';
describe('ShareModalCtrl', () => {
const ctx = {
diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/components/ShareModal/ShareModalCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/shareModalCtrl.ts
rename to public/app/features/dashboard/components/ShareModal/ShareModalCtrl.ts
diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/share_snapshot_ctrl.ts
rename to public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts
diff --git a/public/app/features/dashboard/components/ShareModal/index.ts b/public/app/features/dashboard/components/ShareModal/index.ts
new file mode 100644
index 00000000000..3f27d5a1ba3
--- /dev/null
+++ b/public/app/features/dashboard/components/ShareModal/index.ts
@@ -0,0 +1,2 @@
+export { ShareModalCtrl } from './ShareModalCtrl';
+export { ShareSnapshotCtrl } from './ShareSnapshotCtrl';
diff --git a/public/app/features/dashboard/partials/shareModal.html b/public/app/features/dashboard/components/ShareModal/template.html
similarity index 100%
rename from public/app/features/dashboard/partials/shareModal.html
rename to public/app/features/dashboard/components/ShareModal/template.html
diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts
similarity index 86%
rename from public/app/features/dashboard/submenu/submenu.ts
rename to public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts
index 184d29facee..502e467ad2b 100644
--- a/public/app/features/dashboard/submenu/submenu.ts
+++ b/public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts
@@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash';
-export class SubmenuCtrl {
+export class SubMenuCtrl {
annotations: any;
variables: any;
dashboard: any;
@@ -29,8 +29,8 @@ export class SubmenuCtrl {
export function submenuDirective() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/submenu/submenu.html',
- controller: SubmenuCtrl,
+ templateUrl: 'public/app/features/dashboard/components/SubMenu/template.html',
+ controller: SubMenuCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
diff --git a/public/app/features/dashboard/components/SubMenu/index.ts b/public/app/features/dashboard/components/SubMenu/index.ts
new file mode 100644
index 00000000000..1790aa66782
--- /dev/null
+++ b/public/app/features/dashboard/components/SubMenu/index.ts
@@ -0,0 +1 @@
+export { SubMenuCtrl } from './SubMenuCtrl';
diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/components/SubMenu/template.html
similarity index 100%
rename from public/app/features/dashboard/submenu/submenu.html
rename to public/app/features/dashboard/components/SubMenu/template.html
diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts
similarity index 95%
rename from public/app/features/dashboard/timepicker/timepicker.ts
rename to public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts
index c89e49b54b3..0c388c27f8d 100644
--- a/public/app/features/dashboard/timepicker/timepicker.ts
+++ b/public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts
@@ -159,7 +159,7 @@ export class TimePickerCtrl {
export function settingsDirective() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/timepicker/settings.html',
+ templateUrl: 'public/app/features/dashboard/components/TimePicker/settings.html',
controller: TimePickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
@@ -172,7 +172,7 @@ export function settingsDirective() {
export function timePickerDirective() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/timepicker/timepicker.html',
+ templateUrl: 'public/app/features/dashboard/components/TimePicker/template.html',
controller: TimePickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
@@ -185,5 +185,5 @@ export function timePickerDirective() {
angular.module('grafana.directives').directive('gfTimePickerSettings', settingsDirective);
angular.module('grafana.directives').directive('gfTimePicker', timePickerDirective);
-import { inputDateDirective } from './input_date';
+import { inputDateDirective } from './validation';
angular.module('grafana.directives').directive('inputDatetime', inputDateDirective);
diff --git a/public/app/features/dashboard/components/TimePicker/index.ts b/public/app/features/dashboard/components/TimePicker/index.ts
new file mode 100644
index 00000000000..ca6e2792c43
--- /dev/null
+++ b/public/app/features/dashboard/components/TimePicker/index.ts
@@ -0,0 +1 @@
+export { TimePickerCtrl } from './TimePickerCtrl';
diff --git a/public/app/features/dashboard/timepicker/settings.html b/public/app/features/dashboard/components/TimePicker/settings.html
similarity index 100%
rename from public/app/features/dashboard/timepicker/settings.html
rename to public/app/features/dashboard/components/TimePicker/settings.html
diff --git a/public/app/features/dashboard/timepicker/timepicker.html b/public/app/features/dashboard/components/TimePicker/template.html
similarity index 100%
rename from public/app/features/dashboard/timepicker/timepicker.html
rename to public/app/features/dashboard/components/TimePicker/template.html
diff --git a/public/app/features/dashboard/timepicker/input_date.ts b/public/app/features/dashboard/components/TimePicker/validation.ts
similarity index 100%
rename from public/app/features/dashboard/timepicker/input_date.ts
rename to public/app/features/dashboard/components/TimePicker/validation.ts
diff --git a/public/app/features/dashboard/unsaved_changes_modal.ts b/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts
similarity index 100%
rename from public/app/features/dashboard/unsaved_changes_modal.ts
rename to public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts
diff --git a/public/app/features/dashboard/components/UnsavedChangesModal/index.ts b/public/app/features/dashboard/components/UnsavedChangesModal/index.ts
new file mode 100644
index 00000000000..43943f06694
--- /dev/null
+++ b/public/app/features/dashboard/components/UnsavedChangesModal/index.ts
@@ -0,0 +1 @@
+export { UnsavedChangesModalCtrl } from './UnsavedChangesModalCtrl';
diff --git a/public/app/features/dashboard/specs/history_ctrl.test.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts
similarity index 98%
rename from public/app/features/dashboard/specs/history_ctrl.test.ts
rename to public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts
index 632f3489dae..2b257e148f5 100644
--- a/public/app/features/dashboard/specs/history_ctrl.test.ts
+++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts
@@ -1,6 +1,6 @@
import _ from 'lodash';
-import { HistoryListCtrl } from 'app/features/dashboard/history/history';
-import { versions, compare, restore } from './history_mocks';
+import { HistoryListCtrl } from './HistoryListCtrl';
+import { versions, compare, restore } from './__mocks__/history';
import $q from 'q';
describe('HistoryListCtrl', () => {
diff --git a/public/app/features/dashboard/history/history.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts
similarity index 96%
rename from public/app/features/dashboard/history/history.ts
rename to public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts
index 3563ccc7766..b8632e2eeae 100644
--- a/public/app/features/dashboard/history/history.ts
+++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts
@@ -1,12 +1,10 @@
-import './history_srv';
-
import _ from 'lodash';
import angular from 'angular';
import moment from 'moment';
import locationUtil from 'app/core/utils/location_util';
-import { DashboardModel } from '../dashboard_model';
-import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
+import { DashboardModel } from '../../dashboard_model';
+import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './HistorySrv';
export class HistoryListCtrl {
appending: boolean;
@@ -200,7 +198,7 @@ export class HistoryListCtrl {
export function dashboardHistoryDirective() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/history/history.html',
+ templateUrl: 'public/app/features/dashboard/components/VersionHistory/template.html',
controller: HistoryListCtrl,
bindToController: true,
controllerAs: 'ctrl',
diff --git a/public/app/features/dashboard/specs/history_srv.test.ts b/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts
similarity index 90%
rename from public/app/features/dashboard/specs/history_srv.test.ts
rename to public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts
index 1e2bd57a221..75766060e7f 100644
--- a/public/app/features/dashboard/specs/history_srv.test.ts
+++ b/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts
@@ -1,7 +1,6 @@
-import '../history/history_srv';
-import { versions, restore } from './history_mocks';
-import { HistorySrv } from '../history/history_srv';
-import { DashboardModel } from '../dashboard_model';
+import { versions, restore } from './__mocks__/history';
+import { HistorySrv } from './HistorySrv';
+import { DashboardModel } from '../../dashboard_model';
jest.mock('app/core/store');
describe('historySrv', () => {
diff --git a/public/app/features/dashboard/history/history_srv.ts b/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts
similarity index 96%
rename from public/app/features/dashboard/history/history_srv.ts
rename to public/app/features/dashboard/components/VersionHistory/HistorySrv.ts
index 7f7dc950de3..d52f3ab879c 100644
--- a/public/app/features/dashboard/history/history_srv.ts
+++ b/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts
@@ -1,6 +1,6 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
-import { DashboardModel } from '../dashboard_model';
+import { DashboardModel } from '../../dashboard_model';
export interface HistoryListOpts {
limit: number;
diff --git a/public/app/features/dashboard/specs/history_mocks.ts b/public/app/features/dashboard/components/VersionHistory/__mocks__/history.ts
similarity index 100%
rename from public/app/features/dashboard/specs/history_mocks.ts
rename to public/app/features/dashboard/components/VersionHistory/__mocks__/history.ts
diff --git a/public/app/features/dashboard/components/VersionHistory/index.ts b/public/app/features/dashboard/components/VersionHistory/index.ts
new file mode 100644
index 00000000000..138de434bf3
--- /dev/null
+++ b/public/app/features/dashboard/components/VersionHistory/index.ts
@@ -0,0 +1,2 @@
+export { HistoryListCtrl } from './HistoryListCtrl';
+export { HistorySrv } from './HistorySrv';
diff --git a/public/app/features/dashboard/history/history.html b/public/app/features/dashboard/components/VersionHistory/template.html
similarity index 100%
rename from public/app/features/dashboard/history/history.html
rename to public/app/features/dashboard/components/VersionHistory/template.html
diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 6611a728803..5c4480dbad5 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -22,7 +22,6 @@ export class DashboardCtrl {
private keybindingSrv,
private timeSrv,
private variableSrv,
- private alertingSrv,
private dashboardSrv,
private unsavedChangesSrv,
private dashboardViewStateSrv,
@@ -54,7 +53,6 @@ export class DashboardCtrl {
// init services
this.timeSrv.init(dashboard);
- this.alertingSrv.init(dashboard, data.alerts);
this.annotationsSrv.init(dashboard);
// template values service needs to initialize completely before
diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx
index f0e97162d43..cfff64cb042 100644
--- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx
+++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx
@@ -5,7 +5,7 @@ import classNames from 'classnames';
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
import { importPluginModule } from 'app/features/plugins/plugin_loader';
-import { AddPanelPanel } from './AddPanelPanel';
+import { AddPanelWidget } from '../components/AddPanelWidget';
import { getPanelPluginNotFound } from './PanelPluginNotFound';
import { DashboardRow } from './DashboardRow';
import { PanelChrome } from './PanelChrome';
@@ -53,7 +53,7 @@ export class DashboardPanel extends PureComponent
{
}
renderAddPanel() {
- return ;
+ return ;
}
onPluginTypeChanged = (plugin: PanelPlugin) => {
diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx
index dbf441adaa8..4f5a74f820b 100644
--- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx
+++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx
@@ -3,7 +3,7 @@ import Remarkable from 'remarkable';
import { Tooltip } from '@grafana/ui';
import { PanelModel } from 'app/features/dashboard/panel_model';
import templateSrv from 'app/features/templating/template_srv';
-import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv';
+import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv';
enum InfoModes {
diff --git a/public/app/features/dashboard/folder_permissions_ctrl.ts b/public/app/features/dashboard/folder_permissions_ctrl.ts
deleted file mode 100644
index 4ab91acb3d9..00000000000
--- a/public/app/features/dashboard/folder_permissions_ctrl.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { FolderPageLoader } from './folder_page_loader';
-
-export class FolderPermissionsCtrl {
- navModel: any;
- folderId: number;
- uid: string;
- dashboard: any;
- meta: any;
-
- /** @ngInject */
- constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
- if (this.$routeParams.uid) {
- this.uid = $routeParams.uid;
-
- new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions').then(folder => {
- if ($location.path() !== folder.meta.url) {
- $location.path(`${folder.meta.url}/permissions`).replace();
- }
-
- this.dashboard = folder.dashboard;
- this.meta = folder.meta;
- });
- }
- }
-}
diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts
new file mode 100644
index 00000000000..efa54f0ee07
--- /dev/null
+++ b/public/app/features/dashboard/index.ts
@@ -0,0 +1,33 @@
+import './dashboard_ctrl';
+import './time_srv';
+import './dashgrid/DashboardGridDirective';
+
+// Services
+import './services/DashboardViewStateSrv';
+import './services/UnsavedChangesSrv';
+import './services/DashboardLoaderSrv';
+import './services/DashboardSrv';
+
+// Components
+import './components/DashLinks';
+import './components/DashExportModal';
+import './components/DashNav';
+import './components/ExportDataModal';
+import './components/FolderPicker';
+import './components/VersionHistory';
+import './components/DashboardSettings';
+import './components/SubMenu';
+import './components/TimePicker';
+import './components/UnsavedChangesModal';
+import './components/SaveModals';
+import './components/ShareModal';
+import './components/AdHocFilters';
+import './components/RowOptions';
+
+import DashboardPermissions from './components/DashboardPermissions/DashboardPermissions';
+
+// angular wrappers
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+
+react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
+
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
index 540b6a8353e..2651ab0608c 100644
--- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
+++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
@@ -166,6 +166,7 @@ export class QueryEditorRow extends PureComponent {
onDisableQuery = () => {
this.props.query.hide = !this.props.query.hide;
+ this.onExecuteQuery();
this.forceUpdate();
};
diff --git a/public/app/features/dashboard/partials/folder_permissions.html b/public/app/features/dashboard/partials/folder_permissions.html
deleted file mode 100644
index be44c1051f2..00000000000
--- a/public/app/features/dashboard/partials/folder_permissions.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/public/app/features/dashboard/partials/folder_settings.html b/public/app/features/dashboard/partials/folder_settings.html
deleted file mode 100644
index 8e819be5fe8..00000000000
--- a/public/app/features/dashboard/partials/folder_settings.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
diff --git a/public/app/features/dashboard/partials/inspector.html b/public/app/features/dashboard/partials/inspector.html
deleted file mode 100644
index b30bce3c5fe..00000000000
--- a/public/app/features/dashboard/partials/inspector.html
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
-
-
-
Request details
-
-
-
Request parameters
-
-
-
- {{param.key}}
-
-
- {{param.value}}
-
-
-
-
-
-
-
{{message}}
-
-{{response}}
-
-
-
-
-
-
Message:
-
-{{message}}
-
-
-
Stack trace:
-
-{{stack_trace}}
-
-
-
-
-
-
-
diff --git a/public/app/features/dashboard/specs/change_tracker.test.ts b/public/app/features/dashboard/services/ChangeTracker.test.ts
similarity index 97%
rename from public/app/features/dashboard/specs/change_tracker.test.ts
rename to public/app/features/dashboard/services/ChangeTracker.test.ts
index e7f8ce977b1..dfc9b3fa03f 100644
--- a/public/app/features/dashboard/specs/change_tracker.test.ts
+++ b/public/app/features/dashboard/services/ChangeTracker.test.ts
@@ -1,4 +1,4 @@
-import { ChangeTracker } from 'app/features/dashboard/change_tracker';
+import { ChangeTracker } from './ChangeTracker';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
diff --git a/public/app/features/dashboard/change_tracker.ts b/public/app/features/dashboard/services/ChangeTracker.ts
similarity index 98%
rename from public/app/features/dashboard/change_tracker.ts
rename to public/app/features/dashboard/services/ChangeTracker.ts
index aa71ac2e306..ef3d456db48 100644
--- a/public/app/features/dashboard/change_tracker.ts
+++ b/public/app/features/dashboard/services/ChangeTracker.ts
@@ -1,6 +1,6 @@
import angular from 'angular';
import _ from 'lodash';
-import { DashboardModel } from './dashboard_model';
+import { DashboardModel } from '../dashboard_model';
export class ChangeTracker {
current: any;
diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts
similarity index 100%
rename from public/app/features/dashboard/dashboard_loader_srv.ts
rename to public/app/features/dashboard/services/DashboardLoaderSrv.ts
diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/services/DashboardSrv.ts
similarity index 98%
rename from public/app/features/dashboard/dashboard_srv.ts
rename to public/app/features/dashboard/services/DashboardSrv.ts
index d5695a577c5..67a4938c6aa 100644
--- a/public/app/features/dashboard/dashboard_srv.ts
+++ b/public/app/features/dashboard/services/DashboardSrv.ts
@@ -1,5 +1,5 @@
import coreModule from 'app/core/core_module';
-import { DashboardModel } from './dashboard_model';
+import { DashboardModel } from '../dashboard_model';
import locationUtil from 'app/core/utils/location_util';
export class DashboardSrv {
diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts
similarity index 79%
rename from public/app/features/dashboard/specs/viewstate_srv.test.ts
rename to public/app/features/dashboard/services/DashboardViewStateSrv.test.ts
index f9963afbf85..20215017e1d 100644
--- a/public/app/features/dashboard/specs/viewstate_srv.test.ts
+++ b/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts
@@ -1,7 +1,5 @@
-//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
-import 'app/features/dashboard/view_state_srv';
import config from 'app/core/config';
-import { DashboardViewState } from '../view_state_srv';
+import { DashboardViewStateSrv } from './DashboardViewStateSrv';
import { DashboardModel } from '../dashboard_model';
describe('when updating view state', () => {
@@ -33,7 +31,7 @@ describe('when updating view state', () => {
location.search = jest.fn(() => {
return { fullscreen: true, edit: true, panelId: 1 };
});
- viewState = new DashboardViewState($scope, location, {});
+ viewState = new DashboardViewStateSrv($scope, location, {});
});
it('should update querystring and view state', () => {
@@ -55,12 +53,11 @@ describe('when updating view state', () => {
describe('to fullscreen false', () => {
beforeEach(() => {
- viewState = new DashboardViewState($scope, location, {});
+ viewState = new DashboardViewStateSrv($scope, location, {});
});
it('should remove params from query string', () => {
viewState.update({ fullscreen: true, panelId: 1, edit: true });
viewState.update({ fullscreen: false });
- expect(viewState.dashboard.meta.fullscreen).toBe(false);
expect(viewState.state.fullscreen).toBe(null);
});
});
diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts
similarity index 91%
rename from public/app/features/dashboard/view_state_srv.ts
rename to public/app/features/dashboard/services/DashboardViewStateSrv.ts
index ff12d26233d..816b6d8bd2d 100644
--- a/public/app/features/dashboard/view_state_srv.ts
+++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts
@@ -2,11 +2,11 @@ import angular from 'angular';
import _ from 'lodash';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
-import { DashboardModel } from './dashboard_model';
+import { DashboardModel } from '../dashboard_model';
// represents the transient view state
// like fullscreen panel & edit
-export class DashboardViewState {
+export class DashboardViewStateSrv {
state: any;
panelScopes: any;
$scope: any;
@@ -72,7 +72,6 @@ export class DashboardViewState {
}
_.extend(this.state, state);
- this.dashboard.meta.fullscreen = this.state.fullscreen;
if (!this.state.fullscreen) {
this.state.fullscreen = null;
@@ -117,10 +116,20 @@ export class DashboardViewState {
}
syncState() {
- if (this.dashboard.meta.fullscreen) {
+ if (this.state.fullscreen) {
const panel = this.dashboard.getPanelById(this.state.panelId);
if (!panel) {
+ this.state.fullscreen = null;
+ this.state.panelId = null;
+ this.state.edit = null;
+
+ this.update(this.state);
+
+ setTimeout(() => {
+ appEvents.emit('alert-error', ['Error', 'Panel not found']);
+ }, 100);
+
return;
}
@@ -168,7 +177,7 @@ export class DashboardViewState {
export function dashboardViewStateSrv($location, $timeout) {
return {
create: $scope => {
- return new DashboardViewState($scope, $location, $timeout);
+ return new DashboardViewStateSrv($scope, $location, $timeout);
},
};
}
diff --git a/public/app/features/dashboard/unsaved_changes_srv.ts b/public/app/features/dashboard/services/UnsavedChangesSrv.ts
similarity index 89%
rename from public/app/features/dashboard/unsaved_changes_srv.ts
rename to public/app/features/dashboard/services/UnsavedChangesSrv.ts
index f0a8bf40501..2691cc6ebf8 100644
--- a/public/app/features/dashboard/unsaved_changes_srv.ts
+++ b/public/app/features/dashboard/services/UnsavedChangesSrv.ts
@@ -1,5 +1,5 @@
import angular from 'angular';
-import { ChangeTracker } from './change_tracker';
+import { ChangeTracker } from './ChangeTracker';
/** @ngInject */
export function unsavedChangesSrv(this: any, $rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) {
diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts
index 00c960bdfaa..cfbe094125f 100644
--- a/public/app/features/dashboard/utils/panel.ts
+++ b/public/app/features/dashboard/utils/panel.ts
@@ -80,7 +80,7 @@ export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => {
appEvents.emit('show-modal', {
- src: 'public/app/features/dashboard/partials/shareModal.html',
+ src: 'public/app/features/dashboard/components/ShareModal/template.html',
model: {
dashboard: dashboard,
panel: panel,
diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx
index b6f57a76004..20ab8ee67b9 100644
--- a/public/app/features/explore/Explore.tsx
+++ b/public/app/features/explore/Explore.tsx
@@ -9,8 +9,6 @@ import { AutoSizer } from 'react-virtualized';
import store from 'app/core/store';
// Components
-import { DataSourceSelectItem } from '@grafana/ui/src/types';
-import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { Alert } from './Error';
import ErrorBoundary from './ErrorBoundary';
import GraphContainer from './GraphContainer';
@@ -21,18 +19,13 @@ import TimePicker, { parseTime } from './TimePicker';
// Actions
import {
- changeDatasource,
changeSize,
changeTime,
- clearQueries,
initializeExplore,
modifyQueries,
- runQueries,
scanStart,
scanStop,
setQueries,
- splitClose,
- splitOpen,
} from './state/actions';
// Types
@@ -41,27 +34,23 @@ import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/
import { StoreState } from 'app/types';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter';
+import { ExploreToolbar } from './ExploreToolbar';
interface ExploreProps {
StartPage?: any;
- changeDatasource: typeof changeDatasource;
changeSize: typeof changeSize;
changeTime: typeof changeTime;
- clearQueries: typeof clearQueries;
datasourceError: string;
datasourceInstance: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
- exploreDatasources: DataSourceSelectItem[];
exploreId: ExploreId;
initialDatasource?: string;
initialQueries: DataQuery[];
initializeExplore: typeof initializeExplore;
initialized: boolean;
- loading: boolean;
modifyQueries: typeof modifyQueries;
range: RawTimeRange;
- runQueries: typeof runQueries;
scanner?: RangeScanner;
scanning?: boolean;
scanRange?: RawTimeRange;
@@ -69,8 +58,6 @@ interface ExploreProps {
scanStop: typeof scanStop;
setQueries: typeof setQueries;
split: boolean;
- splitClose: typeof splitClose;
- splitOpen: typeof splitOpen;
showingStartPage?: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
@@ -145,10 +132,6 @@ export class Explore extends React.PureComponent {
this.el = el;
};
- onChangeDatasource = async option => {
- this.props.changeDatasource(this.props.exploreId, option.value);
- };
-
onChangeTime = (range: TimeRange, changedByScanner?: boolean) => {
if (this.props.scanning && !changedByScanner) {
this.onStopScanning();
@@ -156,23 +139,11 @@ export class Explore extends React.PureComponent {
this.props.changeTime(this.props.exploreId, range);
};
- onClickClear = () => {
- this.props.clearQueries(this.props.exploreId);
- };
-
- onClickCloseSplit = () => {
- this.props.splitClose();
- };
-
// Use this in help pages to set page to a single query
onClickExample = (query: DataQuery) => {
this.props.setQueries(this.props.exploreId, [query]);
};
- onClickSplit = () => {
- this.props.splitOpen();
- };
-
onClickLabel = (key: string, value: string) => {
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
};
@@ -204,10 +175,6 @@ export class Explore extends React.PureComponent {
this.props.scanStop(this.props.exploreId);
};
- onSubmit = () => {
- this.props.runQueries(this.props.exploreId);
- };
-
render() {
const {
StartPage,
@@ -215,11 +182,8 @@ export class Explore extends React.PureComponent {
datasourceError,
datasourceLoading,
datasourceMissing,
- exploreDatasources,
exploreId,
- loading,
initialQueries,
- range,
showingStartPage,
split,
supportsGraph,
@@ -227,64 +191,10 @@ export class Explore extends React.PureComponent {
supportsTable,
} = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore';
- const selectedDatasource = datasourceInstance
- ? exploreDatasources.find(d => d.name === datasourceInstance.name)
- : undefined;
return (
-
- {exploreId === 'left' ? (
-
- ) : (
- <>
-
-
-
- Close Split
-
-
- >
- )}
- {!datasourceMissing ? (
-
-
-
- ) : null}
-
- {exploreId === 'left' && !split ? (
-
-
- Split
-
-
- ) : null}
-
-
-
- Clear All
-
-
-
-
- Run Query{' '}
- {loading ? (
-
- ) : (
-
- )}
-
-
-
+
{datasourceLoading ?
Loading datasource...
: null}
{datasourceMissing ? (
Please add a datasource that supports Explore (e.g., Prometheus).
@@ -341,30 +251,24 @@ function mapStateToProps(state: StoreState, { exploreId }) {
datasourceInstance,
datasourceLoading,
datasourceMissing,
- exploreDatasources,
initialDatasource,
initialQueries,
initialized,
- queryTransactions,
range,
showingStartPage,
supportsGraph,
supportsLogs,
supportsTable,
} = item;
- const loading = queryTransactions.some(qt => !qt.done);
return {
StartPage,
datasourceError,
datasourceInstance,
datasourceLoading,
datasourceMissing,
- exploreDatasources,
initialDatasource,
initialQueries,
initialized,
- loading,
- queryTransactions,
range,
showingStartPage,
split,
@@ -375,18 +279,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
}
const mapDispatchToProps = {
- changeDatasource,
changeSize,
changeTime,
- clearQueries,
initializeExplore,
modifyQueries,
- runQueries,
scanStart,
scanStop,
setQueries,
- splitClose,
- splitOpen,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore));
diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx
new file mode 100644
index 00000000000..35f06d11c81
--- /dev/null
+++ b/public/app/features/explore/ExploreToolbar.tsx
@@ -0,0 +1,191 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import { hot } from 'react-hot-loader';
+
+import { ExploreId } from 'app/types/explore';
+import { DataSourceSelectItem, RawTimeRange, TimeRange } from '@grafana/ui';
+import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
+import { StoreState } from 'app/types/store';
+import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions';
+import TimePicker from './TimePicker';
+
+enum IconSide {
+ left = 'left',
+ right = 'right',
+}
+
+const createResponsiveButton = (options: {
+ splitted: boolean;
+ title: string;
+ onClick: () => void;
+ buttonClassName?: string;
+ iconClassName?: string;
+ iconSide?: IconSide;
+}) => {
+ const defaultOptions = {
+ iconSide: IconSide.left,
+ };
+ const props = { ...options, defaultOptions };
+ const { title, onClick, buttonClassName, iconClassName, splitted, iconSide } = props;
+
+ return (
+
+ {iconClassName && iconSide === IconSide.left ? : null}
+ {!splitted ? title : ''}
+ {iconClassName && iconSide === IconSide.right ? : null}
+
+ );
+};
+
+interface OwnProps {
+ exploreId: ExploreId;
+ timepickerRef: React.RefObject
;
+ onChangeTime: (range: TimeRange, changedByScanner?: boolean) => void;
+}
+
+interface StateProps {
+ datasourceMissing: boolean;
+ exploreDatasources: DataSourceSelectItem[];
+ loading: boolean;
+ range: RawTimeRange;
+ selectedDatasource: DataSourceSelectItem;
+ splitted: boolean;
+}
+
+interface DispatchProps {
+ changeDatasource: typeof changeDatasource;
+ clearAll: typeof clearQueries;
+ runQuery: typeof runQueries;
+ closeSplit: typeof splitClose;
+ split: typeof splitOpen;
+}
+
+type Props = StateProps & DispatchProps & OwnProps;
+
+export class UnConnectedExploreToolbar extends PureComponent {
+ constructor(props) {
+ super(props);
+ }
+
+ onChangeDatasource = async option => {
+ this.props.changeDatasource(this.props.exploreId, option.value);
+ };
+
+ onClearAll = () => {
+ this.props.clearAll(this.props.exploreId);
+ };
+
+ onRunQuery = () => {
+ this.props.runQuery(this.props.exploreId);
+ };
+
+ render() {
+ const {
+ datasourceMissing,
+ exploreDatasources,
+ exploreId,
+ loading,
+ range,
+ selectedDatasource,
+ splitted,
+ timepickerRef,
+ } = this.props;
+
+ return (
+
+
+
+
+
+ {exploreId === 'right' && (
+
+
+
+ )}
+
+
+
+
+
+ {!datasourceMissing ? (
+
+ ) : null}
+ {exploreId === 'left' && !splitted ? (
+
+ {createResponsiveButton({
+ splitted,
+ title: 'Split',
+ onClick: this.props.split,
+ iconClassName: 'fa fa-fw fa-columns icon-margin-right',
+ iconSide: IconSide.left,
+ })}
+
+ ) : null}
+
+
+
+
+
+ Clear All
+
+
+
+ {createResponsiveButton({
+ splitted,
+ title: 'Run Query',
+ onClick: this.onRunQuery,
+ buttonClassName: 'navbar-button--primary',
+ iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon',
+ iconSide: IconSide.right,
+ })}
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
+ const splitted = state.explore.split;
+ const exploreItem = state.explore[exploreId];
+ const { datasourceInstance, datasourceMissing, exploreDatasources, queryTransactions, range } = exploreItem;
+ const selectedDatasource = datasourceInstance
+ ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
+ : undefined;
+ const loading = queryTransactions.some(qt => !qt.done);
+
+ return {
+ datasourceMissing,
+ exploreDatasources,
+ loading,
+ range,
+ selectedDatasource,
+ splitted,
+ };
+};
+
+const mapDispatchToProps: DispatchProps = {
+ changeDatasource,
+ clearAll: clearQueries,
+ runQuery: runQueries,
+ closeSplit: splitClose,
+ split: splitOpen,
+};
+
+export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx
index e2610bcc781..c3a92f4007e 100644
--- a/public/app/features/explore/GraphContainer.tsx
+++ b/public/app/features/explore/GraphContainer.tsx
@@ -30,6 +30,11 @@ export class GraphContainer extends PureComponent {
render() {
const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props;
const graphHeight = showingGraph && showingTable ? '200px' : '400px';
+
+ if (!graphResult) {
+ return null;
+ }
+
return (
LogRowModel[];
+ label: string;
+ plain?: boolean;
+ value: string;
+ onClickLabel?: (label: string, value: string) => void;
+}
+
+interface State {
+ showStats: boolean;
+ stats: LogLabelStatsModel[];
+}
+
+export class LogLabel extends PureComponent {
+ state = {
+ stats: null,
+ showStats: false,
+ };
+
+ onClickClose = () => {
+ this.setState({ showStats: false });
+ };
+
+ onClickLabel = () => {
+ const { onClickLabel, label, value } = this.props;
+ if (onClickLabel) {
+ onClickLabel(label, value);
+ }
+ };
+
+ onClickStats = () => {
+ this.setState(state => {
+ if (state.showStats) {
+ return { showStats: false, stats: null };
+ }
+ const allRows = this.props.getRows();
+ const stats = calculateLogsLabelStats(allRows, this.props.label);
+ return { showStats: true, stats };
+ });
+ };
+
+ render() {
+ const { getRows, label, plain, value } = this.props;
+ const { showStats, stats } = this.state;
+ const tooltip = `${label}: ${value}`;
+ return (
+
+
+ {value}
+
+ {!plain && (
+
+ )}
+ {!plain && getRows && }
+ {showStats && (
+
+
+
+ )}
+
+ );
+ }
+}
diff --git a/public/app/features/explore/LogLabelStats.tsx b/public/app/features/explore/LogLabelStats.tsx
new file mode 100644
index 00000000000..b0bd69170c5
--- /dev/null
+++ b/public/app/features/explore/LogLabelStats.tsx
@@ -0,0 +1,72 @@
+import React, { PureComponent } from 'react';
+import classnames from 'classnames';
+import { LogLabelStatsModel } from 'app/core/logs_model';
+
+function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) {
+ const { active, count, proportion, value } = logLabelStatsModel;
+ const percent = `${Math.round(proportion * 100)}%`;
+ const barStyle = { width: percent };
+ const className = classnames('logs-stats-row', { 'logs-stats-row--active': active });
+
+ return (
+
+
+
{value}
+
{count}
+
{percent}
+
+
+
+ );
+}
+
+const STATS_ROW_LIMIT = 5;
+
+interface Props {
+ stats: LogLabelStatsModel[];
+ label: string;
+ value: string;
+ rowCount: number;
+ onClickClose: () => void;
+}
+
+export class LogLabelStats extends PureComponent {
+ render() {
+ const { label, rowCount, stats, value, onClickClose } = this.props;
+ const topRows = stats.slice(0, STATS_ROW_LIMIT);
+ let activeRow = topRows.find(row => row.value === value);
+ let otherRows = stats.slice(STATS_ROW_LIMIT);
+ const insertActiveRow = !activeRow;
+
+ // Remove active row from other to show extra
+ if (insertActiveRow) {
+ activeRow = otherRows.find(row => row.value === value);
+ otherRows = otherRows.filter(row => row.value !== value);
+ }
+
+ const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0);
+ const topCount = topRows.reduce((sum, row) => sum + row.count, 0);
+ const total = topCount + otherCount;
+ const otherProportion = otherCount / total;
+
+ return (
+
+
+
+ {label}: {total} of {rowCount} rows have that label
+
+
+
+
+ {topRows.map(stat => )}
+ {insertActiveRow && activeRow && }
+ {otherCount > 0 && (
+
+ )}
+
+
+ );
+ }
+}
diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx
index 7675fb13152..7105a2a5370 100644
--- a/public/app/features/explore/LogLabels.tsx
+++ b/public/app/features/explore/LogLabels.tsx
@@ -1,147 +1,20 @@
import React, { PureComponent } from 'react';
-import classnames from 'classnames';
-import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model';
+import { LogsStreamLabels, LogRowModel } from 'app/core/logs_model';
+import { LogLabel } from './LogLabel';
-function StatsRow({ active, count, proportion, value }: LogsLabelStat) {
- const percent = `${Math.round(proportion * 100)}%`;
- const barStyle = { width: percent };
- const className = classnames('logs-stats-row', { 'logs-stats-row--active': active });
-
- return (
-
-
-
{value}
-
{count}
-
{percent}
-
-
-
- );
-}
-
-const STATS_ROW_LIMIT = 5;
-export class Stats extends PureComponent<{
- stats: LogsLabelStat[];
- label: string;
- value: string;
- rowCount: number;
- onClickClose: () => void;
-}> {
- render() {
- const { label, rowCount, stats, value, onClickClose } = this.props;
- const topRows = stats.slice(0, STATS_ROW_LIMIT);
- let activeRow = topRows.find(row => row.value === value);
- let otherRows = stats.slice(STATS_ROW_LIMIT);
- const insertActiveRow = !activeRow;
- // Remove active row from other to show extra
- if (insertActiveRow) {
- activeRow = otherRows.find(row => row.value === value);
- otherRows = otherRows.filter(row => row.value !== value);
- }
- const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0);
- const topCount = topRows.reduce((sum, row) => sum + row.count, 0);
- const total = topCount + otherCount;
- const otherProportion = otherCount / total;
-
- return (
-
-
-
- {label}: {total} of {rowCount} rows have that label
-
-
-
-
- {topRows.map(stat => )}
- {insertActiveRow && activeRow && }
- {otherCount > 0 && (
-
- )}
-
-
- );
- }
-}
-
-class Label extends PureComponent<
- {
- getRows?: () => LogRow[];
- label: string;
- plain?: boolean;
- value: string;
- onClickLabel?: (label: string, value: string) => void;
- },
- { showStats: boolean; stats: LogsLabelStat[] }
-> {
- state = {
- stats: null,
- showStats: false,
- };
-
- onClickClose = () => {
- this.setState({ showStats: false });
- };
-
- onClickLabel = () => {
- const { onClickLabel, label, value } = this.props;
- if (onClickLabel) {
- onClickLabel(label, value);
- }
- };
-
- onClickStats = () => {
- this.setState(state => {
- if (state.showStats) {
- return { showStats: false, stats: null };
- }
- const allRows = this.props.getRows();
- const stats = calculateLogsLabelStats(allRows, this.props.label);
- return { showStats: true, stats };
- });
- };
-
- render() {
- const { getRows, label, plain, value } = this.props;
- const { showStats, stats } = this.state;
- const tooltip = `${label}: ${value}`;
- return (
-
-
- {value}
-
- {!plain && (
-
- )}
- {!plain && getRows && }
- {showStats && (
-
-
-
- )}
-
- );
- }
-}
-
-export default class LogLabels extends PureComponent<{
- getRows?: () => LogRow[];
+interface Props {
+ getRows?: () => LogRowModel[];
labels: LogsStreamLabels;
plain?: boolean;
onClickLabel?: (label: string, value: string) => void;
-}> {
+}
+
+export class LogLabels extends PureComponent {
render() {
const { getRows, labels, onClickLabel, plain } = this.props;
return Object.keys(labels).map(key => (
-
+
));
}
}
diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx
new file mode 100644
index 00000000000..66b0e6a69fe
--- /dev/null
+++ b/public/app/features/explore/LogRow.tsx
@@ -0,0 +1,194 @@
+import React, { PureComponent } from 'react';
+import _ from 'lodash';
+import Highlighter from 'react-highlight-words';
+import classnames from 'classnames';
+
+import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
+import { LogLabels } from './LogLabels';
+import { findHighlightChunksInText } from 'app/core/utils/text';
+import { LogLabelStats } from './LogLabelStats';
+
+interface Props {
+ highlighterExpressions?: string[];
+ row: LogRowModel;
+ showDuplicates: boolean;
+ showLabels: boolean | null; // Tristate: null means auto
+ showLocalTime: boolean;
+ showUtc: boolean;
+ getRows: () => LogRowModel[];
+ onClickLabel?: (label: string, value: string) => void;
+}
+
+interface State {
+ fieldCount: number;
+ fieldLabel: string;
+ fieldStats: LogLabelStatsModel[];
+ fieldValue: string;
+ parsed: boolean;
+ parser?: LogsParser;
+ parsedFieldHighlights: string[];
+ showFieldStats: boolean;
+}
+
+/**
+ * Renders a highlighted field.
+ * When hovering, a stats icon is shown.
+ */
+const FieldHighlight = onClick => props => {
+ return (
+
+ {props.children}
+ onClick(props.children)} />
+
+ );
+};
+
+/**
+ * Renders a log line.
+ *
+ * When user hovers over it for a certain time, it lazily parses the log line.
+ * Once a parser is found, it will determine fields, that will be highlighted.
+ * When the user requests stats for a field, they will be calculated and rendered below the row.
+ */
+export class LogRow extends PureComponent {
+ mouseMessageTimer: NodeJS.Timer;
+
+ state = {
+ fieldCount: 0,
+ fieldLabel: null,
+ fieldStats: null,
+ fieldValue: null,
+ parsed: false,
+ parser: undefined,
+ parsedFieldHighlights: [],
+ showFieldStats: false,
+ };
+
+ componentWillUnmount() {
+ clearTimeout(this.mouseMessageTimer);
+ }
+
+ onClickClose = () => {
+ this.setState({ showFieldStats: false });
+ };
+
+ onClickHighlight = (fieldText: string) => {
+ const { getRows } = this.props;
+ const { parser } = this.state;
+ const allRows = getRows();
+
+ // Build value-agnostic row matcher based on the field label
+ const fieldLabel = parser.getLabelFromField(fieldText);
+ const fieldValue = parser.getValueFromField(fieldText);
+ const matcher = parser.buildMatcher(fieldLabel);
+ const fieldStats = calculateFieldStats(allRows, matcher);
+ const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
+
+ this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
+ };
+
+ onMouseOverMessage = () => {
+ // Don't parse right away, user might move along
+ this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
+ };
+
+ onMouseOutMessage = () => {
+ clearTimeout(this.mouseMessageTimer);
+ this.setState({ parsed: false });
+ };
+
+ parseMessage = () => {
+ if (!this.state.parsed) {
+ const { row } = this.props;
+ const parser = getParser(row.entry);
+ if (parser) {
+ // Use parser to highlight detected fields
+ const parsedFieldHighlights = parser.getFields(this.props.row.entry);
+ this.setState({ parsedFieldHighlights, parsed: true, parser });
+ }
+ }
+ };
+
+ render() {
+ const {
+ getRows,
+ highlighterExpressions,
+ onClickLabel,
+ row,
+ showDuplicates,
+ showLabels,
+ showLocalTime,
+ showUtc,
+ } = this.props;
+ const {
+ fieldCount,
+ fieldLabel,
+ fieldStats,
+ fieldValue,
+ parsed,
+ parsedFieldHighlights,
+ showFieldStats,
+ } = this.state;
+ const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
+ const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
+ const needsHighlighter = highlights && highlights.length > 0;
+ const highlightClassName = classnames('logs-row__match-highlight', {
+ 'logs-row__match-highlight--preview': previewHighlights,
+ });
+ return (
+
+ {showDuplicates && (
+
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
+ )}
+
+ {showUtc && (
+
+ {row.timestamp}
+
+ )}
+ {showLocalTime && (
+
+ {row.timeLocal}
+
+ )}
+ {showLabels && (
+
+
+
+ )}
+
+ {parsed && (
+
+ )}
+ {!parsed &&
+ needsHighlighter && (
+
+ )}
+ {!parsed && !needsHighlighter && row.entry}
+ {showFieldStats && (
+
+
+
+ )}
+
+
+ );
+ }
+}
diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx
index 650cddd035e..6f8077064e0 100644
--- a/public/app/features/explore/Logs.tsx
+++ b/public/app/features/explore/Logs.tsx
@@ -1,7 +1,5 @@
import _ from 'lodash';
import React, { PureComponent } from 'react';
-import Highlighter from 'react-highlight-words';
-import classnames from 'classnames';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch } from '@grafana/ui';
@@ -11,19 +9,15 @@ import {
LogsModel,
dedupLogRows,
filterLogLevels,
- getParser,
LogLevel,
LogsMetaKind,
- LogsLabelStat,
- LogsParser,
- LogRow,
- calculateFieldStats,
} from 'app/core/logs_model';
-import { findHighlightChunksInText } from 'app/core/utils/text';
+
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph';
-import LogLabels, { Stats } from './LogLabels';
+import { LogLabels } from './LogLabels';
+import { LogRow } from './LogRow';
const PREVIEW_LIMIT = 100;
@@ -42,191 +36,6 @@ const graphOptions = {
},
};
-/**
- * Renders a highlighted field.
- * When hovering, a stats icon is shown.
- */
-const FieldHighlight = onClick => props => {
- return (
-
- {props.children}
- onClick(props.children)} />
-
- );
-};
-
-interface RowProps {
- highlighterExpressions?: string[];
- row: LogRow;
- showDuplicates: boolean;
- showLabels: boolean | null; // Tristate: null means auto
- showLocalTime: boolean;
- showUtc: boolean;
- getRows: () => LogRow[];
- onClickLabel?: (label: string, value: string) => void;
-}
-
-interface RowState {
- fieldCount: number;
- fieldLabel: string;
- fieldStats: LogsLabelStat[];
- fieldValue: string;
- parsed: boolean;
- parser?: LogsParser;
- parsedFieldHighlights: string[];
- showFieldStats: boolean;
-}
-
-/**
- * Renders a log line.
- *
- * When user hovers over it for a certain time, it lazily parses the log line.
- * Once a parser is found, it will determine fields, that will be highlighted.
- * When the user requests stats for a field, they will be calculated and rendered below the row.
- */
-class Row extends PureComponent {
- mouseMessageTimer: NodeJS.Timer;
-
- state = {
- fieldCount: 0,
- fieldLabel: null,
- fieldStats: null,
- fieldValue: null,
- parsed: false,
- parser: undefined,
- parsedFieldHighlights: [],
- showFieldStats: false,
- };
-
- componentWillUnmount() {
- clearTimeout(this.mouseMessageTimer);
- }
-
- onClickClose = () => {
- this.setState({ showFieldStats: false });
- };
-
- onClickHighlight = (fieldText: string) => {
- const { getRows } = this.props;
- const { parser } = this.state;
- const allRows = getRows();
-
- // Build value-agnostic row matcher based on the field label
- const fieldLabel = parser.getLabelFromField(fieldText);
- const fieldValue = parser.getValueFromField(fieldText);
- const matcher = parser.buildMatcher(fieldLabel);
- const fieldStats = calculateFieldStats(allRows, matcher);
- const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
-
- this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
- };
-
- onMouseOverMessage = () => {
- // Don't parse right away, user might move along
- this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
- };
-
- onMouseOutMessage = () => {
- clearTimeout(this.mouseMessageTimer);
- this.setState({ parsed: false });
- };
-
- parseMessage = () => {
- if (!this.state.parsed) {
- const { row } = this.props;
- const parser = getParser(row.entry);
- if (parser) {
- // Use parser to highlight detected fields
- const parsedFieldHighlights = parser.getFields(this.props.row.entry);
- this.setState({ parsedFieldHighlights, parsed: true, parser });
- }
- }
- };
-
- render() {
- const {
- getRows,
- highlighterExpressions,
- onClickLabel,
- row,
- showDuplicates,
- showLabels,
- showLocalTime,
- showUtc,
- } = this.props;
- const {
- fieldCount,
- fieldLabel,
- fieldStats,
- fieldValue,
- parsed,
- parsedFieldHighlights,
- showFieldStats,
- } = this.state;
- const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
- const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
- const needsHighlighter = highlights && highlights.length > 0;
- const highlightClassName = classnames('logs-row__match-highlight', {
- 'logs-row__match-highlight--preview': previewHighlights,
- });
- return (
-
- {showDuplicates && (
-
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
- )}
-
- {showUtc && (
-
- {row.timestamp}
-
- )}
- {showLocalTime && (
-
- {row.timeLocal}
-
- )}
- {showLabels && (
-
-
-
- )}
-
- {parsed && (
-
- )}
- {!parsed &&
- needsHighlighter && (
-
- )}
- {!parsed && !needsHighlighter && row.entry}
- {showFieldStats && (
-
-
-
- )}
-
-
- );
- }
-}
-
function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) {
return (
@@ -238,8 +47,8 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
return value;
}
-interface LogsProps {
- data: LogsModel;
+interface Props {
+ data?: LogsModel;
exploreId: string;
highlighterExpressions: string[];
loading: boolean;
@@ -252,7 +61,7 @@ interface LogsProps {
onStopScanning?: () => void;
}
-interface LogsState {
+interface State {
dedup: LogsDedupStrategy;
deferLogs: boolean;
hiddenLogLevels: Set;
@@ -262,7 +71,7 @@ interface LogsState {
showUtc: boolean;
}
-export default class Logs extends PureComponent {
+export default class Logs extends PureComponent {
deferLogsTimer: NodeJS.Timer;
renderAllTimer: NodeJS.Timer;
@@ -355,6 +164,11 @@ export default class Logs extends PureComponent {
scanning,
scanRange,
} = this.props;
+
+ if (!data) {
+ return null;
+ }
+
const { dedup, deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc } = this.state;
let { showLabels } = this.state;
const hasData = data && data.rows && data.rows.length > 0;
@@ -440,10 +254,9 @@ export default class Logs extends PureComponent {
{hasData &&
- !deferLogs &&
- // Only inject highlighterExpression in the first set for performance reasons
+ !deferLogs && // Only inject highlighterExpression in the first set for performance reasons
firstRows.map(row => (
-
{
!deferLogs &&
renderAll &&
lastRows.map(row => (
- {
scanning,
scanRange,
} = this.props;
+
return (
{
}
render() {
- return (this.element = element)} style={{ width: '100%' }} />;
+ return
(this.element = element)} style={{ width: '100%' }} />;
}
}
diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx
index 24b8b8f5b16..d27213cea34 100644
--- a/public/app/features/explore/QueryField.tsx
+++ b/public/app/features/explore/QueryField.tsx
@@ -73,6 +73,7 @@ export class QueryField extends React.PureComponent
{
- this.setState({
- suggestions: [],
- typeaheadIndex: 0,
- typeaheadPrefix: '',
- typeaheadContext: null,
- });
- this.resetTimer = null;
+ if (this.mounted) {
+ this.setState({
+ suggestions: [],
+ typeaheadIndex: 0,
+ typeaheadPrefix: '',
+ typeaheadContext: null,
+ });
+ this.resetTimer = null;
+ }
};
handleBlur = () => {
diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx
index 1d00a441e14..f386e5ab99b 100644
--- a/public/app/features/explore/TableContainer.tsx
+++ b/public/app/features/explore/TableContainer.tsx
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
-import { toggleGraph } from './state/actions';
+import { toggleTable } from './state/actions';
import Table from './Table';
import Panel from './Panel';
import TableModel from 'app/core/table_model';
@@ -16,16 +16,21 @@ interface TableContainerProps {
onClickCell: (key: string, value: string) => void;
showingTable: boolean;
tableResult?: TableModel;
- toggleGraph: typeof toggleGraph;
+ toggleTable: typeof toggleTable;
}
export class TableContainer extends PureComponent {
onClickTableButton = () => {
- this.props.toggleGraph(this.props.exploreId);
+ this.props.toggleTable(this.props.exploreId);
};
render() {
const { loading, onClickCell, showingTable, tableResult } = this.props;
+
+ if (!tableResult) {
+ return null;
+ }
+
return (
@@ -43,7 +48,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
}
const mapDispatchToProps = {
- toggleGraph,
+ toggleTable,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx
index 8476c6b2b27..38c3f2e7498 100644
--- a/public/app/features/explore/TimePicker.tsx
+++ b/public/app/features/explore/TimePicker.tsx
@@ -293,6 +293,7 @@ export default class TimePicker extends PureComponent
diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx
index 7ea8f228af8..aca2e6d8cbd 100644
--- a/public/app/features/explore/Wrapper.tsx
+++ b/public/app/features/explore/Wrapper.tsx
@@ -7,14 +7,16 @@ import { StoreState } from 'app/types';
import { ExploreId, ExploreUrlState } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
-import { initializeExploreSplit } from './state/actions';
+import { initializeExploreSplit, resetExplore } from './state/actions';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
+import { CustomScrollbar } from '@grafana/ui';
interface WrapperProps {
initializeExploreSplit: typeof initializeExploreSplit;
split: boolean;
updateLocation: typeof updateLocation;
+ resetExplore: typeof resetExplore;
urlStates: { [key: string]: string };
}
@@ -41,20 +43,28 @@ export class Wrapper extends Component
{
}
}
+ componentWillUnmount() {
+ this.props.resetExplore();
+ }
+
render() {
const { split } = this.props;
const { leftState, rightState } = this.urlStates;
return (
-
-
-
-
- {split && (
-
-
-
- )}
+
+
+
+
+
+
+ {split && (
+
+
+
+ )}
+
+
);
}
@@ -69,6 +79,7 @@ const mapStateToProps = (state: StoreState) => {
const mapDispatchToProps = {
initializeExploreSplit,
updateLocation,
+ resetExplore,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts
index 850f2137541..21918e1c013 100644
--- a/public/app/features/explore/state/actionTypes.ts
+++ b/public/app/features/explore/state/actionTypes.ts
@@ -1,6 +1,6 @@
// Types
import { Emitter } from 'app/core/core';
-import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
+import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
import {
ExploreId,
ExploreItemState,
@@ -41,6 +41,7 @@ export enum ActionTypes {
ToggleGraph = 'explore/TOGGLE_GRAPH',
ToggleLogs = 'explore/TOGGLE_LOGS',
ToggleTable = 'explore/TOGGLE_TABLE',
+ ResetExplore = 'explore/RESET_EXPLORE',
}
export interface AddQueryRowAction {
@@ -123,7 +124,7 @@ export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
payload: {
exploreId: ExploreId;
- datasourceId: number;
+ datasourceName: string;
};
}
@@ -270,6 +271,11 @@ export interface ToggleLogsAction {
};
}
+export interface ResetExploreAction {
+ type: ActionTypes.ResetExplore;
+ payload: {};
+}
+
export type Action =
| AddQueryRowAction
| ChangeQueryAction
@@ -297,4 +303,5 @@ export type Action =
| SplitOpenAction
| ToggleGraphAction
| ToggleLogsAction
- | ToggleTableAction;
+ | ToggleTableAction
+ | ResetExploreAction;
diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts
index 34169a999a3..59df0c47ef9 100644
--- a/public/app/features/explore/state/actions.ts
+++ b/public/app/features/explore/state/actions.ts
@@ -21,7 +21,7 @@ import { updateLocation } from 'app/core/actions';
// Types
import { StoreState } from 'app/types';
-import { DataQuery, DataSourceSelectItem, QueryHint } from '@grafana/ui/src/types';
+import { DataQuery, DataSourceSelectItem, QueryHint } from '@grafana/ui/src/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
ExploreId,
@@ -33,7 +33,7 @@ import {
} from 'app/types/explore';
import { Emitter } from 'app/core/core';
-import { RawTimeRange, TimeRange } from '@grafana/ui';
+import { RawTimeRange, TimeRange, DataSourceApi } from '@grafana/ui';
import {
Action as ThunkableAction,
ActionTypes,
@@ -48,7 +48,6 @@ import {
ScanStopAction,
} from './actionTypes';
-
type ThunkResult
= ThunkAction;
/**
@@ -216,11 +215,11 @@ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissi
/**
* Start the async process of loading a datasource to display a loading indicator
*/
-export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({
+export const loadDatasourcePending = (exploreId: ExploreId, datasourceName: string): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
payload: {
exploreId,
- datasourceId,
+ datasourceName,
},
});
@@ -266,12 +265,12 @@ export const loadDatasourceSuccess = (
/**
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
*/
-export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult {
+export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult {
return async (dispatch, getState) => {
- const datasourceId = instance.meta.id;
+ const datasourceName = instance.name;
// Keep ID to track selection
- dispatch(loadDatasourcePending(exploreId, datasourceId));
+ dispatch(loadDatasourcePending(exploreId, datasourceName));
let datasourceError = null;
try {
@@ -280,12 +279,13 @@ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult
} catch (error) {
datasourceError = (error && error.statusText) || 'Network error';
}
+
if (datasourceError) {
dispatch(loadDatasourceFailure(exploreId, datasourceError));
return;
}
- if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
+ if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
return;
}
@@ -311,7 +311,7 @@ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult
}
}
- if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
+ if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
return;
}
@@ -538,6 +538,7 @@ export function runQueries(exploreId: ExploreId) {
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
+ dispatch(stateSave()); // Remember to saves to state and update location
return;
}
@@ -765,3 +766,12 @@ export function toggleTable(exploreId: ExploreId): ThunkResult {
}
};
}
+
+/**
+ * Resets state for explore.
+ */
+export function resetExplore(): ThunkResult {
+ return dispatch => {
+ dispatch({ type: ActionTypes.ResetExplore, payload: {} });
+ };
+}
diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts
new file mode 100644
index 00000000000..8227a947c5b
--- /dev/null
+++ b/public/app/features/explore/state/reducers.test.ts
@@ -0,0 +1,42 @@
+import { Action, ActionTypes } from './actionTypes';
+import { itemReducer, makeExploreItemState } from './reducers';
+import { ExploreId } from 'app/types/explore';
+
+describe('Explore item reducer', () => {
+ describe('scanning', () => {
+ test('should start scanning', () => {
+ let state = makeExploreItemState();
+ const action: Action = {
+ type: ActionTypes.ScanStart,
+ payload: {
+ exploreId: ExploreId.left,
+ scanner: jest.fn(),
+ },
+ };
+ state = itemReducer(state, action);
+ expect(state.scanning).toBeTruthy();
+ expect(state.scanner).toBe(action.payload.scanner);
+ });
+ test('should stop scanning', () => {
+ let state = makeExploreItemState();
+ const start: Action = {
+ type: ActionTypes.ScanStart,
+ payload: {
+ exploreId: ExploreId.left,
+ scanner: jest.fn(),
+ },
+ };
+ state = itemReducer(state, start);
+ expect(state.scanning).toBeTruthy();
+ const action: Action = {
+ type: ActionTypes.ScanStop,
+ payload: {
+ exploreId: ExploreId.left,
+ },
+ };
+ state = itemReducer(state, action);
+ expect(state.scanning).toBeFalsy();
+ expect(state.scanner).toBeUndefined();
+ });
+ });
+});
diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts
index ba87e8818df..5e9cc23c64f 100644
--- a/public/app/features/explore/state/reducers.ts
+++ b/public/app/features/explore/state/reducers.ts
@@ -20,7 +20,7 @@ const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
/**
* Returns a fresh Explore area state
*/
-const makeExploreItemState = (): ExploreItemState => ({
+export const makeExploreItemState = (): ExploreItemState => ({
StartPage: undefined,
containerWidth: 0,
datasourceInstance: null,
@@ -48,7 +48,7 @@ const makeExploreItemState = (): ExploreItemState => ({
/**
* Global Explore state that handles multiple Explore areas and the split state
*/
-const initialExploreState: ExploreState = {
+export const initialExploreState: ExploreState = {
split: null,
left: makeExploreItemState(),
right: makeExploreItemState(),
@@ -57,7 +57,7 @@ const initialExploreState: ExploreState = {
/**
* Reducer for an Explore area, to be used by the global Explore reducer.
*/
-const itemReducer = (state, action: Action): ExploreItemState => {
+export const itemReducer = (state, action: Action): ExploreItemState => {
switch (action.type) {
case ActionTypes.AddQueryRow: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
@@ -185,7 +185,7 @@ const itemReducer = (state, action: Action): ExploreItemState => {
}
case ActionTypes.LoadDatasourcePending: {
- return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId };
+ return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.datasourceName };
}
case ActionTypes.LoadDatasourceSuccess: {
@@ -217,6 +217,7 @@ const itemReducer = (state, action: Action): ExploreItemState => {
supportsTable,
datasourceLoading: false,
datasourceMissing: false,
+ datasourceError: null,
logsHighlighterExpressions: undefined,
modifiedQueries: initialQueries.slice(),
queryTransactions: [],
@@ -277,7 +278,7 @@ const itemReducer = (state, action: Action): ExploreItemState => {
}
case ActionTypes.QueryTransactionStart: {
- const { datasourceInstance, queryIntervals, queryTransactions } = state;
+ const { queryTransactions } = state;
const { resultType, rowIndex, transaction } = action.payload;
// Discarding existing transactions of same type
const remainingTransactions = queryTransactions.filter(
@@ -287,15 +288,9 @@ const itemReducer = (state, action: Action): ExploreItemState => {
// Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
- const results = calculateResultsFromQueryTransactions(
- nextQueryTransactions,
- datasourceInstance,
- queryIntervals.intervalMs
- );
return {
...state,
- ...results,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
};
@@ -359,13 +354,19 @@ const itemReducer = (state, action: Action): ExploreItemState => {
}
case ActionTypes.ScanStart: {
- return { ...state, scanning: true };
+ return { ...state, scanning: true, scanner: action.payload.scanner };
}
case ActionTypes.ScanStop: {
const { queryTransactions } = state;
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
- return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined };
+ return {
+ ...state,
+ queryTransactions: nextQueryTransactions,
+ scanning: false,
+ scanRange: undefined,
+ scanner: undefined,
+ };
}
case ActionTypes.SetQueries: {
@@ -421,25 +422,19 @@ const itemReducer = (state, action: Action): ExploreItemState => {
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
switch (action.type) {
case ActionTypes.SplitClose: {
- return {
- ...state,
- split: false,
- };
+ return { ...state, split: false };
}
case ActionTypes.SplitOpen: {
- return {
- ...state,
- split: true,
- right: action.payload.itemState,
- };
+ return { ...state, split: true, right: action.payload.itemState };
}
case ActionTypes.InitializeExploreSplit: {
- return {
- ...state,
- split: true,
- };
+ return { ...state, split: true };
+ }
+
+ case ActionTypes.ResetExplore: {
+ return initialExploreState;
}
}
diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/folders/CreateFolderCtrl.ts
similarity index 96%
rename from public/app/features/dashboard/create_folder_ctrl.ts
rename to public/app/features/folders/CreateFolderCtrl.ts
index 99b2e8d4853..db70c2a18a2 100644
--- a/public/app/features/dashboard/create_folder_ctrl.ts
+++ b/public/app/features/folders/CreateFolderCtrl.ts
@@ -1,7 +1,7 @@
import appEvents from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util';
-export class CreateFolderCtrl {
+export default class CreateFolderCtrl {
title = '';
navModel: any;
titleTouched = false;
@@ -38,3 +38,4 @@ export class CreateFolderCtrl {
});
}
}
+
diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/folders/FolderDashboardsCtrl.ts
similarity index 84%
rename from public/app/features/dashboard/folder_dashboards_ctrl.ts
rename to public/app/features/folders/FolderDashboardsCtrl.ts
index 05cc420c489..95ff355937b 100644
--- a/public/app/features/dashboard/folder_dashboards_ctrl.ts
+++ b/public/app/features/folders/FolderDashboardsCtrl.ts
@@ -1,7 +1,7 @@
-import { FolderPageLoader } from './folder_page_loader';
+import { FolderPageLoader } from './services/FolderPageLoader';
import locationUtil from 'app/core/utils/location_util';
-export class FolderDashboardsCtrl {
+export default class FolderDashboardsCtrl {
navModel: any;
folderId: number;
uid: string;
@@ -23,3 +23,4 @@ export class FolderDashboardsCtrl {
}
}
}
+
diff --git a/public/app/features/dashboard/partials/create_folder.html b/public/app/features/folders/partials/create_folder.html
similarity index 100%
rename from public/app/features/dashboard/partials/create_folder.html
rename to public/app/features/folders/partials/create_folder.html
diff --git a/public/app/features/dashboard/partials/folder_dashboards.html b/public/app/features/folders/partials/folder_dashboards.html
similarity index 100%
rename from public/app/features/dashboard/partials/folder_dashboards.html
rename to public/app/features/folders/partials/folder_dashboards.html
diff --git a/public/app/features/dashboard/folder_page_loader.ts b/public/app/features/folders/services/FolderPageLoader.ts
similarity index 100%
rename from public/app/features/dashboard/folder_page_loader.ts
rename to public/app/features/folders/services/FolderPageLoader.ts
diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts b/public/app/features/manage-dashboards/DashboardImportCtrl.test.ts
similarity index 95%
rename from public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts
rename to public/app/features/manage-dashboards/DashboardImportCtrl.test.ts
index bcde009cb3a..c9037c0a62d 100644
--- a/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts
+++ b/public/app/features/manage-dashboards/DashboardImportCtrl.test.ts
@@ -1,5 +1,5 @@
-import { DashboardImportCtrl } from '../dashboard_import_ctrl';
-import config from '../../../core/config';
+import { DashboardImportCtrl } from './DashboardImportCtrl';
+import config from 'app/core/config';
describe('DashboardImportCtrl', () => {
const ctx: any = {};
diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/manage-dashboards/DashboardImportCtrl.ts
similarity index 99%
rename from public/app/features/dashboard/dashboard_import_ctrl.ts
rename to public/app/features/manage-dashboards/DashboardImportCtrl.ts
index 455fa682edd..d2c6584d13d 100644
--- a/public/app/features/dashboard/dashboard_import_ctrl.ts
+++ b/public/app/features/manage-dashboards/DashboardImportCtrl.ts
@@ -232,3 +232,5 @@ export class DashboardImportCtrl {
this.gnetInfo = '';
}
}
+
+export default DashboardImportCtrl;
diff --git a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts b/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts
similarity index 93%
rename from public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts
rename to public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts
index 075583b971b..c183f38d92a 100644
--- a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts
+++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts
@@ -46,7 +46,7 @@ export class MoveToFolderCtrl {
export function moveToFolderModal() {
return {
restrict: 'E',
- templateUrl: 'public/app/features/dashboard/move_to_folder_modal/move_to_folder.html',
+ templateUrl: 'public/app/features/manage-dashboards/components/MoveToFolderModal/template.html',
controller: MoveToFolderCtrl,
bindToController: true,
controllerAs: 'ctrl',
diff --git a/public/app/features/manage-dashboards/components/MoveToFolderModal/index.ts b/public/app/features/manage-dashboards/components/MoveToFolderModal/index.ts
new file mode 100644
index 00000000000..df0553aedb9
--- /dev/null
+++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/index.ts
@@ -0,0 +1 @@
+export { MoveToFolderCtrl } from './MoveToFolderCtrl';
diff --git a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html
similarity index 100%
rename from public/app/features/dashboard/move_to_folder_modal/move_to_folder.html
rename to public/app/features/manage-dashboards/components/MoveToFolderModal/template.html
diff --git a/public/app/features/manage-dashboards/components/UploadDashboard/index.ts b/public/app/features/manage-dashboards/components/UploadDashboard/index.ts
new file mode 100644
index 00000000000..828b4f76982
--- /dev/null
+++ b/public/app/features/manage-dashboards/components/UploadDashboard/index.ts
@@ -0,0 +1 @@
+export { uploadDashboardDirective } from './uploadDashboardDirective';
diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts
similarity index 96%
rename from public/app/features/dashboard/upload.ts
rename to public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts
index ec4ad9a03cb..0c38a1247f1 100644
--- a/public/app/features/dashboard/upload.ts
+++ b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts
@@ -11,7 +11,7 @@ const template = `
`;
/** @ngInject */
-function uploadDashboardDirective(timer, $location) {
+export function uploadDashboardDirective(timer, $location) {
return {
restrict: 'E',
template: template,
diff --git a/public/app/features/manage-dashboards/index.ts b/public/app/features/manage-dashboards/index.ts
index 046740904e1..9d7c2bbc811 100644
--- a/public/app/features/manage-dashboards/index.ts
+++ b/public/app/features/manage-dashboards/index.ts
@@ -1,7 +1,15 @@
-import coreModule from 'app/core/core_module';
+// Services
+export { ValidationSrv } from './services/ValidationSrv';
+// Components
+export * from './components/MoveToFolderModal';
+export * from './components/UploadDashboard';
+
+// Controllers
import { DashboardListCtrl } from './DashboardListCtrl';
import { SnapshotListCtrl } from './SnapshotListCtrl';
+import coreModule from 'app/core/core_module';
+
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
coreModule.controller('SnapshotListCtrl', SnapshotListCtrl);
diff --git a/public/app/features/dashboard/partials/dashboard_import.html b/public/app/features/manage-dashboards/partials/dashboard_import.html
similarity index 100%
rename from public/app/features/dashboard/partials/dashboard_import.html
rename to public/app/features/manage-dashboards/partials/dashboard_import.html
diff --git a/public/app/features/dashboard/validation_srv.ts b/public/app/features/manage-dashboards/services/ValidationSrv.ts
similarity index 100%
rename from public/app/features/dashboard/validation_srv.ts
rename to public/app/features/manage-dashboards/services/ValidationSrv.ts
diff --git a/public/app/features/panel/all.ts b/public/app/features/panel/all.ts
index d461b491897..8bc2822a77c 100644
--- a/public/app/features/panel/all.ts
+++ b/public/app/features/panel/all.ts
@@ -4,3 +4,5 @@ import './solo_panel_ctrl';
import './query_ctrl';
import './panel_editor_tab';
import './query_editor_row';
+import './repeat_option';
+import './panellinks/module';
diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts
index f68423315d7..2f1ef72cedd 100644
--- a/public/app/features/panel/panel_ctrl.ts
+++ b/public/app/features/panel/panel_ctrl.ts
@@ -290,17 +290,4 @@ export class PanelCtrl {
html += ' ';
return sanitize(html);
}
-
- openInspector() {
- const modalScope = this.$scope.$new();
- modalScope.panel = this.panel;
- modalScope.dashboard = this.dashboard;
- modalScope.panelInfoHtml = this.getInfoContent({ mode: 'inspector' });
-
- modalScope.inspector = $.extend(true, {}, this.inspector);
- this.publishAppEvent('show-modal', {
- src: 'public/app/features/dashboard/partials/inspector.html',
- scope: modalScope,
- });
- }
}
diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts
index f503aa4386d..1fd0b129720 100644
--- a/public/app/features/panel/panel_directive.ts
+++ b/public/app/features/panel/panel_directive.ts
@@ -192,11 +192,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
scope.$watchGroup(['ctrl.error', 'ctrl.panel.description'], updatePanelCornerInfo);
scope.$watchCollection('ctrl.panel.links', updatePanelCornerInfo);
- cornerInfoElem.on('click', () => {
- infoDrop.close();
- scope.$apply(ctrl.openInspector.bind(ctrl));
- });
-
elem.on('mouseenter', mouseEnter);
elem.on('mouseleave', mouseLeave);
diff --git a/public/app/features/dashboard/panellinks/link_srv.ts b/public/app/features/panel/panellinks/link_srv.ts
similarity index 100%
rename from public/app/features/dashboard/panellinks/link_srv.ts
rename to public/app/features/panel/panellinks/link_srv.ts
diff --git a/public/app/features/dashboard/panellinks/module.html b/public/app/features/panel/panellinks/module.html
similarity index 100%
rename from public/app/features/dashboard/panellinks/module.html
rename to public/app/features/panel/panellinks/module.html
diff --git a/public/app/features/dashboard/panellinks/module.ts b/public/app/features/panel/panellinks/module.ts
similarity index 100%
rename from public/app/features/dashboard/panellinks/module.ts
rename to public/app/features/panel/panellinks/module.ts
diff --git a/public/app/features/dashboard/panellinks/specs/link_srv.test.ts b/public/app/features/panel/panellinks/specs/link_srv.test.ts
similarity index 100%
rename from public/app/features/dashboard/panellinks/specs/link_srv.test.ts
rename to public/app/features/panel/panellinks/specs/link_srv.test.ts
diff --git a/public/app/features/dashboard/repeat_option/repeat_option.ts b/public/app/features/panel/repeat_option.ts
similarity index 100%
rename from public/app/features/dashboard/repeat_option/repeat_option.ts
rename to public/app/features/panel/repeat_option.ts
diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts
index 9d3b635a1e5..0a80ce0cdf0 100644
--- a/public/app/features/playlist/playlist_srv.ts
+++ b/public/app/features/playlist/playlist_srv.ts
@@ -4,12 +4,13 @@ import appEvents from 'app/core/app_events';
import _ from 'lodash';
import { toUrlParams } from 'app/core/utils/url';
-class PlaylistSrv {
+export class PlaylistSrv {
private cancelPromise: any;
- private dashboards: any;
+ private dashboards: Array<{ uri: string }>;
private index: number;
- private interval: any;
+ private interval: number;
private startUrl: string;
+ private numberOfLoops = 0;
isPlaying: boolean;
/** @ngInject */
@@ -20,8 +21,15 @@ class PlaylistSrv {
const playedAllDashboards = this.index > this.dashboards.length - 1;
if (playedAllDashboards) {
- window.location.href = this.startUrl;
- return;
+ this.numberOfLoops++;
+
+ // This does full reload of the playlist to keep memory in check due to existing leaks but at the same time
+ // we do not want page to flicker after each full loop.
+ if (this.numberOfLoops >= 3) {
+ window.location.href = this.startUrl;
+ return;
+ }
+ this.index = 0;
}
const dash = this.dashboards[this.index];
@@ -46,8 +54,8 @@ class PlaylistSrv {
this.index = 0;
this.isPlaying = true;
- this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
- this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
+ return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
+ return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
this.dashboards = dashboards;
this.interval = kbn.interval_to_ms(playlist.interval);
this.next();
diff --git a/public/app/features/playlist/specs/playlist_srv.test.ts b/public/app/features/playlist/specs/playlist_srv.test.ts
new file mode 100644
index 00000000000..e6b7671c964
--- /dev/null
+++ b/public/app/features/playlist/specs/playlist_srv.test.ts
@@ -0,0 +1,103 @@
+import { PlaylistSrv } from '../playlist_srv';
+
+const dashboards = [{ uri: 'dash1' }, { uri: 'dash2' }];
+
+const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance }] => {
+ const mockBackendSrv = {
+ get: jest.fn(url => {
+ switch (url) {
+ case '/api/playlists/1':
+ return Promise.resolve({ interval: '1s' });
+ case '/api/playlists/1/dashboards':
+ return Promise.resolve(dashboards);
+ default:
+ throw new Error(`Unexpected url=${url}`);
+ }
+ }),
+ };
+
+ const mockLocation = {
+ url: jest.fn(),
+ search: () => ({}),
+ };
+
+ const mockTimeout = jest.fn();
+ (mockTimeout as any).cancel = jest.fn();
+
+ return [new PlaylistSrv(mockLocation, mockTimeout, mockBackendSrv), mockLocation];
+};
+
+const mockWindowLocation = (): [jest.MockInstance, () => void] => {
+ const oldLocation = window.location;
+ const hrefMock = jest.fn();
+
+ // JSDom defines window in a way that you cannot tamper with location so this seems to be the only way to change it.
+ // https://github.com/facebook/jest/issues/5124#issuecomment-446659510
+ delete window.location;
+ window.location = {} as any;
+
+ // Only mocking href as that is all this test needs, but otherwise there is lots of things missing, so keep that
+ // in mind if this is reused.
+ Object.defineProperty(window.location, 'href', {
+ set: hrefMock,
+ get: hrefMock,
+ });
+ const unmock = () => {
+ window.location = oldLocation;
+ };
+ return [hrefMock, unmock];
+};
+
+describe('PlaylistSrv', () => {
+ let srv: PlaylistSrv;
+ let mockLocationService: { url: jest.MockInstance };
+ let hrefMock: jest.MockInstance;
+ let unmockLocation: () => void;
+ const initialUrl = 'http://localhost/playlist';
+
+ beforeEach(() => {
+ [srv, mockLocationService] = createPlaylistSrv();
+ [hrefMock, unmockLocation] = mockWindowLocation();
+
+ // This will be cached in the srv when start() is called
+ hrefMock.mockReturnValue(initialUrl);
+ });
+
+ afterEach(() => {
+ unmockLocation();
+ });
+
+ it('runs all dashboards in cycle and reloads page after 3 cycles', async () => {
+ await srv.start(1);
+
+ for (let i = 0; i < 6; i++) {
+ expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
+ srv.next();
+ }
+
+ expect(hrefMock).toHaveBeenCalledTimes(2);
+ expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
+ });
+
+ it('keeps the refresh counter value after restarting', async () => {
+ await srv.start(1);
+
+ // 1 complete loop
+ for (let i = 0; i < 3; i++) {
+ expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
+ srv.next();
+ }
+
+ srv.stop();
+ await srv.start(1);
+
+ // Another 2 loops
+ for (let i = 0; i < 4; i++) {
+ expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
+ srv.next();
+ }
+
+ expect(hrefMock).toHaveBeenCalledTimes(3);
+ expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
+ });
+});
diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts
index 7805341d1a2..30faffea3be 100644
--- a/public/app/features/templating/specs/template_srv.test.ts
+++ b/public/app/features/templating/specs/template_srv.test.ts
@@ -275,6 +275,11 @@ describe('templateSrv', () => {
expect(result).toBe('test,test2');
});
+ it('multi value and percentencode format should render percent-encoded string', () => {
+ const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode');
+ expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D');
+ });
+
it('slash should be properly escaped in regex format', () => {
const result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).toBe('Gi3\\/14');
@@ -464,6 +469,11 @@ describe('templateSrv', () => {
name: 'empty_on_init',
current: { value: '', text: '' },
},
+ {
+ type: 'custom',
+ name: 'foo',
+ current: { value: 'constructor', text: 'constructor' },
+ }
]);
_templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m');
_templateSrv.updateTemplateData();
@@ -478,6 +488,12 @@ describe('templateSrv', () => {
const target = _templateSrv.replaceWithText('Hello $empty_on_init');
expect(target).toBe('Hello ');
});
+
+ it('should not return a string representation of a constructor property', () => {
+ const target = _templateSrv.replaceWithText('$foo');
+ expect(target).not.toBe('function Object() { [native code] }');
+ expect(target).toBe('constructor');
+ });
});
describe('built in interval variables', () => {
diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts
index 74da017bb93..2f8068137e5 100644
--- a/public/app/features/templating/template_srv.ts
+++ b/public/app/features/templating/template_srv.ts
@@ -77,6 +77,15 @@ export class TemplateSrv {
return '(' + quotedValues.join(' OR ') + ')';
}
+ // encode string according to RFC 3986; in contrast to encodeURIComponent()
+ // also the sub-delims "!", "'", "(", ")" and "*" are encoded;
+ // unicode handling uses UTF-8 as in ECMA-262.
+ encodeURIComponentStrict(str) {
+ return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
+ return '%' + c.charCodeAt(0).toString(16).toUpperCase();
+ });
+ }
+
formatValue(value, format, variable) {
// for some scopedVars there is no variable
variable = variable || {};
@@ -118,6 +127,13 @@ export class TemplateSrv {
}
return value;
}
+ case 'percentencode': {
+ // like glob, but url escaped
+ if (_.isArray(value)) {
+ return this.encodeURIComponentStrict('{' + value.join(',') + '}');
+ }
+ return this.encodeURIComponentStrict(value);
+ }
default: {
if (_.isArray(value)) {
return '{' + value.join(',') + '}';
@@ -238,7 +254,9 @@ export class TemplateSrv {
return match;
}
- return this.grafanaVariables[variable.current.value] || variable.current.text;
+ const value = this.grafanaVariables[variable.current.value];
+
+ return typeof(value) === 'string' ? value : variable.current.text;
});
}
diff --git a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
index 49f6b74e8b6..a7b865cde3f 100644
--- a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
+++ b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
@@ -26,7 +26,7 @@ export default (props: any) => (
Loki Cheat Sheet
{CHEAT_SHEET_ITEMS.map(item => (
-
+
{item.title}
{item.expression && (
({
- ...query,
+ refId: query.refId,
expr: '',
}));
}
diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts
index 1f86f20d6fd..9cd4ee0779b 100644
--- a/public/app/plugins/datasource/loki/result_transformer.ts
+++ b/public/app/plugins/datasource/loki/result_transformer.ts
@@ -5,7 +5,7 @@ import {
LogLevel,
LogsMetaItem,
LogsModel,
- LogRow,
+ LogRowModel,
LogsStream,
LogsStreamEntry,
LogsStreamLabels,
@@ -115,7 +115,7 @@ export function processEntry(
parsedLabels: LogsStreamLabels,
uniqueLabels: LogsStreamLabels,
search: string
-): LogRow {
+): LogRowModel {
const { line } = entry;
const ts = entry.ts || entry.timestamp;
// Assumes unique-ness, needs nanosec precision for timestamp
@@ -156,9 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LI
}));
// Merge stream entries into single list of log rows
- const sortedRows: LogRow[] = _.chain(streams)
+ const sortedRows: LogRowModel[] = _.chain(streams)
.reduce(
- (acc: LogRow[], stream: LogsStream) => [
+ (acc: LogRowModel[], stream: LogsStream) => [
...acc,
...stream.entries.map(entry =>
processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)
diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
index 6fd450394a3..f5b5b311b2a 100644
--- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
+++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
@@ -91,7 +91,6 @@ interface PromQueryFieldProps {
initialQuery: PromQuery;
hint?: any;
history?: any[];
- metricsByPrefix?: CascaderOption[];
onClickHintFix?: (action: any) => void;
onPressEnter?: () => void;
onQueryChange?: (value: PromQuery, override?: boolean) => void;
@@ -99,7 +98,6 @@ interface PromQueryFieldProps {
interface PromQueryFieldState {
metricsOptions: any[];
- metricsByPrefix: CascaderOption[];
syntaxLoaded: boolean;
}
@@ -124,7 +122,6 @@ class PromQueryField extends React.PureComponent
{
+ const { panel } = scope.ctrl;
+ return [
+ panel.content,
+ panel.mode
+ ].join();
+ };
+
$scope.$watch(
- 'ctrl.panel.content',
+ renderWhenChanged,
_.throttle(() => {
this.render();
- }, 1000)
+ }, 100)
);
}
@@ -62,7 +72,7 @@ export class TextPanelCtrl extends PanelCtrl {
this.renderingCompleted();
}
- renderText(content) {
+ renderText(content: string) {
content = content
.replace(/&/g, '&')
.replace(/>/g, '>')
@@ -71,7 +81,7 @@ export class TextPanelCtrl extends PanelCtrl {
this.updateContent(content);
}
- renderMarkdown(content) {
+ renderMarkdown(content: string) {
if (!this.remarkable) {
this.remarkable = new Remarkable();
}
@@ -81,7 +91,8 @@ export class TextPanelCtrl extends PanelCtrl {
});
}
- updateContent(html) {
+ updateContent(html: string) {
+ html = config.disableSanitizeHtml ? html : sanitize(html);
try {
this.content = this.$sce.trustAsHtml(this.templateSrv.replace(html, this.panel.scopedVars));
} catch (e) {
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts
index 8552d0510a9..e7381740435 100644
--- a/public/app/routes/routes.ts
+++ b/public/app/routes/routes.ts
@@ -10,6 +10,9 @@ import ApiKeys from 'app/features/api-keys/ApiKeysPage';
import PluginListPage from 'app/features/plugins/PluginListPage';
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
import FolderPermissions from 'app/features/folders/FolderPermissions';
+import CreateFolderCtrl from 'app/features/folders/CreateFolderCtrl';
+import FolderDashboardsCtrl from 'app/features/folders/FolderDashboardsCtrl';
+import DashboardImportCtrl from 'app/features/manage-dashboards/DashboardImportCtrl';
import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
import UsersListPage from 'app/features/users/UsersListPage';
@@ -66,8 +69,8 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
pageClass: 'page-dashboard',
})
.when('/dashboard/import', {
- templateUrl: 'public/app/features/dashboard/partials/dashboard_import.html',
- controller: 'DashboardImportCtrl',
+ templateUrl: 'public/app/features/manage-dashboards/partials/dashboard_import.html',
+ controller: DashboardImportCtrl,
controllerAs: 'ctrl',
})
.when('/datasources', {
@@ -100,8 +103,8 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl',
})
.when('/dashboards/folder/new', {
- templateUrl: 'public/app/features/dashboard/partials/create_folder.html',
- controller: 'CreateFolderCtrl',
+ templateUrl: 'public/app/features/folders/partials/create_folder.html',
+ controller: CreateFolderCtrl,
controllerAs: 'ctrl',
})
.when('/dashboards/f/:uid/:slug/permissions', {
@@ -117,8 +120,8 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
},
})
.when('/dashboards/f/:uid/:slug', {
- templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
- controller: 'FolderDashboardsCtrl',
+ templateUrl: 'public/app/features/folders/partials/folder_dashboards.html',
+ controller: FolderDashboardsCtrl,
controllerAs: 'ctrl',
})
.when('/dashboards/f/:uid', {
diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts
index 570a387cd74..dc9a478adf3 100644
--- a/public/app/store/configureStore.ts
+++ b/public/app/store/configureStore.ts
@@ -1,6 +1,6 @@
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
-import { createLogger } from 'redux-logger';
+// import { createLogger } from 'redux-logger';
import sharedReducers from 'app/core/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/state/reducers';
@@ -39,7 +39,7 @@ export function configureStore() {
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
- setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
+ setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
} else {
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
}
diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts
index c69e93ff88e..ce5ea1047dd 100644
--- a/public/app/types/explore.ts
+++ b/public/app/types/explore.ts
@@ -186,7 +186,7 @@ export interface ExploreItemState {
* Allows the selection to be discarded if something went wrong during the asynchronous
* loading of the datasource.
*/
- requestedDatasourceId?: number;
+ requestedDatasourceName?: string;
/**
* Time range for this Explore. Managed by the time picker and used by all query runs.
*/
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss
index 9e74b343b2d..739ccb6c412 100644
--- a/public/sass/_grafana.scss
+++ b/public/sass/_grafana.scss
@@ -39,6 +39,7 @@
@import 'layout/page';
// COMPONENTS
+@import '../app/features/dashboard/components/AddPanelWidget/AddPanelWidget';
@import 'components/scrollbar';
@import 'components/cards';
@import 'components/buttons';
@@ -58,7 +59,6 @@
@import 'components/panel_table';
@import 'components/panel_text';
@import 'components/panel_heatmap';
-@import 'components/panel_add_panel';
@import 'components/panel_logs';
@import 'components/settings_permissions';
@import 'components/tagsinput';
diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss
index 237574b93bc..44941540598 100644
--- a/public/sass/components/_sidemenu.scss
+++ b/public/sass/components/_sidemenu.scss
@@ -149,6 +149,19 @@
color: #ebedf2;
}
+.side-menu-header-link {
+ // Removes left-brand-border-gradient from link
+ color: #ebedf2 !important;
+ border: none !important;
+ padding: 0 !important;
+}
+
+.dropdown-menu--sidemenu > li > .side-menu-header-link:hover {
+ // Makes sure it looks good on light theme
+ color: #fff !important;
+ background-color: $side-menu-item-hover-bg !important;
+}
+
.sidemenu-subtitle {
padding: 0.5rem 1rem 0.5rem;
font-size: $font-size-sm;
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index abd13a10368..dce944dc7b2 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -1,182 +1,324 @@
-.explore {
- flex: 1 1 auto;
+.icon-margin-right {
+ margin-right: 0.25em;
+}
- &-container {
- padding: $dashboard-padding;
+.icon-margin-left {
+ margin-left: 0.25em;
+}
+
+.run-icon {
+ transform: rotate(90deg);
+}
+
+.timepicker {
+ display: flex;
+}
+
+.timepicker-rangestring {
+ margin-left: 0.5em;
+}
+
+.datasource-picker {
+ .ds-picker {
+ min-width: 200px;
+ max-width: 200px;
+ }
+}
+
+.sidemenu-open {
+ .explore-toolbar-header {
+ padding: 0;
+ margin-left: 0;
}
- &-wrapper {
- display: flex;
-
- > .explore-split {
- width: 50%;
+ .explore-toolbar-header-title {
+ .navbar-page-btn {
+ padding-left: 0;
}
}
+}
- // Push split button a bit
- .explore-first-button {
- margin-left: 15px;
+.explore-toolbar {
+ background: inherit;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: flex-start;
+ height: auto;
+ padding: 0px $dashboard-padding;
+ border-bottom: 1px solid #0000;
+ transition-duration: 0.35s;
+ transition-timing-function: ease-in-out;
+ transition-property: box-shadow, border-bottom;
+}
+
+.explore-toolbar-item {
+ position: relative;
+ align-self: center;
+}
+
+.explore-toolbar.splitted {
+ .explore-toolbar-item {
+ flex: 1 1 100%;
}
- .explore-panel {
- margin-top: $panel-margin;
+ .explore-toolbar-content-item:first-child {
+ padding-left: 0;
+ margin-right: auto;
}
+}
- .explore-panel__body {
- padding: $panel-padding;
- }
+.explore-toolbar-item:last-child {
+ flex: auto;
+}
- .explore-panel__header {
- padding: $panel-padding;
- padding-top: 5px;
- padding-bottom: 0;
- display: flex;
- cursor: pointer;
- margin-bottom: 5px;
- transition: all 0.1s linear;
- }
+.explore-toolbar-header {
+ display: flex;
+ flex: 1 1 0;
+ flex-flow: row nowrap;
+ font-size: 18px;
+ min-height: 55px;
+ line-height: 55px;
+ justify-content: space-between;
+ margin-left: $panel-margin * 3;
+}
- .explore-panel__header-label {
- font-weight: 500;
- margin-right: $panel-margin;
- font-size: $font-size-h6;
- box-shadow: $text-shadow-faint;
- }
+.explore-toolbar-header {
+ justify-content: space-between;
+ align-items: center;
+}
- .explore-panel__header-buttons {
- margin-right: $panel-margin;
- font-size: $font-size-lg;
- line-height: $font-size-h6;
- }
-
- // Make sure wrap buttons around on small screens
- .navbar {
- flex-wrap: wrap;
- height: auto;
- }
+.explore-toolbar-header-title {
+ color: darken($link-color, 5%);
.navbar-page-btn {
- margin-right: 1rem;
+ padding-left: $dashboard-padding;
+ }
- // Explore icon in header
- .fa {
- font-size: 100%;
- opacity: 0.75;
- margin-right: 0.5em;
+ .fa {
+ font-size: 100%;
+ opacity: 0.75;
+ margin-right: 0.5em;
+ }
+}
+
+.explore-toolbar-header-close {
+ margin-left: auto;
+}
+
+.explore-toolbar-content {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.explore-toolbar-content-item {
+ padding: 10px 2px;
+}
+
+.explore-toolbar-content-item:first-child {
+ padding-left: $dashboard-padding;
+ margin-right: auto;
+}
+
+@media only screen and (max-width: 1545px) {
+ .explore-toolbar.splitted {
+ .timepicker-rangestring {
+ display: none;
}
}
+}
- // Toggle mode
- .navbar-button.active {
- color: $btn-active-text-color;
- background-color: $btn-active-bg;
- }
-
- .navbar-button--no-icon {
- line-height: 18px;
- }
-
- .result-options {
- margin: 2 * $panel-margin 0;
- }
-
- .time-series-disclaimer {
- width: 300px;
- margin: $panel-margin auto;
- padding: 10px 0;
- border-radius: $border-radius;
- text-align: center;
- background-color: $panel-bg;
-
- .disclaimer-icon {
- color: $yellow;
- margin-right: $panel-margin/2;
- }
-
- .show-all-time-series {
- cursor: pointer;
- color: $external-link-color;
- }
- }
-
- .navbar .elapsed-time {
- position: absolute;
- left: 0;
- right: 0;
- top: 3.5rem;
- text-align: center;
- font-size: 0.8rem;
- }
-
- .graph-legend {
- flex-wrap: wrap;
- }
-
- .explore-panel__loader {
- height: 2px;
- position: relative;
- overflow: hidden;
- background: none;
- margin: $panel-margin / 2;
- transition: background-color 1s ease;
- }
-
- .explore-panel__loader--active {
- background: $text-color-faint;
- }
-
- .explore-panel__loader--active:after {
- content: ' ';
- display: block;
- width: 25%;
- top: 0;
- top: -50%;
- height: 250%;
- position: absolute;
- animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67);
- animation-iteration-count: 100;
- background: $blue;
- }
-
- @keyframes loader {
- from {
- left: -25%;
- }
- to {
- left: 100%;
- }
- }
-
- .datasource-picker {
- min-width: 200px;
- }
-
+@media only screen and (max-width: 1070px) {
.timepicker {
- display: flex;
-
- &-rangestring {
- margin-left: 0.5em;
+ .timepicker-rangestring {
+ display: none;
}
}
- .run-icon {
- margin-left: 0.25em;
- transform: rotate(90deg);
+ .explore-toolbar-content {
+ justify-content: flex-start;
}
- .relative {
- position: relative;
+ .explore-toolbar.splitted {
+ .explore-toolbar-content-item {
+ padding: 2px 0;
+ margin: 0;
+ }
}
- .link {
- text-decoration: underline;
+ .explore-toolbar-content-item {
+ padding: 2px 2px;
}
}
+@media only screen and (max-width: 803px) {
+ .sidemenu-open {
+ .explore-toolbar-header-title {
+ .navbar-page-btn {
+ padding-left: 0;
+ margin-left: 0;
+ }
+ }
+ }
+
+ .explore-toolbar-header-title {
+ .navbar-page-btn {
+ padding-left: 0;
+ margin-left: $dashboard-padding;
+ }
+ }
+
+ .btn-title {
+ display: none;
+ }
+}
+
+@media only screen and (max-width: 702px) {
+ .explore-toolbar-content-item:first-child {
+ padding-left: 2px;
+ margin-right: 0;
+ }
+}
+
+@media only screen and (max-width: 544px) {
+ .sidemenu-open {
+ .explore-toolbar-header-title {
+ .navbar-page-btn {
+ padding-left: 0;
+ margin-left: $dashboard-padding;
+ }
+ }
+ }
+
+ .explore-toolbar-header-title {
+ .navbar-page-btn {
+ padding-left: 0;
+ margin-left: $dashboard-padding;
+ }
+ }
+}
+
+.explore {
+ flex: 1 1 auto;
+}
+
.explore + .explore {
border-left: 1px dotted $table-border;
}
+.explore-container {
+ padding: $dashboard-padding;
+}
+
+.explore-wrapper {
+ display: flex;
+
+ > .explore-split {
+ width: 50%;
+ }
+}
+
+.explore-panel {
+ margin-top: $panel-margin;
+}
+
+.explore-panel__body {
+ padding: $panel-padding;
+}
+
+.explore-panel__header {
+ padding: $panel-padding;
+ padding-top: 5px;
+ padding-bottom: 0;
+ display: flex;
+ cursor: pointer;
+ margin-bottom: 5px;
+ transition: all 0.1s linear;
+}
+
+.explore-panel__header-label {
+ font-weight: 500;
+ margin-right: $panel-margin;
+ font-size: $font-size-h6;
+ box-shadow: $text-shadow-faint;
+}
+
+.explore-panel__header-buttons {
+ margin-right: $panel-margin;
+ font-size: $font-size-lg;
+ line-height: $font-size-h6;
+}
+
+.result-options {
+ margin: 2 * $panel-margin 0;
+}
+
+.time-series-disclaimer {
+ width: 300px;
+ margin: $panel-margin auto;
+ padding: 10px 0;
+ border-radius: $border-radius;
+ text-align: center;
+ background-color: $panel-bg;
+
+ .disclaimer-icon {
+ color: $yellow;
+ margin-right: $panel-margin/2;
+ }
+
+ .show-all-time-series {
+ cursor: pointer;
+ color: $external-link-color;
+ }
+}
+
+.navbar .elapsed-time {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 3.5rem;
+ text-align: center;
+ font-size: 0.8rem;
+}
+
+.graph-legend {
+ flex-wrap: wrap;
+}
+
+.explore-panel__loader {
+ height: 2px;
+ position: relative;
+ overflow: hidden;
+ background: none;
+ margin: $panel-margin / 2;
+}
+
+.explore-panel__loader--active:after {
+ content: ' ';
+ display: block;
+ width: 25%;
+ top: 0;
+ top: -50%;
+ height: 250%;
+ position: absolute;
+ animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms;
+ animation-iteration-count: 100;
+ left: -25%;
+ background: $blue;
+}
+
+@keyframes loader {
+ from {
+ left: -25%;
+ opacity: 0.1;
+ }
+ to {
+ left: 100%;
+ opacity: 1;
+ }
+}
+
.query-row {
display: flex;
position: relative;
diff --git a/public/vendor/flot/jquery.flot.pie.js b/public/vendor/flot/jquery.flot.pie.js
index 6553c8ea3a8..dee47e6e504 100644
--- a/public/vendor/flot/jquery.flot.pie.js
+++ b/public/vendor/flot/jquery.flot.pie.js
@@ -73,6 +73,7 @@ More detail and specific examples can be found in the included HTML file.
centerLeft = null,
centerTop = null,
processed = false,
+ options = null,
ctx = null;
// interactive variables
diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh
index dd69f8139c8..411e00a1646 100755
--- a/scripts/build/build-all.sh
+++ b/scripts/build/build-all.sh
@@ -32,10 +32,12 @@ echo "Build arguments: $OPT"
# build only amd64 for enterprise
if echo "$EXTRA_OPTS" | grep -vq enterprise ; then
+go run build.go -goarch armv6 -cc ${CCARMV7} ${OPT} build
go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build
go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build
go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build
fi
+
go run build.go -goos windows -cc ${CCWIN64} ${OPT} build
# Do not remove CC from the linux build, its there for compatibility with Centos6
@@ -67,6 +69,7 @@ rm tools/phantomjs/phantomjs
# build only amd64 for enterprise
if echo "$EXTRA_OPTS" | grep -vq enterprise ; then
+ go run build.go -goos linux -pkg-arch armv6 ${OPT} -skipRpm package-only
go run build.go -goos linux -pkg-arch armv7 ${OPT} package-only
go run build.go -goos linux -pkg-arch arm64 ${OPT} package-only
diff --git a/scripts/build/build.sh b/scripts/build/build.sh
index ac6aab0b867..5f42744082a 100755
--- a/scripts/build/build.sh
+++ b/scripts/build/build.sh
@@ -28,6 +28,7 @@ fi
echo "Build arguments: $OPT"
+go run build.go -goarch armv6 -cc ${CCARMV7} ${OPT} build
go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build
go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build
@@ -49,6 +50,7 @@ source /etc/profile.d/rvm.sh
echo "Packaging"
go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only
+go run build.go -goos linux -pkg-arch armv6 ${OPT} -skipRpm package-only
go run build.go -goos linux -pkg-arch armv7 ${OPT} package-only
go run build.go -goos linux -pkg-arch arm64 ${OPT} package-only
diff --git a/scripts/build/publish.go b/scripts/build/publish.go
deleted file mode 100644
index d5b19877724..00000000000
--- a/scripts/build/publish.go
+++ /dev/null
@@ -1,194 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/json"
- "flag"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "time"
-)
-
-var apiUrl = flag.String("apiUrl", "https://grafana.com/api", "api url")
-var apiKey = flag.String("apiKey", "", "api key")
-var version = ""
-var versionRe = regexp.MustCompile(`grafana-(.*)(\.|_)(arm64|armhfp|aarch64|armv7|darwin|linux|windows|x86_64)`)
-var debVersionRe = regexp.MustCompile(`grafana_(.*)_(arm64|armv7|armhf|amd64)\.deb`)
-var builds = []build{}
-var architectureMapping = map[string]string{
- "armv7": "armv7",
- "armhfp": "armv7",
- "armhf": "armv7",
- "arm64": "arm64",
- "aarch64": "arm64",
- "amd64": "amd64",
- "x86_64": "amd64",
-}
-
-func main() {
- flag.Parse()
- if *apiKey == "" {
- log.Fatalf("Require apiKey command line parameters")
- }
-
- err := filepath.Walk("dist", packageWalker)
- if err != nil {
- log.Fatalf("Cannot find any packages to publish, %v", err)
- }
-
- if version == "" {
- log.Fatalf("No version found")
- }
-
- if len(builds) == 0 {
- log.Fatalf("No builds found")
- }
-
- nightly := release{
- Version: version,
- ReleaseDate: time.Now(),
- Stable: false,
- Nightly: true,
- Beta: false,
- WhatsNewUrl: "",
- ReleaseNotesUrl: "",
- Builds: builds,
- }
-
- postRequest("/grafana/versions", nightly, fmt.Sprintf("Create Release %s", nightly.Version))
- postRequest("/grafana/versions/"+nightly.Version, nightly, fmt.Sprintf("Update Release %s", nightly.Version))
-
- for _, b := range nightly.Builds {
- postRequest(fmt.Sprintf("/grafana/versions/%s/packages", nightly.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch))
- postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", nightly.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch))
- }
-}
-
-func mapPackage(path string, name string, shaBytes []byte) (build, error) {
- log.Printf("Finding package file %s", name)
- result := versionRe.FindSubmatch([]byte(name))
- debResult := debVersionRe.FindSubmatch([]byte(name))
-
- if len(result) > 0 {
- version = string(result[1])
- log.Printf("Version detected: %v", version)
- } else if len(debResult) > 0 {
- version = string(debResult[1])
- } else {
- return build{}, fmt.Errorf("Unable to figure out version from '%v'", name)
- }
-
- os := ""
- if strings.Contains(name, "linux") {
- os = "linux"
- }
- if strings.HasSuffix(name, "windows-amd64.zip") {
- os = "win"
- }
- if strings.HasSuffix(name, "darwin-amd64.tar.gz") {
- os = "darwin"
- }
- if strings.HasSuffix(name, ".rpm") {
- os = "rhel"
- }
- if strings.HasSuffix(name, ".deb") {
- os = "deb"
- }
- if os == "" {
- return build{}, fmt.Errorf("Unable to figure out os from '%v'", name)
- }
-
- arch := ""
- for archListed, archReal := range architectureMapping {
- if strings.Contains(name, archListed) {
- arch = archReal
- break
- }
- }
- if arch == "" {
- return build{}, fmt.Errorf("Unable to figure out arch from '%v'", name)
- }
-
- return build{
- Os: os,
- Arch: arch,
- Url: "https://s3-us-west-2.amazonaws.com/grafana-releases/master/" + name,
- Sha256: string(shaBytes),
- }, nil
-}
-
-func packageWalker(path string, f os.FileInfo, err error) error {
- if err != nil {
- log.Printf("error: %v", err)
- }
- if f.Name() == "dist" || strings.Contains(f.Name(), "sha256") || strings.Contains(f.Name(), "latest") {
- return nil
- }
-
- shaBytes, err := ioutil.ReadFile(path + ".sha256")
- if err != nil {
- log.Fatalf("Failed to read sha256 file %v", err)
- }
-
- build, err := mapPackage(path, f.Name(), shaBytes)
- if err != nil {
- log.Printf("Could not map metadata from package: %v", err)
- return nil
- }
-
- builds = append(builds, build)
- return nil
-}
-
-func postRequest(url string, obj interface{}, desc string) {
- jsonBytes, _ := json.Marshal(obj)
- req, _ := http.NewRequest(http.MethodPost, (*apiUrl)+url, bytes.NewReader(jsonBytes))
- req.Header.Add("Authorization", "Bearer "+(*apiKey))
- req.Header.Add("Content-Type", "application/json")
-
- res, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Fatalf("error: %v", err)
- }
-
- if res.StatusCode == http.StatusOK {
- log.Printf("Action: %s \t OK", desc)
- } else {
-
- if res.Body != nil {
- defer res.Body.Close()
- body, _ := ioutil.ReadAll(res.Body)
- if strings.Contains(string(body), "already exists") || strings.Contains(string(body), "Nothing to update") {
- log.Printf("Action: %s \t Already exists", desc)
- } else {
- log.Printf("Action: %s \t Failed - Status: %v", desc, res.Status)
- log.Printf("Resp: %s", body)
- log.Fatalf("Quitting")
- }
- }
- }
-}
-
-type release struct {
- Version string `json:"version"`
- ReleaseDate time.Time `json:"releaseDate"`
- Stable bool `json:"stable"`
- Beta bool `json:"beta"`
- Nightly bool `json:"nightly"`
- WhatsNewUrl string `json:"whatsNewUrl"`
- ReleaseNotesUrl string `json:"releaseNotesUrl"`
- Builds []build `json:"-"`
-}
-
-type build struct {
- Os string `json:"os"`
- Url string `json:"url"`
- Sha256 string `json:"sha256"`
- Arch string `json:"arch"`
-}
diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go
index 992cba38f90..64e879cace4 100644
--- a/scripts/build/release_publisher/externalrelease.go
+++ b/scripts/build/release_publisher/externalrelease.go
@@ -14,10 +14,10 @@ type releaseFromExternalContent struct {
artifactConfigurations []buildArtifact
}
-func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) {
+func (re releaseFromExternalContent) prepareRelease(baseArchiveURL, whatsNewURL string, releaseNotesURL string, nightly bool) (*release, error) {
version := re.rawVersion[1:]
beta := strings.Contains(version, "beta")
- var rt ReleaseType
+ var rt releaseType
if beta {
rt = BETA
} else if nightly {
@@ -28,11 +28,11 @@ func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl
builds := []build{}
for _, ba := range re.artifactConfigurations {
- sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, rt)))
+ sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getURL(baseArchiveURL, version, rt)))
if err != nil {
return nil, err
}
- builds = append(builds, newBuild(baseArchiveUrl, ba, version, rt, sha256))
+ builds = append(builds, newBuild(baseArchiveURL, ba, version, rt, sha256))
}
r := release{
@@ -41,8 +41,8 @@ func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl
Stable: rt.stable(),
Beta: rt.beta(),
Nightly: rt.nightly(),
- WhatsNewUrl: whatsNewUrl,
- ReleaseNotesUrl: releaseNotesUrl,
+ WhatsNewURL: whatsNewURL,
+ ReleaseNotesURL: releaseNotesURL,
Builds: builds,
}
return &r, nil
@@ -52,9 +52,9 @@ type urlGetter interface {
getContents(url string) (string, error)
}
-type getHttpContents struct{}
+type getHTTPContents struct{}
-func (getHttpContents) getContents(url string) (string, error) {
+func (getHTTPContents) getContents(url string) (string, error) {
response, err := http.Get(url)
if err != nil {
return "", err
diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go
index 4f4575c4ff4..332654ee625 100644
--- a/scripts/build/release_publisher/localrelease.go
+++ b/scripts/build/release_publisher/localrelease.go
@@ -2,7 +2,6 @@ package main
import (
"fmt"
- "github.com/pkg/errors"
"io/ioutil"
"log"
"os"
@@ -10,6 +9,8 @@ import (
"regexp"
"strings"
"time"
+
+ "github.com/pkg/errors"
)
type releaseLocalSources struct {
@@ -17,11 +18,11 @@ type releaseLocalSources struct {
artifactConfigurations []buildArtifact
}
-func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) {
+func (r releaseLocalSources) prepareRelease(baseArchiveURL, whatsNewURL string, releaseNotesURL string, nightly bool) (*release, error) {
if !nightly {
- return nil, errors.New("Local releases only supported for nightly builds.")
+ return nil, errors.New("Local releases only supported for nightly builds")
}
- buildData := r.findBuilds(baseArchiveUrl)
+ buildData := r.findBuilds(baseArchiveURL)
rel := release{
Version: buildData.version,
@@ -29,8 +30,8 @@ func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string,
Stable: false,
Beta: false,
Nightly: nightly,
- WhatsNewUrl: whatsNewUrl,
- ReleaseNotesUrl: releaseNotesUrl,
+ WhatsNewURL: whatsNewURL,
+ ReleaseNotesURL: releaseNotesURL,
Builds: buildData.builds,
}
@@ -42,13 +43,13 @@ type buildData struct {
builds []build
}
-func (r releaseLocalSources) findBuilds(baseArchiveUrl string) buildData {
+func (r releaseLocalSources) findBuilds(baseArchiveURL string) buildData {
data := buildData{}
- filepath.Walk(r.path, createBuildWalker(r.path, &data, r.artifactConfigurations, baseArchiveUrl))
+ filepath.Walk(r.path, createBuildWalker(r.path, &data, r.artifactConfigurations, baseArchiveURL))
return data
}
-func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifact, baseArchiveUrl string) func(path string, f os.FileInfo, err error) error {
+func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifact, baseArchiveURL string) func(path string, f os.FileInfo, err error) error {
return func(path string, f os.FileInfo, err error) error {
if err != nil {
log.Printf("error: %v", err)
@@ -73,7 +74,7 @@ func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifac
data.version = version
data.builds = append(data.builds, build{
Os: archive.os,
- Url: archive.getUrl(baseArchiveUrl, version, NIGHTLY),
+ URL: archive.getURL(baseArchiveURL, version, NIGHTLY),
Sha256: string(shaBytes),
Arch: archive.arch,
})
@@ -90,5 +91,5 @@ func grabVersion(name string, suffix string) (string, error) {
return string(match[2]), nil
}
- return "", errors.New("No version found.")
+ return "", errors.New("No version found")
}
diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go
index 90acb2d3e62..6e1c8f782f0 100644
--- a/scripts/build/release_publisher/main.go
+++ b/scripts/build/release_publisher/main.go
@@ -9,8 +9,8 @@ import (
func main() {
var version string
- var whatsNewUrl string
- var releaseNotesUrl string
+ var whatsNewURL string
+ var releaseNotesURL string
var dryRun bool
var enterprise bool
var fromLocal bool
@@ -18,8 +18,8 @@ func main() {
var apiKey string
flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)")
- flag.StringVar(&whatsNewUrl, "wn", "", "What's new url (ex: --wn http://docs.grafana.org/guides/whats-new-in-v5-2/)")
- flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)")
+ flag.StringVar(&whatsNewURL, "wn", "", "What's new url (ex: --wn http://docs.grafana.org/guides/whats-new-in-v5-2/)")
+ flag.StringVar(&releaseNotesURL, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)")
flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)")
flag.BoolVar(&dryRun, "dry-run", false, "--dry-run")
flag.BoolVar(&enterprise, "enterprise", false, "--enterprise")
@@ -37,7 +37,7 @@ func main() {
if dryRun {
log.Println("Dry-run has been enabled.")
}
- var baseUrl string
+ var baseURL string
var builder releaseBuilder
var product string
@@ -46,7 +46,7 @@ func main() {
if enterprise {
product = "grafana-enterprise"
- baseUrl = createBaseUrl(archiveProviderRoot, "enterprise", product, nightly)
+ baseURL = createBaseURL(archiveProviderRoot, "enterprise", product, nightly)
var err error
buildArtifacts, err = filterBuildArtifacts([]artifactFilter{
{os: "deb", arch: "amd64"},
@@ -61,7 +61,7 @@ func main() {
} else {
product = "grafana"
- baseUrl = createBaseUrl(archiveProviderRoot, "oss", product, nightly)
+ baseURL = createBaseURL(archiveProviderRoot, "oss", product, nightly)
}
if fromLocal {
@@ -72,7 +72,7 @@ func main() {
}
} else {
builder = releaseFromExternalContent{
- getter: getHttpContents{},
+ getter: getHTTPContents{},
rawVersion: version,
artifactConfigurations: buildArtifacts,
}
@@ -80,18 +80,18 @@ func main() {
p := publisher{
apiKey: apiKey,
- apiUri: "https://grafana.com/api",
+ apiURI: "https://grafana.com/api",
product: product,
dryRun: dryRun,
enterprise: enterprise,
- baseArchiveUrl: baseUrl,
+ baseArchiveURL: baseURL,
builder: builder,
}
- if err := p.doRelease(whatsNewUrl, releaseNotesUrl, nightly); err != nil {
+ if err := p.doRelease(whatsNewURL, releaseNotesURL, nightly); err != nil {
log.Fatalf("error: %v", err)
}
}
-func createBaseUrl(root string, bucketName string, product string, nightly bool) string {
+func createBaseURL(root string, bucketName string, product string, nightly bool) string {
var subPath string
if nightly {
subPath = "master"
diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go
index 1d93c1e306e..8fd139c2638 100644
--- a/scripts/build/release_publisher/publisher.go
+++ b/scripts/build/release_publisher/publisher.go
@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
- "github.com/pkg/errors"
"io/ioutil"
"log"
"net/http"
@@ -14,20 +13,20 @@ import (
type publisher struct {
apiKey string
- apiUri string
+ apiURI string
product string
dryRun bool
enterprise bool
- baseArchiveUrl string
+ baseArchiveURL string
builder releaseBuilder
}
type releaseBuilder interface {
- prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error)
+ prepareRelease(baseArchiveURL, whatsNewURL string, releaseNotesURL string, nightly bool) (*release, error)
}
-func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string, nightly bool) error {
- currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl, nightly)
+func (p *publisher) doRelease(whatsNewURL string, releaseNotesURL string, nightly bool) error {
+ currentRelease, err := p.builder.prepareRelease(p.baseArchiveURL, whatsNewURL, releaseNotesURL, nightly)
if err != nil {
return err
}
@@ -62,23 +61,26 @@ func (p *publisher) postRelease(r *release) error {
return nil
}
-type ReleaseType int
+type releaseType int
const (
- STABLE ReleaseType = iota + 1
+ // STABLE is a release type constant
+ STABLE releaseType = iota + 1
+ // BETA is a release type constant
BETA
+ // NIGHTLY is a release type constant
NIGHTLY
)
-func (rt ReleaseType) beta() bool {
+func (rt releaseType) beta() bool {
return rt == BETA
}
-func (rt ReleaseType) stable() bool {
+func (rt releaseType) stable() bool {
return rt == STABLE
}
-func (rt ReleaseType) nightly() bool {
+func (rt releaseType) nightly() bool {
return rt == NIGHTLY
}
@@ -88,7 +90,7 @@ type buildArtifact struct {
urlPostfix string
}
-func (t buildArtifact) getUrl(baseArchiveUrl, version string, releaseType ReleaseType) string {
+func (t buildArtifact) getURL(baseArchiveURL, version string, releaseType releaseType) string {
prefix := "-"
rhelReleaseExtra := ""
@@ -100,7 +102,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, releaseType Releas
rhelReleaseExtra = "-1"
}
- url := strings.Join([]string{baseArchiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
+ url := strings.Join([]string{baseArchiveURL, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
return url
}
@@ -125,11 +127,21 @@ var completeBuildArtifactConfigurations = []buildArtifact{
arch: "armv7",
urlPostfix: "_armhf.deb",
},
+ {
+ os: "deb",
+ arch: "armv6",
+ urlPostfix: "_armel.deb",
+ },
{
os: "rhel",
arch: "armv7",
urlPostfix: ".armhfp.rpm",
},
+ {
+ os: "linux",
+ arch: "armv6",
+ urlPostfix: ".linux-armv6.tar.gz",
+ },
{
os: "linux",
arch: "armv7",
@@ -181,23 +193,23 @@ func filterBuildArtifacts(filters []artifactFilter) ([]buildArtifact, error) {
}
if !matched {
- return nil, errors.New(fmt.Sprintf("No buildArtifact for os=%v, arch=%v", f.os, f.arch))
+ return nil, fmt.Errorf("No buildArtifact for os=%v, arch=%v", f.os, f.arch)
}
}
return artifacts, nil
}
-func newBuild(baseArchiveUrl string, ba buildArtifact, version string, rt ReleaseType, sha256 string) build {
+func newBuild(baseArchiveURL string, ba buildArtifact, version string, rt releaseType, sha256 string) build {
return build{
Os: ba.os,
- Url: ba.getUrl(baseArchiveUrl, version, rt),
+ URL: ba.getURL(baseArchiveURL, version, rt),
Sha256: sha256,
Arch: ba.arch,
}
}
-func (p *publisher) apiUrl(url string) string {
- return fmt.Sprintf("%s/%s%s", p.apiUri, p.product, url)
+func (p *publisher) apiURL(url string) string {
+ return fmt.Sprintf("%s/%s%s", p.apiURI, p.product, url)
}
func (p *publisher) postRequest(url string, obj interface{}, desc string) error {
@@ -207,12 +219,12 @@ func (p *publisher) postRequest(url string, obj interface{}, desc string) error
}
if p.dryRun {
- log.Println(fmt.Sprintf("POST to %s:", p.apiUrl(url)))
+ log.Println(fmt.Sprintf("POST to %s:", p.apiURL(url)))
log.Println(string(jsonBytes))
return nil
}
- req, err := http.NewRequest(http.MethodPost, p.apiUrl(url), bytes.NewReader(jsonBytes))
+ req, err := http.NewRequest(http.MethodPost, p.apiURL(url), bytes.NewReader(jsonBytes))
if err != nil {
return err
}
@@ -254,14 +266,14 @@ type release struct {
Stable bool `json:"stable"`
Beta bool `json:"beta"`
Nightly bool `json:"nightly"`
- WhatsNewUrl string `json:"whatsNewUrl"`
- ReleaseNotesUrl string `json:"releaseNotesUrl"`
+ WhatsNewURL string `json:"whatsNewUrl"`
+ ReleaseNotesURL string `json:"releaseNotesUrl"`
Builds []build `json:"-"`
}
type build struct {
Os string `json:"os"`
- Url string `json:"url"`
+ URL string `json:"url"`
Sha256 string `json:"sha256"`
Arch string `json:"arch"`
}
diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go
index 2aea55d5ee1..bd2bbdce60e 100644
--- a/scripts/build/release_publisher/publisher_test.go
+++ b/scripts/build/release_publisher/publisher_test.go
@@ -7,69 +7,69 @@ func TestPreparingReleaseFromRemote(t *testing.T) {
cases := []struct {
version string
expectedVersion string
- whatsNewUrl string
- relNotesUrl string
+ whatsNewURL string
+ relNotesURL string
nightly bool
expectedBeta bool
expectedStable bool
expectedArch string
expectedOs string
- expectedUrl string
- baseArchiveUrl string
+ expectedURL string
+ baseArchiveURL string
buildArtifacts []buildArtifact
}{
{
version: "v5.2.0-beta1",
expectedVersion: "5.2.0-beta1",
- whatsNewUrl: "https://whatsnews.foo/",
- relNotesUrl: "https://relnotes.foo/",
+ whatsNewURL: "https://whatsnews.foo/",
+ relNotesURL: "https://relnotes.foo/",
nightly: false,
expectedBeta: true,
expectedStable: false,
expectedArch: "amd64",
expectedOs: "linux",
- expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.0-beta1.linux-amd64.tar.gz",
- baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
+ expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.0-beta1.linux-amd64.tar.gz",
+ baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
buildArtifacts: []buildArtifact{{"linux", "amd64", ".linux-amd64.tar.gz"}},
},
{
version: "v5.2.3",
expectedVersion: "5.2.3",
- whatsNewUrl: "https://whatsnews.foo/",
- relNotesUrl: "https://relnotes.foo/",
+ whatsNewURL: "https://whatsnews.foo/",
+ relNotesURL: "https://relnotes.foo/",
nightly: false,
expectedBeta: false,
expectedStable: true,
expectedArch: "amd64",
expectedOs: "rhel",
- expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.3-1.x86_64.rpm",
- baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
+ expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.3-1.x86_64.rpm",
+ baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm"}},
},
{
version: "v5.4.0-pre1asdf",
expectedVersion: "5.4.0-pre1asdf",
- whatsNewUrl: "https://whatsnews.foo/",
- relNotesUrl: "https://relnotes.foo/",
+ whatsNewURL: "https://whatsnews.foo/",
+ relNotesURL: "https://relnotes.foo/",
nightly: true,
expectedBeta: false,
expectedStable: false,
expectedArch: "amd64",
expectedOs: "rhel",
- expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.x86_64.rpm",
- baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
+ expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.x86_64.rpm",
+ baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm"}},
},
}
for _, test := range cases {
builder := releaseFromExternalContent{
- getter: mockHttpGetter{},
+ getter: mockHTTPGetter{},
rawVersion: test.version,
artifactConfigurations: test.buildArtifacts,
}
- rel, _ := builder.prepareRelease(test.baseArchiveUrl, test.whatsNewUrl, test.relNotesUrl, test.nightly)
+ rel, _ := builder.prepareRelease(test.baseArchiveURL, test.whatsNewURL, test.relNotesURL, test.nightly)
if rel.Beta != test.expectedBeta || rel.Stable != test.expectedStable {
t.Errorf("%s should have been tagged as beta=%v, stable=%v.", test.version, test.expectedBeta, test.expectedStable)
@@ -93,21 +93,21 @@ func TestPreparingReleaseFromRemote(t *testing.T) {
t.Errorf("Expected os to be %v, but it was %v", test.expectedOs, build.Os)
}
- if build.Url != test.expectedUrl {
- t.Errorf("Expected url to be %v, but it was %v", test.expectedUrl, build.Url)
+ if build.URL != test.expectedURL {
+ t.Errorf("Expected url to be %v, but it was %v", test.expectedURL, build.URL)
}
}
}
-type mockHttpGetter struct{}
+type mockHTTPGetter struct{}
-func (mockHttpGetter) getContents(url string) (string, error) {
+func (mockHTTPGetter) getContents(url string) (string, error) {
return url, nil
}
func TestPreparingReleaseFromLocal(t *testing.T) {
- whatsNewUrl := "https://whatsnews.foo/"
- relNotesUrl := "https://relnotes.foo/"
+ whatsNewURL := "https://whatsnews.foo/"
+ relNotesURL := "https://relnotes.foo/"
expectedVersion := "5.4.0-123pre1"
expectedBuilds := 4
@@ -118,17 +118,17 @@ func TestPreparingReleaseFromLocal(t *testing.T) {
artifactConfigurations: completeBuildArtifactConfigurations,
}
- relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true)
+ relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewURL, relNotesURL, true)
if relAll.Stable || !relAll.Nightly {
t.Error("Expected a nightly release but wasn't.")
}
- if relAll.ReleaseNotesUrl != relNotesUrl {
- t.Errorf("expected releaseNotesUrl to be %s, but it was %s", relNotesUrl, relAll.ReleaseNotesUrl)
+ if relAll.ReleaseNotesURL != relNotesURL {
+ t.Errorf("expected releaseNotesURL to be %s, but it was %s", relNotesURL, relAll.ReleaseNotesURL)
}
- if relAll.WhatsNewUrl != whatsNewUrl {
- t.Errorf("expected whatsNewUrl to be %s, but it was %s", whatsNewUrl, relAll.WhatsNewUrl)
+ if relAll.WhatsNewURL != whatsNewURL {
+ t.Errorf("expected whatsNewURL to be %s, but it was %s", whatsNewURL, relAll.WhatsNewURL)
}
if relAll.Beta {
@@ -155,7 +155,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) {
}},
}
- relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true)
+ relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewURL, relNotesURL, true)
if len(relOne.Builds) != 1 {
t.Errorf("Expected 1 artifact, but was %v", len(relOne.Builds))
diff --git a/style_guides/frontend.md b/style_guides/frontend.md
new file mode 100644
index 00000000000..8d0849506a3
--- /dev/null
+++ b/style_guides/frontend.md
@@ -0,0 +1,62 @@
+# Frontend Style Guide
+
+Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react).
+
+## Table of Contents
+
+ 1. [Basic Rules](#basic-rules)
+ 1. [File & Component Organization](#Organization)
+ 1. [Naming](#naming)
+ 1. [Declaration](#declaration)
+ 1. [Props](#props)
+ 1. [Refs](#refs)
+ 1. [Methods](#methods)
+ 1. [Ordering](#ordering)
+
+## Basic rules
+
+* Try to keep files small and focused and break large components up into sub components.
+
+## Organization
+
+* Components and types that needs to be used by external plugins needs to go into @grafana/ui
+* Components should get their own folder under features/xxx/components
+ * Sub components can live in that component folders, so not small component needs their own folder
+ * Place test next to their component file (same dir)
+ * Mocks in __mocks__ dir
+ * Test utils in __tests__ dir
+ * Component sass should live in the same folder as component code
+* State logic & domain models should live in features/xxx/state
+* Containers (pages) can live in feature root features/xxx
+ * up for debate?
+
+## Props
+
+* Name callback props & handlers with a "on" prefix.
+
+```tsx
+// good
+onChange = () => {
+
+};
+
+render() {
+ return (
+
+ );
+}
+
+// bad
+handleChange = () => {
+
+};
+
+render() {
+ return (
+
+ );
+}
+```
+
+
+
diff --git a/yarn.lock b/yarn.lock
index 3cf75250712..c674f3e1709 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -50,11 +50,11 @@
source-map "^0.5.0"
"@babel/generator@^7.0.0", "@babel/generator@^7.2.2":
- version "7.2.2"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.2.tgz#18c816c70962640eab42fe8cae5f3947a5c65ccc"
- integrity sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg==
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.0.tgz#f663838cd7b542366de3aa608a657b8ccb2a99eb"
+ integrity sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==
dependencies:
- "@babel/types" "^7.2.2"
+ "@babel/types" "^7.3.0"
jsesc "^2.5.1"
lodash "^4.17.10"
source-map "^0.5.0"
@@ -75,12 +75,12 @@
"@babel/helper-explode-assignable-expression" "^7.1.0"
"@babel/types" "^7.0.0"
-"@babel/helper-builder-react-jsx@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0.tgz#fa154cb53eb918cf2a9a7ce928e29eb649c5acdb"
- integrity sha512-ebJ2JM6NAKW0fQEqN8hOLxK84RbRz9OkUhGS/Xd5u56ejMfVbayJ4+LykERZCOUM6faa6Fp3SZNX3fcT16MKHw==
+"@babel/helper-builder-react-jsx@^7.3.0":
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz#a1ac95a5d2b3e88ae5e54846bf462eeb81b318a4"
+ integrity sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==
dependencies:
- "@babel/types" "^7.0.0"
+ "@babel/types" "^7.3.0"
esutils "^2.0.0"
"@babel/helper-call-delegate@^7.1.0":
@@ -92,10 +92,10 @@
"@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0"
-"@babel/helper-create-class-features-plugin@^7.2.3":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.2.3.tgz#f6e719abb90cb7f4a69591e35fd5eb89047c4a7c"
- integrity sha512-xO/3Gn+2C7/eOUeb0VRnSP1+yvWHNxlpAot1eMhtoKDCN7POsyQP5excuT5UsV5daHxMWBeIIOeI5cmB8vMRgQ==
+"@babel/helper-create-class-features-plugin@^7.3.0":
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.0.tgz#2b01a81b3adc2b1287f9ee193688ef8dc71e718f"
+ integrity sha512-DUsQNS2CGLZZ7I3W3fvh0YpPDd6BuWJlDl+qmZZpABZHza2ErE3LxtEzLJFHFC1ZwtlAXvHhbFYbtM5o5B0WBw==
dependencies:
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-member-expression-to-functions" "^7.0.0"
@@ -235,13 +235,13 @@
"@babel/types" "^7.2.0"
"@babel/helpers@^7.1.0", "@babel/helpers@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.2.0.tgz#8335f3140f3144270dc63c4732a4f8b0a50b7a21"
- integrity sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A==
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9"
+ integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==
dependencies:
"@babel/template" "^7.1.2"
"@babel/traverse" "^7.1.5"
- "@babel/types" "^7.2.0"
+ "@babel/types" "^7.3.0"
"@babel/highlight@^7.0.0":
version "7.0.0"
@@ -253,9 +253,9 @@
js-tokens "^4.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.1.3", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489"
- integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.1.tgz#8f4ffd45f779e6132780835ffa7a215fa0b2d181"
+ integrity sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==
"@babel/plugin-proposal-async-generator-functions@^7.1.0", "@babel/plugin-proposal-async-generator-functions@^7.2.0":
version "7.2.0"
@@ -279,11 +279,11 @@
"@babel/plugin-syntax-class-properties" "^7.0.0"
"@babel/plugin-proposal-class-properties@^7.2.0":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.2.3.tgz#c9e1294363b346cff333007a92080f3203698461"
- integrity sha512-FVuQngLoN2iDrpW7LmhPZ2sO4DJxf35FOcwidwB9Ru9tMvI5URthnkVHuG14IStV+TzkMTyLMoOUlSTtrdVwqw==
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd"
+ integrity sha512-wNHxLkEKTQ2ay0tnsam2z7fGZUi+05ziDJflEt3AZTP3oXLKHJp9HqhfroB/vdMvt3sda9fAbq7FsG8QPDrZBg==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.2.3"
+ "@babel/helper-create-class-features-plugin" "^7.3.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-decorators@7.1.2":
@@ -312,10 +312,10 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.0.0"
-"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz#88f5fec3e7ad019014c97f7ee3c992f0adbf7fb8"
- integrity sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg==
+"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.3.1":
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz#f69fb6a1ea6a4e1c503994a91d9cf76f3c4b36e8"
+ integrity sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
@@ -589,6 +589,13 @@
"@babel/helper-module-transforms" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0"
+"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0":
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50"
+ integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw==
+ dependencies:
+ regexp-tree "^0.1.0"
+
"@babel/plugin-transform-new-target@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a"
@@ -660,11 +667,11 @@
"@babel/plugin-syntax-jsx" "^7.2.0"
"@babel/plugin-transform-react-jsx@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.2.0.tgz#ca36b6561c4d3b45524f8efb6f0fbc9a0d1d622f"
- integrity sha512-h/fZRel5wAfCqcKgq3OhbmYaReo7KkoJBpt8XnvpS7wqaNMqtw5xhxutzcm35iMUWucfAdT/nvGTsWln0JTg2Q==
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz#f2cab99026631c767e2745a5368b331cfe8f5290"
+ integrity sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==
dependencies:
- "@babel/helper-builder-react-jsx" "^7.0.0"
+ "@babel/helper-builder-react-jsx" "^7.3.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-jsx" "^7.2.0"
@@ -795,18 +802,19 @@
semver "^5.3.0"
"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.1.6", "@babel/preset-env@^7.2.0":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.2.3.tgz#948c8df4d4609c99c7e0130169f052ea6a7a8933"
- integrity sha512-AuHzW7a9rbv5WXmvGaPX7wADxFkZIqKlbBh1dmZUQp4iwiPpkE/Qnrji6SC4UQCQzvWY/cpHET29eUhXS9cLPw==
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db"
+ integrity sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
"@babel/plugin-proposal-json-strings" "^7.2.0"
- "@babel/plugin-proposal-object-rest-spread" "^7.2.0"
+ "@babel/plugin-proposal-object-rest-spread" "^7.3.1"
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.2.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
"@babel/plugin-transform-arrow-functions" "^7.2.0"
@@ -826,6 +834,7 @@
"@babel/plugin-transform-modules-commonjs" "^7.2.0"
"@babel/plugin-transform-modules-systemjs" "^7.2.0"
"@babel/plugin-transform-modules-umd" "^7.2.0"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0"
"@babel/plugin-transform-new-target" "^7.0.0"
"@babel/plugin-transform-object-super" "^7.2.0"
"@babel/plugin-transform-parameters" "^7.2.0"
@@ -876,9 +885,9 @@
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f"
- integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a"
+ integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==
dependencies:
regenerator-runtime "^0.12.0"
@@ -906,10 +915,10 @@
globals "^11.1.0"
lodash "^4.17.10"
-"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2":
- version "7.2.2"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e"
- integrity sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg==
+"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0":
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.0.tgz#61dc0b336a93badc02bf5f69c4cd8e1353f2ffc0"
+ integrity sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==
dependencies:
esutils "^2.0.2"
lodash "^4.17.10"
@@ -1074,16 +1083,16 @@
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@storybook/addon-actions@^4.1.7":
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-4.1.7.tgz#7c2625ea45b0c51d907d97d24d8dc56a701967b9"
- integrity sha512-xhZCkehXZl3FM/2Dw1YOla0KcXyDgPVJbv6fRt0ebbmzaVbeB+cdknZ10SImPbKUWLCdBEKKIAR/CNF29hAH5A==
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-4.1.11.tgz#8946ea78f050ae2d06a2f2231ec56d1831942e15"
+ integrity sha512-iVsxEPmOCuPMAaJhHbpxQhzEPzKnZad4GELNfKrwmmvv3mY+3UN/z208HguW4NHjhMJZVYSS3H/qic8CQS+pHw==
dependencies:
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
"@emotion/styled" "^0.10.6"
- "@storybook/addons" "4.1.7"
- "@storybook/components" "4.1.7"
- "@storybook/core-events" "4.1.7"
+ "@storybook/addons" "4.1.11"
+ "@storybook/components" "4.1.11"
+ "@storybook/core-events" "4.1.11"
core-js "^2.5.7"
deep-equal "^1.0.1"
global "^4.3.2"
@@ -1094,13 +1103,13 @@
uuid "^3.3.2"
"@storybook/addon-info@^4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/addon-info/-/addon-info-4.1.6.tgz#48479487cd13d674807b77dbd42500cd770c954f"
- integrity sha512-pZeBVFO7sRISeHQuliLy7FE5Y++MxeumJLJKkSSndcX73X2LOxe3zip5CYbL7O5Eb6m8hBqRlRKpifP2Dp4H5Q==
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-info/-/addon-info-4.1.11.tgz#b2ea3a4fb4cad208f9d6075737b5bfe8636e28f9"
+ integrity sha512-eROXuXS5YgLeXsnkqjXqbZ8UFgNIwORDkn4UfD+Aej1//SWpGeNihOxQvx+pvs0NnsTR+/w4c1gbqa/Gr3f78w==
dependencies:
- "@storybook/addons" "4.1.6"
- "@storybook/client-logger" "4.1.6"
- "@storybook/components" "4.1.6"
+ "@storybook/addons" "4.1.11"
+ "@storybook/client-logger" "4.1.11"
+ "@storybook/components" "4.1.11"
core-js "^2.5.7"
global "^4.3.2"
marksy "^6.1.0"
@@ -1111,14 +1120,14 @@
util-deprecate "^1.0.2"
"@storybook/addon-knobs@^4.1.7":
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-4.1.7.tgz#298a1bd17f177e4653b073c9bca70c073746daea"
- integrity sha512-h2YSyZvz+KQIggAKttdTZvRpsCVQUJtyY9mzVHEFDKTccAlC9WjZPD9UDSHYQZKm+0gHtFkxOidFTcjt/5r+8A==
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-4.1.11.tgz#fd6c90d62a5bf5f94899746a95b02f1ef127cd81"
+ integrity sha512-UQzYZoo0WKHKHSayaEBLvyZNqlqCOKahXzT2r+hS3t6wRnHJSfPtEHD0xTYMJSkA5t+bhlIOFJy0tribd0sdPQ==
dependencies:
"@emotion/styled" "^0.10.6"
- "@storybook/addons" "4.1.7"
- "@storybook/components" "4.1.7"
- "@storybook/core-events" "4.1.7"
+ "@storybook/addons" "4.1.11"
+ "@storybook/components" "4.1.11"
+ "@storybook/core-events" "4.1.11"
copy-to-clipboard "^3.0.8"
core-js "^2.5.7"
escape-html "^1.0.3"
@@ -1130,54 +1139,39 @@
react-lifecycles-compat "^3.0.4"
util-deprecate "^1.0.2"
-"@storybook/addons@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.1.6.tgz#458c4c6baf8b2acaffb9ced705a8db224bd874b6"
- integrity sha512-5dG0adChzNRbRLS/YD/5mEoWLTk3uaJpzbRSJVKe6HQKBPDXmuEMYYPiHI83o15YBJjGHx68+PkHBI08oRsuhQ==
+"@storybook/addons@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.1.11.tgz#a0d537bd10d123ecee6cb1f5f149b148ce250e57"
+ integrity sha512-n9oDs7GgJbiN5NYPkR3B3e5W0Tr6bIZvFfcJzgyP4dn50AUvS1IE1CEthezfn1L/nc2suw/8Oe30bOXOyTl/SQ==
dependencies:
- "@storybook/channels" "4.1.6"
- "@storybook/components" "4.1.6"
+ "@storybook/channels" "4.1.11"
+ "@storybook/components" "4.1.11"
global "^4.3.2"
util-deprecate "^1.0.2"
-"@storybook/addons@4.1.7":
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.1.7.tgz#59ca76fff6594ff196bc58135689d1597cb90a1b"
- integrity sha512-psKz/uMlImHkuUqdYaEq84Kyh3VIcSo00yHsnpWeq0xv3v6ONQKrFZ0bdmuZT30XQC0sYvXaP+YOKRq/hdgWaQ==
+"@storybook/channel-postmessage@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-4.1.11.tgz#3320a5f3e05652466eff1c53843205c262a92dfb"
+ integrity sha512-/9p4I5CZWVl6mszY5AR5XPRdQ88LUaAt4iyhdxMIaqNRiVo3Rq4ptMXiw35eCr+sLQuG2KO2SiPIwaA4/FgQuw==
dependencies:
- "@storybook/channels" "4.1.7"
- "@storybook/components" "4.1.7"
- global "^4.3.2"
- util-deprecate "^1.0.2"
-
-"@storybook/channel-postmessage@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-4.1.6.tgz#ff7af1c0ee1ab72ee8f10ec2e29cd413c39fc14d"
- integrity sha512-CUFcnzZE5y24AUZqArt9/95wLdk0phsrOzDU8Q/WNWpzYCzTaaNzd82DDG4AWWHd7awtbgGGueKDCoAXU99t5A==
- dependencies:
- "@storybook/channels" "4.1.6"
+ "@storybook/channels" "4.1.11"
global "^4.3.2"
json-stringify-safe "^5.0.1"
-"@storybook/channels@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-4.1.6.tgz#52b56f5c94f58442aba2d9c5919bb00ff33475c5"
- integrity sha512-8MqGYypdaPmZR7eORXdxtJijGOz5UMHXoMskVtodvKi26tmltFKX+okXFNh/teKe3+8s0QWkpzM95VI+Z+0qFA==
+"@storybook/channels@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-4.1.11.tgz#d161497fa3cd848cc9d518aa1c37052857e22e3c"
+ integrity sha512-zYusY8cno4keMozn2lDpBgyNSOueFh+hrPETioSB5Z8Kd3F5OjM7681vJC8QA67yOBEie2hHk0CVxRpuxziMwA==
-"@storybook/channels@4.1.7":
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-4.1.7.tgz#bf3412859cca8cd3d221afcd68de348645dbb329"
- integrity sha512-UVLrCcQ8f52PQYdWh97XcWBdqiXKBznePbd4L36//C6cIJkfwQ5nK9UDzzrCP/tlCnolVxnqRlorns4YeJcrSg==
+"@storybook/client-logger@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-4.1.11.tgz#2b1e34e892045199592fdb01656e5dcdcd1999d7"
+ integrity sha512-Xxy6sY7Zd405o28wUAhlpqY2FbSZsTrsN3g/uo4Mqo4XD2f0Z4wIv1GOuM5DI2KlHpHGI+36YPO2VFx5Bq+yiQ==
-"@storybook/client-logger@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-4.1.6.tgz#3409c20467abe4f98751db9a8d6769c21d0c80e5"
- integrity sha512-P65Pw3m2FRW7QDIU51zj3ANzo7twL7/9nQKoyMJKy/1rgqk1RMa/boHERPRhfATzqYPf5hh2G0PBTMKBEG4A8w==
-
-"@storybook/components@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/components/-/components-4.1.6.tgz#f4430c77fc3eceddcd912a6c5e461554539c4f4d"
- integrity sha512-kWIUiexzFurNwW8NaJEhlFWD1kohnvNlOxgph7oSoXo/yCBodkEYpIuNbznQnNSH2xIjqh1dvbniJNSJZyEbTQ==
+"@storybook/components@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/components/-/components-4.1.11.tgz#25458a4a4f2edd836b1e4b944cfcfcb4a3567036"
+ integrity sha512-KJA8Nr8MbXiibDLcndx1GRVmVDyBBL2Tbb1kfQfr58vDwz6qhYxempejY6W+voaEqohnFxrOtnnbqlCyf8peUQ==
dependencies:
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
@@ -1190,48 +1184,27 @@
react-textarea-autosize "^7.0.4"
render-fragment "^0.1.1"
-"@storybook/components@4.1.7":
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/@storybook/components/-/components-4.1.7.tgz#5e7f8247aebe0b79246c19f580e520cc59a39a1d"
- integrity sha512-PgEREFw58tlyzzGbA7q1WlXgOLk6Dd+qkHZYJ+4JlTAl6kPdjirfRshg0uyvTVy5jGbtDa5mhN7Fo1TtuYiyDw==
- dependencies:
- "@emotion/core" "^0.13.1"
- "@emotion/provider" "^0.11.2"
- "@emotion/styled" "^0.10.6"
- global "^4.3.2"
- lodash "^4.17.11"
- prop-types "^15.6.2"
- react-inspector "^2.3.0"
- react-split-pane "^0.1.84"
- react-textarea-autosize "^7.0.4"
- render-fragment "^0.1.1"
+"@storybook/core-events@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-4.1.11.tgz#78cfb2b4014ca27909421cdebfa9c96533929a5a"
+ integrity sha512-rVb76xFLJkTFcBHL1oTdJW8O2N7q+Cc6Mo7v9u3TnM4WuRk08/GyzzO7sRvEg3Mvo59AOLu1uqYovRMo4tZEnQ==
-"@storybook/core-events@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-4.1.6.tgz#6c147b171a04cee3ed66990ebdf45be45dc658ba"
- integrity sha512-07ki5+VuruWQv7B1ZBlsNYEVSC3dQwIZKjEFL4aKFO57ruaNijkZTF1QHkSGJapyBPa7+LLM2fXqnBkputoEZw==
-
-"@storybook/core-events@4.1.7":
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-4.1.7.tgz#15803967783231dcb96432ad45b360d5e95188df"
- integrity sha512-IpggH1Br51UAlLaAhNr6qY0IVT5mN1akfeVi1DpCj2ji7zqdfeDO3zmX6aWSjYVefg6OvNol71WKh+wjcYNvXg==
-
-"@storybook/core@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/core/-/core-4.1.6.tgz#818124a8d47c9432637e000e509b01d051b9603e"
- integrity sha512-0T4mDt3Wzyg8UIrF0kPvP5kA+S+fGhaZI/vWGUPvmxsYuVQHGhvna1ZiayIW8R9TdHx1g+uSYkUxb8wVVUmwQA==
+"@storybook/core@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/core/-/core-4.1.11.tgz#f91cf77d4750edeb92717f6b2a2a4258b0a06c64"
+ integrity sha512-iUrtFCav7xJicCLhp4zdqbhaOXRWXrx4wMPSs0keBD2G7NQtSg/TQMUdx2VYFBl5thIFT1jt5dAm66y0Q2OCTQ==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.2.0"
"@babel/preset-env" "^7.2.0"
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
"@emotion/styled" "^0.10.6"
- "@storybook/addons" "4.1.6"
- "@storybook/channel-postmessage" "4.1.6"
- "@storybook/client-logger" "4.1.6"
- "@storybook/core-events" "4.1.6"
- "@storybook/node-logger" "4.1.6"
- "@storybook/ui" "4.1.6"
+ "@storybook/addons" "4.1.11"
+ "@storybook/channel-postmessage" "4.1.11"
+ "@storybook/client-logger" "4.1.11"
+ "@storybook/core-events" "4.1.11"
+ "@storybook/node-logger" "4.1.11"
+ "@storybook/ui" "4.1.11"
airbnb-js-shims "^1 || ^2"
autoprefixer "^9.3.1"
babel-plugin-macros "^2.4.2"
@@ -1295,10 +1268,10 @@
"@storybook/react-simple-di" "^1.2.1"
babel-runtime "6.x.x"
-"@storybook/node-logger@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-4.1.6.tgz#005b6f388a1c3a498c50a9b9d8cc3c4351827d6a"
- integrity sha512-3mLcNp0eTjwQKHJ0vWpZLlayPOUaZJR/Umc6kWzdMn1K398/k7FU0fBK4FJ7VmnI0z1sYTlqaTqjqN0U3XaxjA==
+"@storybook/node-logger@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-4.1.11.tgz#8ea9779eb6260a02bf06c02eafbff5925b883f9f"
+ integrity sha512-rCXk1PUcakkV72oyTR+nOVDUGnkk1On8/sm9u3NtBEUuwsCtm4p+jh42Pp4jsTtWpG36AVABDtiN65VgCfS+9w==
dependencies:
chalk "^2.4.1"
core-js "^2.5.7"
@@ -1343,16 +1316,16 @@
babel-runtime "^6.5.0"
"@storybook/react@^4.1.4":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/react/-/react-4.1.6.tgz#e597ea2e4d6ce09b5843c287db9baaa97076731c"
- integrity sha512-JavzdoIrLQprLlt/0Bm0QMKMOzqweL7gjXKSl8W5p38hhDpyxRt989t0yfZnwF6K0iGWeU88mAb9OoRkn7o8tA==
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/react/-/react-4.1.11.tgz#fb3cda82fd3334a6653325ec281a2da284ac6895"
+ integrity sha512-NPNcfOlWmFBevza/+GXIK23h46HqdWKFmId19E0PtoWGoR5xOR56rnwagr2+yHngUb4AATUCzhzTwxfWGxSvBg==
dependencies:
"@babel/plugin-transform-react-constant-elements" "^7.2.0"
"@babel/preset-flow" "^7.0.0"
"@babel/preset-react" "^7.0.0"
"@emotion/styled" "^0.10.6"
- "@storybook/core" "4.1.6"
- "@storybook/node-logger" "4.1.6"
+ "@storybook/core" "4.1.11"
+ "@storybook/node-logger" "4.1.11"
"@svgr/webpack" "^4.0.3"
babel-plugin-named-asset-import "^0.2.3"
babel-plugin-react-docgen "^2.0.0"
@@ -1368,16 +1341,16 @@
semver "^5.6.0"
webpack "^4.23.1"
-"@storybook/ui@4.1.6":
- version "4.1.6"
- resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-4.1.6.tgz#f8516dc03c2b5e4c57352c5fc6b26eaf9fd25488"
- integrity sha512-MXod0JMu/P2sTYlN6DM6yuwvizUKjwFn4lOf+F9hNe4lxZEXw74KogCv0CN32VUrWUP++ZY9DybQr4SMRFVVww==
+"@storybook/ui@4.1.11":
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-4.1.11.tgz#0c6fc34a8096028ef236a5196e7b91831702f2fb"
+ integrity sha512-bgIagh2Z4flGA7jv4JN++ThLwGq8CI8Wq+1/vhCiTxjTE3H1j9VNPdJfhNgxrQypcLRVHl5AKhf0mlSMyz0S1A==
dependencies:
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
"@emotion/styled" "^0.10.6"
- "@storybook/components" "4.1.6"
- "@storybook/core-events" "4.1.6"
+ "@storybook/components" "4.1.11"
+ "@storybook/core-events" "4.1.11"
"@storybook/mantra-core" "^1.7.2"
"@storybook/podda" "^1.2.3"
"@storybook/react-komposer" "^2.0.5"
@@ -1654,9 +1627,9 @@
integrity sha512-WQ6Ivy7VuUlZ/Grqc8493ZxC+y/fpvZLy5+8ELvmCr2hll8eJPUqC05l6fgRRA7kjqlpbH7lbmvY6pRKf6yzxw==
"@types/d3-shape@*":
- version "1.2.7"
- resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.7.tgz#c029081205f60caa9c98ef1d52c621e36a8d7160"
- integrity sha512-b2jpGcddOseeNxchaR1SNLqA5xZAbgKix3cXiFeuGeYIEAEUu91UbtelCxOHIUTbNURFnjcbkf4plRbejNzVaQ==
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.8.tgz#7750d34b8f817d543d62eaeb8d38b77886211b5c"
+ integrity sha512-eHAi4Nuw1/69hjBFNXNWYifcNTFhwy360PI969ssMX22Si9henYiNKLeJoBhNfyXFajeFjI1HGsYzyCWKOozdA==
dependencies:
"@types/d3-path" "*"
@@ -1740,24 +1713,24 @@
"@types/react" "*"
"@types/geojson@*":
- version "7946.0.4"
- resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.4.tgz#4e049756383c3f055dd8f3d24e63fb543e98eb07"
- integrity sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==
+ version "7946.0.5"
+ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.5.tgz#9aea839ea5af4b1bc079f1d9fa977d48665e02b0"
+ integrity sha512-rLlMXpd3rdlrp0+xsrda/hFfOpIxgqFcRpk005UKbHtcdFK+QXAjhBAPnvO58qF4O1LdDXrcaiJxMgstCIlcaw==
"@types/jest@^23.3.2":
- version "23.3.12"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.12.tgz#7e0ced251fa94c3bc2d1023d4b84b2992fa06376"
- integrity sha512-/kQvbVzdEpOq4tEWT79yAHSM4nH4xMlhJv2GrLVQt4Qmo8yYsPdioBM1QpN/2GX1wkfMnyXvdoftvLUr0LBj7Q==
+ version "23.3.13"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.13.tgz#c81484b6f4ca007bb09887ed15ecb3286d58f928"
+ integrity sha512-ePl4l+7dLLmCucIwgQHAgjiepY++qcI6nb8eAwGNkB6OxmTe3Z9rQU3rSpomqu42PCCnlThZbOoxsf+qylJsLA==
"@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", "@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/lodash@^4.14.119":
+ version "4.14.120"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.120.tgz#cf265d06f6c7a710db087ed07523ab8c1a24047b"
+ integrity sha512-jQ21kQ120mo+IrDs1nFNVm/AsdFxIx2+vZ347DbogHJPd/JzKNMOqU6HCYin1W6v8l5R9XSO2/e9cxmn7HAnVw==
"@types/node@*", "@types/node@^10.12.18":
version "10.12.18"
@@ -1823,7 +1796,7 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@^16.7.6":
+"@types/react@*", "@types/react@16.7.6", "@types/react@^16.7.6":
version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA==
@@ -2270,6 +2243,11 @@ acorn-dynamic-import@^3.0.0:
dependencies:
acorn "^5.0.0"
+acorn-dynamic-import@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
+ integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
+
acorn-es7-plugin@^1.0.12:
version "1.1.7"
resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b"
@@ -2305,7 +2283,7 @@ acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.2:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-acorn@^6.0.1:
+acorn@^6.0.1, acorn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.5.tgz#81730c0815f3f3b34d8efa95cb7430965f4d887a"
integrity sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==
@@ -2378,9 +2356,9 @@ ajv-keywords@^1.0.0:
integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw=
ajv-keywords@^3.1.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
- integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.3.0.tgz#cb6499da9b83177af8bc1732b2f0a1a1a3aacf8c"
+ integrity sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==
ajv@^4.7.0:
version "4.11.8"
@@ -2481,9 +2459,9 @@ ansi-escapes@^1.0.0, ansi-escapes@^1.1.0:
integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
ansi-escapes@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
- integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
ansi-html@0.0.7:
version "0.0.7"
@@ -2880,15 +2858,15 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0:
postcss-value-parser "^3.2.3"
autoprefixer@^9.3.1:
- version "9.4.5"
- resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.5.tgz#a13ccb001e4bc8837f71c3354005b42f02cc03d7"
- integrity sha512-M602C0ZxzFpJKqD4V6eq2j+K5CkzlhekCrcQupJmAOrPEZjWJyj/wSeo6qRSNoN6M3/9mtLPQqTTrABfReytQg==
+ version "9.4.6"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.6.tgz#0ace275e33b37de16b09a5547dbfe73a98c1d446"
+ integrity sha512-Yp51mevbOEdxDUy5WjiKtpQaecqYq9OqZSL04rSoCiry7Tc5I9FEyo3bfxiTJc1DfHeKwSFCUYbBAiOQ2VGfiw==
dependencies:
- browserslist "^4.4.0"
- caniuse-lite "^1.0.30000928"
+ browserslist "^4.4.1"
+ caniuse-lite "^1.0.30000929"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
- postcss "^7.0.11"
+ postcss "^7.0.13"
postcss-value-parser "^3.3.1"
awesome-typescript-loader@^5.2.1:
@@ -4258,13 +4236,13 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
-browserslist@^4.1.0, browserslist@^4.3.4, browserslist@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.0.tgz#7050d1412cbfc5274aba609ed5e50359ca1a5fdf"
- integrity sha512-tQkHS8VVxWbrjnNDXgt7/+SuPJ7qDvD0Y2e6bLtoQluR2SPvlmPUcfcU75L1KAalhqULlIFJlJ6BDfnYyJxJsw==
+browserslist@^4.1.0, browserslist@^4.3.4, browserslist@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062"
+ integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==
dependencies:
- caniuse-lite "^1.0.30000928"
- electron-to-chromium "^1.3.100"
+ caniuse-lite "^1.0.30000929"
+ electron-to-chromium "^1.3.103"
node-releases "^1.1.3"
bs-logger@0.x:
@@ -4530,15 +4508,15 @@ caniuse-api@^1.5.2:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
+caniuse-db@1.0.30000772, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000772"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
integrity sha1-UarokXaChureSj2DGep21qAbUSs=
-caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000928:
- version "1.0.30000928"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000928.tgz#805e828dc72b06498e3683a32e61c7507fd67b88"
- integrity sha512-aSpMWRXL6ZXNnzm8hgE4QDLibG5pVJ2Ujzsuj3icazlIkxXkPXtL+BWnMx6FBkWmkZgBHGUxPZQvrbRw2ZTxhg==
+caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000929:
+ version "1.0.30000932"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz#d01763e9ce77810962ca7391ff827b5949ce4272"
+ integrity sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A==
capture-exit@^1.2.0:
version "1.2.0"
@@ -4553,9 +4531,9 @@ capture-stack-trace@^1.0.0:
integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
case-sensitive-paths-webpack-plugin@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.2.tgz#c899b52175763689224571dad778742e133f0192"
- integrity sha512-oEZgAFfEvKtjSRCu6VgYkuGxwrWXMnQzyBmlLPP7r6PWQVtHxP5Z5N6XsuJvtoVax78am/r7lr46bwo3IVEBOg==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz#3371ef6365ef9c25fa4b81c16ace0e9c7dc58c3e"
+ integrity sha512-u5ElzokS8A1pm9vM3/iDgTcI3xqHxuCao94Oz8etI3cf0Tio0p8izkDYbTIn09uP3yUUr6+veaE6IkjnTYS46g==
caseless@~0.12.0:
version "0.12.0"
@@ -5300,9 +5278,9 @@ core-js@^1.0.0:
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.0.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.5.7:
- version "2.6.2"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944"
- integrity sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49"
+ integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@@ -5586,6 +5564,11 @@ cssesc@^0.1.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=
+cssfilter@0.0.10:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
+ integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=
+
cssnano@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
@@ -5652,9 +5635,9 @@ cssstyle@^1.0.0:
cssom "0.3.x"
csstype@^2.2.0, csstype@^2.5.2:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.0.tgz#6cf7b2fa7fc32aab3d746802c244d4eda71371a2"
- integrity sha512-by8hi8BlLbowQq0qtkx54d9aN73R9oUW20HISpka5kmgsR9F7nnxgfsemuR2sdCKZh+CDNf5egW9UZMm4mgJRg==
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.1.tgz#4cfbf637a577497036ebcd7e32647ef19a0b8076"
+ integrity sha512-wv7IRqCGsL7WGKB8gPvrl+++HlFM9kxAM6jL1EXNPNTshEJYilMkbfS2SnuHha77uosp/YVK0wAp2jmlBzn1tg==
currently-unhandled@^0.4.1:
version "0.4.1"
@@ -5889,9 +5872,9 @@ d3-scale@1.0.7:
d3-time-format "2"
d3-selection@1, d3-selection@^1.1.0:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.2.tgz#6e70a9df60801c8af28ac24d10072d82cbfdf652"
- integrity sha512-OoXdv1nZ7h2aKMVg3kaUFbLLK5jXUFAMLD/Tu5JA96mjf8f2a9ZUESGY+C36t8R1WFeWk/e55hy54Ml2I62CRQ==
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.0.tgz#ab9ac1e664cf967ebf1b479cc07e28ce9908c474"
+ integrity sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==
d3-selection@1.3.0:
version "1.3.0"
@@ -5940,9 +5923,9 @@ d3-timer@1.0.7:
integrity sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA==
d3-transition@1:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.3.tgz#3a435b05ce9cef9524fe0d38121cfb6905331ca6"
- integrity sha512-tEvo3qOXL6pZ1EzcXxFcPNxC/Ygivu5NoBY6mbzidATAeML86da+JfVIUzon3dNM6UX6zjDx+xbYDmMVtTSjuA==
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.2.0.tgz#f538c0e21b2aa1f05f3e965f8567e81284b3b2b8"
+ integrity sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==
dependencies:
d3-color "1"
d3-dispatch "1"
@@ -6094,7 +6077,7 @@ debug@^4.1.0:
dependencies:
ms "^2.1.1"
-debuglog@^1.0.1:
+debuglog@*, debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -6355,9 +6338,9 @@ dir-glob@2.0.0:
path-type "^3.0.0"
dir-glob@^2.0.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.1.tgz#ce8413234ffe8452b76b7741c32f116cf2a7b1a7"
- integrity sha512-UN6X6XwRjllabfRhBdkVSo63uurJ8nSvMGrwl94EYVz6g+exhTV+yVSYk5VC/xl3MBFBTtC0J20uFKce4Brrng==
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
+ integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
dependencies:
path-type "^3.0.0"
@@ -6520,25 +6503,31 @@ dot-prop@^4.1.0:
dependencies:
is-obj "^1.0.0"
-dotenv-expand@^4.0.1, dotenv-expand@^4.2.0:
+dotenv-defaults@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz#441cf5f067653fca4bbdce9dd3b803f6f84c585d"
+ integrity sha512-iXFvHtXl/hZPiFj++1hBg4lbKwGM+t/GlvELDnRtOFdjXyWP7mubkVr+eZGWG62kdsbulXAef6v/j6kiWc/xGA==
+ dependencies:
+ dotenv "^6.2.0"
+
+dotenv-expand@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.2.0.tgz#def1f1ca5d6059d24a766e587942c21106ce1275"
integrity sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=
dotenv-webpack@^1.5.7:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.6.0.tgz#ea5758ce4da1e0c3574ef777a32ee20beb61b3a5"
- integrity sha512-jTbHXmcVw3KMVhTdgthYNLWWHRGtucrADpZWwVCdiP+pCvuWvxLcUadwEnmz8Wqv/d2UAJxJhp1jrxGlMYCetg==
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.7.0.tgz#4384d8c57ee6f405c296278c14a9f9167856d3a1"
+ integrity sha512-wwNtOBW/6gLQSkb8p43y0Wts970A3xtNiG/mpwj9MLUhtPCQG6i+/DSXXoNN7fbPCU/vQ7JjwGmgOeGZSSZnsw==
dependencies:
- dotenv "^5.0.1"
- dotenv-expand "^4.0.1"
+ dotenv-defaults "^1.0.2"
dotenv@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
-dotenv@^6.0.0:
+dotenv@^6.0.0, dotenv@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
@@ -6576,7 +6565,7 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
-editions@^2.0.2, editions@^2.1.2:
+editions@^2.1.2, editions@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/editions/-/editions-2.1.3.tgz#727ccf3ec2c7b12dcc652c71000f16c4824d6f7d"
integrity sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==
@@ -6599,10 +6588,10 @@ ejs@^2.5.7, ejs@^2.5.9, ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==
-electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.100, electron-to-chromium@^1.3.62:
- version "1.3.103"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.103.tgz#a695777efdbc419cad6cbb0e58458251302cd52f"
- integrity sha512-tObPqGmY9X8MUM8i3MEimYmbnLLf05/QV5gPlkR8MQ3Uj8G8B2govE1U4cQcBYtv3ymck9Y8cIOu4waoiykMZQ==
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.62:
+ version "1.3.108"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.108.tgz#2e79a6fcaa4b3e7c75abf871505bda8e268c910e"
+ integrity sha512-/QI4hMpAh48a1Sea6PALGv+kuVne9A2EWGd8HrWHMdYhIzGtbhVVHh6heL5fAzGaDnZuPyrlWJRl8WPm4RyiQQ==
elegant-spinner@^1.0.1:
version "1.0.1"
@@ -6702,25 +6691,26 @@ envinfo@^5.7.0:
integrity sha512-pwdo0/G3CIkQ0y6PCXq4RdkvId2elvtPCJMG0konqlrfkWQbf1DWeH9K2b/cvu2YgGvPPTOnonZxXM1gikFu1w==
enzyme-adapter-react-16@^1.5.0:
- version "1.7.1"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.7.1.tgz#c37c4cb0fd75e88a063154a7a88096474914496a"
- integrity sha512-OQXKgfHWyHN3sFu2nKj3mhgRcqIPIJX6aOzq5AHVFES4R9Dw/vCBZFMPyaG81g2AZ5DogVh39P3MMNUbqNLTcw==
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.8.0.tgz#7055d8e908d8d27b807cf4292244db3c815ca11d"
+ integrity sha512-7cVHIKutqnesGeM3CjNFHSvktpypSWBokrBO8wIW+BVx+HGxWCF87W9TpkIIYJqgCtdw9FQGFrAbLg8kSwPRuQ==
dependencies:
- enzyme-adapter-utils "^1.9.0"
+ enzyme-adapter-utils "^1.10.0"
function.prototype.name "^1.1.0"
object.assign "^4.1.0"
- object.values "^1.0.4"
+ object.values "^1.1.0"
prop-types "^15.6.2"
- react-is "^16.6.1"
+ react-is "^16.7.0"
react-test-renderer "^16.0.0-0"
-enzyme-adapter-utils@^1.9.0:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.9.1.tgz#68196fdaf2a9f51f31603cbae874618661233d72"
- integrity sha512-LWc88BbKztLXlpRf5Ba/pSMJRaNezAwZBvis3N/IuB65ltZEh2E2obWU9B36pAbw7rORYeBUuqc79OL17ZzN1A==
+enzyme-adapter-utils@^1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.0.tgz#5836169f68b9e8733cb5b69cad5da2a49e34f550"
+ integrity sha512-VnIXJDYVTzKGbdW+lgK8MQmYHJquTQZiGzu/AseCZ7eHtOMAj4Rtvk8ZRopodkfPves0EXaHkXBDkVhPa3t0jA==
dependencies:
function.prototype.name "^1.1.0"
object.assign "^4.1.0"
+ object.fromentries "^2.0.0"
prop-types "^15.6.2"
semver "^5.6.0"
@@ -6812,9 +6802,9 @@ es-to-primitive@^1.2.0:
is-symbol "^1.0.2"
es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
- version "0.10.46"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572"
- integrity sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==
+ version "0.10.47"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.47.tgz#d24232e1380daad5449a817be19bde9729024a11"
+ integrity sha512-/1TItLfj+TTfWoeRcDn/0FbGV6SNo4R+On2GGVucPU/j3BWnXE2Co8h8CTo4Tu34gFJtnmwS9xiScKs4EjZhdw==
dependencies:
es6-iterator "~2.0.3"
es6-symbol "~3.1.1"
@@ -7064,10 +7054,10 @@ eventemitter3@^3.0.0, eventemitter3@^3.1.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
-events@^1.0.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
- integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
+events@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"
+ integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==
eventsource@0.1.6:
version "0.1.6"
@@ -7614,9 +7604,9 @@ flatten@^1.0.2:
integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
flow-parser@^0.*:
- version "0.90.0"
- resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.90.0.tgz#27f2f563dc4296bf0c555183bbf48cb6135b1b21"
- integrity sha512-a6Ohgdzvf2e1/F8sI98qcPLtDIjLayRkRgAwrWHzHFMHCNq92jyRbRG0w5fGjs6xdI320Ud39HkI0Dk5OPs17g==
+ version "0.91.0"
+ resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.91.0.tgz#bc42c5bf9d12e3121dad6667b1dcdab09049aee6"
+ integrity sha512-qvKoEaVmyZhTsRb2Qyp6hxewy7EEm1ahEb/fgrnB0rB6VgBvyLOuKoxpUpGgX1scd7oubShZqi0q56B97pfyOw==
flush-write-stream@^1.0.0:
version "1.0.3"
@@ -7807,9 +7797,9 @@ fs.realpath@^1.0.0:
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
fsevents@^1.2.2, fsevents@^1.2.3:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
- integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4"
+ integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==
dependencies:
nan "^2.9.2"
node-pre-gyp "^0.10.0"
@@ -8650,11 +8640,11 @@ hoist-non-react-statics@^2.5.0:
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.1.0:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz#c09c0555c84b38a7ede6912b61efddafd6e75e1e"
- integrity sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
+ integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==
dependencies:
- react-is "^16.3.2"
+ react-is "^16.7.0"
home-or-tmp@^2.0.0:
version "2.0.0"
@@ -9001,7 +8991,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
-imurmurhash@^0.1.4:
+imurmurhash@*, imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -9780,12 +9770,12 @@ istanbul-reports@^1.5.1:
handlebars "^4.0.3"
istextorbinary@^2.2.1:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.3.0.tgz#29458d7b10edcb52f4db9c57945bb67cd20cc4fd"
- integrity sha512-xs+IFjzw1/5n45nMYUh2ipLWGarmE0bDVR85WAiYUXzawc8NYn1WW0qaq2rSEFIR3NoNkaAvOr3FVMojFz5uUg==
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.5.1.tgz#14a33824cf6b9d5d7743eac1be2bd2c310d0ccbd"
+ integrity sha512-pv/JNPWnfpwGjPx7JrtWTwsWsxkrK3fNzcEVnt92YKEIErps4Fsk49+qzCe9iQF2hjqK8Naqf8P9kzoeCuQI1g==
dependencies:
binaryextensions "^2.1.2"
- editions "^2.0.2"
+ editions "^2.1.3"
textextensions "^2.4.0"
isurl@^1.0.0-alpha5:
@@ -10139,9 +10129,9 @@ jquery@^3.2.1:
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
js-base64@^2.1.8, js-base64@^2.1.9:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e"
- integrity sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
+ integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==
js-levenshtein@^1.1.3:
version "1.1.6"
@@ -10773,6 +10763,11 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
+lodash._baseindexof@*:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
+ integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
+
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -10781,12 +10776,29 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
+lodash._bindcallback@*:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
+ integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
+
+lodash._cacheindexof@*:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
+ integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
+
+lodash._createcache@*:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
+ integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
+ dependencies:
+ lodash._getnative "^3.0.0"
+
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
-lodash._getnative@^3.0.0:
+lodash._getnative@*, lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
@@ -10880,6 +10892,11 @@ lodash.mergewith@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
+lodash.restparam@*:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
+ integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
+
lodash.some@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
@@ -11541,9 +11558,9 @@ mocha@^4.0.1:
supports-color "4.4.0"
moment@^2.22.2:
- version "2.23.0"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225"
- integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+ integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
moo@^0.4.3:
version "0.4.3"
@@ -11770,9 +11787,9 @@ no-case@^2.2.0, no-case@^2.3.2:
lower-case "^1.1.1"
node-abi@^2.2.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.5.1.tgz#bb17288fc3b2f68fea0ed9897c66979fd754ed47"
- integrity sha512-oDbFc7vCFx0RWWCweTer3hFm1u+e60N5FtGnmRV6QqvgATGFH/XRR6vqWIeBVosCYCqt6YdIr2L0exLZuEdVcQ==
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.6.0.tgz#3adc69b28b334a0556fbadca378c7d18b8c82397"
+ integrity sha512-kCnEh6af6Z6DB7RFI/7LHNwqRjvJW7rgrv3lhIFoQ/+XhLPI/lJYwsk5vzvkldPWWgqnAMcuPF5S8/jj56kVOA==
dependencies:
semver "^5.4.1"
@@ -11839,9 +11856,9 @@ node-int64@^0.4.0:
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
node-libs-browser@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
- integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77"
+ integrity sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==
dependencies:
assert "^1.1.1"
browserify-zlib "^0.2.0"
@@ -11850,7 +11867,7 @@ node-libs-browser@^2.0.0:
constants-browserify "^1.0.0"
crypto-browserify "^3.11.0"
domain-browser "^1.1.1"
- events "^1.0.0"
+ events "^3.0.0"
https-browserify "^1.0.0"
os-browserify "^0.3.0"
path-browserify "0.0.0"
@@ -11864,7 +11881,7 @@ node-libs-browser@^2.0.0:
timers-browserify "^2.0.4"
tty-browserify "0.0.0"
url "^0.11.0"
- util "^0.10.3"
+ util "^0.11.0"
vm-browserify "0.0.4"
node-notifier@^5.2.1:
@@ -11894,9 +11911,9 @@ node-pre-gyp@^0.10.0:
tar "^4"
node-releases@^1.0.0-alpha.11, node-releases@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.3.tgz#aad9ce0dcb98129c753f772c0aa01360fb90fbd2"
- integrity sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ==
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.5.tgz#1dbee1380742125fe99e0476c456670bf3590b89"
+ integrity sha512-6C2K0x1QlYTz9wCueMN/DVZFcBVg/qsj2k9iV5gV/+OvG4KNrl7Nu7TWbWFQ3/Z2V10qVFQWtj5Xa+VBodcI6g==
dependencies:
semver "^5.3.0"
@@ -12378,6 +12395,16 @@ object.fromentries@^1.0.0:
function-bind "^1.1.1"
has "^1.0.1"
+object.fromentries@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab"
+ integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.11.0"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+
object.getownpropertydescriptors@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
@@ -12401,7 +12428,7 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.0.4:
+object.values@^1.0.4, object.values@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
@@ -12766,15 +12793,16 @@ param-case@2.1.x, param-case@^2.1.0:
no-case "^2.2.0"
parse-asn1@^5.0.0:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
- integrity sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.3.tgz#1600c6cc0727365d68b97f3aa78939e735a75204"
+ integrity sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==
dependencies:
asn1.js "^4.0.0"
browserify-aes "^1.0.0"
create-hash "^1.1.0"
evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3"
+ safe-buffer "^5.1.1"
parse-glob@^3.0.4:
version "3.0.4"
@@ -13362,10 +13390,10 @@ postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.23, postcss@^6.0.8:
source-map "^0.6.1"
supports-color "^5.4.0"
-postcss@^7.0.0, postcss@^7.0.11:
- version "7.0.11"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.11.tgz#f63c513b78026d66263bb2ca995bf02e3d1a697d"
- integrity sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==
+postcss@^7.0.0, postcss@^7.0.13:
+ version "7.0.14"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5"
+ integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
@@ -13522,9 +13550,9 @@ prettier@1.9.2:
integrity sha512-piXx9N2WT8hWb7PBbX1glAuJVIkEyUV9F5fMXFINpZ0x3otVOFKKeGmeuiclFJlP/UrgTckyV606VjH2rNK4bw==
prettier@^1.12.1:
- version "1.15.3"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a"
- integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg==
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.1.tgz#534c2c9d7853f8845e5e078384e71973bd74089f"
+ integrity sha512-XXUITwIkGb3CPJ2hforHah/zTINRyie5006Jd2HKy2qz7snEJXl0KLfsJZW/wst9g6R2rFvqba3VpNYdu1hDcA==
pretty-bytes@^4.0.2:
version "4.0.2"
@@ -14012,9 +14040,9 @@ react-dev-utils@^6.1.0:
text-table "0.2.0"
react-docgen-typescript-loader@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.0.0.tgz#4042e2854d29380e4d01e479d438c03ec00de8e8"
- integrity sha512-xtE4bZrU9+7grFFzs8v6gWc+Wl2FCCL59hldHoX2DuQAXOmJIilUm2uPmDmRNA8RpxU1Ax+9Gl0JfUcWgx2QPA==
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.0.1.tgz#889aa472450c8db82ea0355656a307806ec74e77"
+ integrity sha512-SAVMFdW5p76pVMS6c7Jl1mGM5EZLVBWJ+rfoTvEaC2SeDgSFflnkM8eJGZVhty+7FSefvOJcBBz2rMkciphERA==
dependencies:
"@webpack-contrib/schema-utils" "^1.0.0-beta.0"
loader-utils "^1.1.0"
@@ -14134,7 +14162,7 @@ react-inspector@^2.3.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
-react-is@^16.3.2, react-is@^16.6.0, react-is@^16.6.1, react-is@^16.7.0:
+react-is@^16.6.0, react-is@^16.7.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==
@@ -14435,7 +14463,7 @@ readable-stream@~1.1.10:
isarray "0.0.1"
string_decoder "~0.10.x"
-readdir-scoped-modules@^1.0.0:
+readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c=
@@ -14627,6 +14655,15 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
+regexp-tree@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.0.tgz#a56ad7746097888ea16457479029ec9345b96ab0"
+ integrity sha512-rHQv+tzu+0l3KS/ERabas1yK49ahNVxuH40WcPg53CzP5p8TgmmyBgHELLyJcvjhTD0e5ahSY6C76LbEVtr7cg==
+ dependencies:
+ cli-table3 "^0.5.0"
+ colors "^1.1.2"
+ yargs "^10.0.3"
+
regexp.prototype.flags@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c"
@@ -14903,9 +14940,9 @@ resolve@1.1.7, resolve@~1.1.0:
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@1.x, resolve@^1.1.6, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06"
- integrity sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
+ integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
dependencies:
path-parse "^1.0.6"
@@ -15560,16 +15597,16 @@ slide@^1.1.3, slide@^1.1.5, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6:
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
+smart-buffer@4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.2.tgz#5207858c3815cc69110703c6b94e46c15634395d"
+ integrity sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw==
+
smart-buffer@^1.0.13:
version "1.1.15"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
integrity sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=
-smart-buffer@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.1.tgz#07ea1ca8d4db24eb4cac86537d7d18995221ace3"
- integrity sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==
-
snake-case@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f"
@@ -15664,12 +15701,12 @@ socks@^1.1.10:
smart-buffer "^1.0.13"
socks@~2.2.0:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.2.tgz#f061219fc2d4d332afb4af93e865c84d3fa26e2b"
- integrity sha512-g6wjBnnMOZpE0ym6e0uHSddz9p3a+WsBaaYQaBaSCJYvrC4IXykQR9MNGjLQf38e9iIIhp3b1/Zk8YZI3KGJ0Q==
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.3.tgz#7399ce11e19b2a997153c983a9ccb6306721f2dc"
+ integrity sha512-+2r83WaRT3PXYoO/1z+RDEBE7Z2f9YcdQnJ0K/ncXXbV5gJ6wYfNAebYFYiiUjM6E4JyXnPY8cimwyvFYHVUUA==
dependencies:
ip "^1.1.5"
- smart-buffer "^4.0.1"
+ smart-buffer "4.0.2"
sort-keys@^1.0.0:
version "1.1.2"
@@ -15842,9 +15879,9 @@ sprintf-js@~1.0.2:
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sshpk@^1.7.0:
- version "1.16.0"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de"
- integrity sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
@@ -15921,9 +15958,9 @@ stealthy-require@^1.1.0:
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stream-browserify@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
- integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
+ integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
dependencies:
inherits "~2.0.1"
readable-stream "^2.0.2"
@@ -16813,9 +16850,9 @@ typedarray@^0.0.6:
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.0.3, 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==
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d"
+ integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==
ua-parser-js@^0.7.18:
version "0.7.19"
@@ -17155,7 +17192,7 @@ util@0.10.3:
dependencies:
inherits "2.0.1"
-"util@>=0.10.3 <1":
+"util@>=0.10.3 <1", util@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
@@ -17459,9 +17496,9 @@ webpack-dev-middleware@3.4.0:
webpack-log "^2.0.0"
webpack-dev-middleware@^3.4.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.5.0.tgz#fff0a07b0461314fb6ca82df3642c2423f768429"
- integrity sha512-1Zie7+dMr4Vv3nGyhr8mxGQkzTQK1PTS8K3yJ4yB1mfRGwO1DzQibgmNfUqbEfQY6eEtEEUzC+o7vhpm/Sfn5w==
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.5.1.tgz#9265b7742ef50f54f54c1d9af022fc17c1be9b88"
+ integrity sha512-4dwCh/AyMOYAybggUr8fiCkRnjVDp+Cqlr9c+aaNB3GJYgRGYQWJ1YX/WAKUNA9dPNHZ6QSN2lYDKqjKSI8Vqw==
dependencies:
memory-fs "~0.4.1"
mime "^2.3.1"
@@ -17578,16 +17615,16 @@ webpack@4.19.1:
webpack-sources "^1.2.0"
webpack@^4.23.1:
- version "4.28.4"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.28.4.tgz#1ddae6c89887d7efb752adf0c3cd32b9b07eacd0"
- integrity sha512-NxjD61WsK/a3JIdwWjtIpimmvE6UrRi3yG54/74Hk9rwNj5FPkA4DJCf1z4ByDWLkvZhTZE+P3C/eh6UD5lDcw==
+ version "4.29.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.0.tgz#f2cfef83f7ae404ba889ff5d43efd285ca26e750"
+ integrity sha512-pxdGG0keDBtamE1mNvT5zyBdx+7wkh6mh7uzMOo/uRQ/fhsdj5FXkh/j5mapzs060forql1oXqXN9HJGju+y7w==
dependencies:
"@webassemblyjs/ast" "1.7.11"
"@webassemblyjs/helper-module-context" "1.7.11"
"@webassemblyjs/wasm-edit" "1.7.11"
"@webassemblyjs/wasm-parser" "1.7.11"
- acorn "^5.6.2"
- acorn-dynamic-import "^3.0.0"
+ acorn "^6.0.5"
+ acorn-dynamic-import "^4.0.0"
ajv "^6.1.0"
ajv-keywords "^3.1.0"
chrome-trace-event "^1.0.0"
@@ -17761,9 +17798,9 @@ write-file-atomic@^1.2.0:
slide "^1.1.5"
write-file-atomic@^2.0.0, write-file-atomic@^2.1.0, write-file-atomic@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
- integrity sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.2.tgz#a7181706dfba17855d221140a9c06e15fcdd87b9"
+ integrity sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==
dependencies:
graceful-fs "^4.1.11"
imurmurhash "^0.1.4"
@@ -17821,6 +17858,14 @@ xregexp@4.0.0:
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020"
integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==
+xss@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.3.tgz#d04bd2558fd6c29c46113824d5e8b2a910054e23"
+ integrity sha512-LTpz3jXPLUphMMmyufoZRSKnqMj41OVypZ8uYGzvjkMV9C1EdACrhQl/EM8Qfh5htSAuMIQFOejmKAZGkJfaCg==
+ dependencies:
+ commander "^2.9.0"
+ cssfilter "0.0.10"
+
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -17860,6 +17905,13 @@ yargs-parser@^5.0.0:
dependencies:
camelcase "^3.0.0"
+yargs-parser@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
+ integrity sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==
+ dependencies:
+ camelcase "^4.1.0"
+
yargs-parser@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
@@ -17885,6 +17937,24 @@ yargs@12.0.2:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^10.1.0"
+yargs@^10.0.3:
+ version "10.1.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"
+ integrity sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.1.1"
+ find-up "^2.1.0"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^8.1.0"
+
yargs@^11.0.0, yargs@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77"