diff --git a/.circleci/config.yml b/.circleci/config.yml
index 69cea87dccd..49fb3776534 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -56,6 +56,20 @@ jobs:
name: postgres integration tests
command: './scripts/circle-test-postgres.sh'
+ cache-server-test:
+ docker:
+ - image: circleci/golang:1.11.5
+ - image: circleci/redis:4-alpine
+ - image: memcached
+ working_directory: /go/src/github.com/grafana/grafana
+ steps:
+ - checkout
+ - run: dockerize -wait tcp://127.0.0.1:11211 -timeout 120s
+ - run: dockerize -wait tcp://127.0.0.1:6379 -timeout 120s
+ - run:
+ name: cache server tests
+ command: './scripts/circle-test-cache-servers.sh'
+
codespell:
docker:
- image: circleci/python
@@ -308,7 +322,7 @@ jobs:
deploy-enterprise-master:
docker:
- - image: grafana/grafana-ci-deploy:1.2.0
+ - image: grafana/grafana-ci-deploy:1.2.1
steps:
- attach_workspace:
at: .
@@ -331,7 +345,7 @@ jobs:
deploy-enterprise-release:
docker:
- - image: grafana/grafana-ci-deploy:1.2.0
+ - image: grafana/grafana-ci-deploy:1.2.1
steps:
- checkout
- attach_workspace:
@@ -364,7 +378,7 @@ jobs:
deploy-master:
docker:
- - image: grafana/grafana-ci-deploy:1.2.0
+ - image: grafana/grafana-ci-deploy:1.2.1
steps:
- attach_workspace:
at: .
@@ -395,7 +409,7 @@ jobs:
deploy-release:
docker:
- - image: grafana/grafana-ci-deploy:1.2.0
+ - image: grafana/grafana-ci-deploy:1.2.1
steps:
- checkout
- attach_workspace:
@@ -545,6 +559,8 @@ workflows:
filters: *filter-not-release-or-master
- postgres-integration-test:
filters: *filter-not-release-or-master
+ - cache-server-test:
+ filters: *filter-not-release-or-master
- grafana-docker-pr:
requires:
- build
@@ -554,4 +570,5 @@ workflows:
- gometalinter
- mysql-integration-test
- postgres-integration-test
+ - cache-server-test
filters: *filter-not-release-or-master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5483529f75e..5468a2b41f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@
* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
+* **VictorOps**: Adds more information to the victor ops notifiers [#15744](https://github.com/grafana/grafana/issues/15744), thx [@zhulongcheng](https://github.com/zhulongcheng)
+* **Cache**: Adds support for using out of proc caching in the backend [#10816](https://github.com/grafana/grafana/issues/10816)
+* **Dataproxy**: Make it possible to add user details to requests sent to the dataproxy [#6359](https://github.com/grafana/grafana/issues/6359) and [#15931](https://github.com/grafana/grafana/issues/15931)
+* **Auth**: Support listing and revoking auth tokens via API [#15836](https://github.com/grafana/grafana/issues/15836)
+* **Datasource**: Only log connection string in dev environment [#16001](https://github.com/grafana/grafana/issues/16001)
### Bug Fixes
* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
@@ -15,6 +20,16 @@
* **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019)
+# 6.0.2 (2019-03-19)
+
+### Bug Fixes
+* **Alerting**: Fixed issue with AlertList panel links resulting in panel not found errors. [#15975](https://github.com/grafana/grafana/pull/15975), [@torkelo](https://github.com/torkelo)
+* **Dashboard**: Improved error handling when rendering dashboard panels. [#15970](https://github.com/grafana/grafana/pull/15970), [@torkelo](https://github.com/torkelo)
+* **LDAP**: Fix allow anonymous server bind for ldap search. [#15872](https://github.com/grafana/grafana/pull/15872), [@marefr](https://github.com/marefr)
+* **Discord**: Fix discord notifier so it doesn't crash when there are no image generated. [#15833](https://github.com/grafana/grafana/pull/15833), [@marefr](https://github.com/marefr)
+* **Panel Edit**: Prevent search in VizPicker from stealing focus. [#15802](https://github.com/grafana/grafana/pull/15802), [@peterholmberg](https://github.com/peterholmberg)
+* **Datasource admin**: Fixed url of back button in datasource edit page, when root_url configured. [#15759](https://github.com/grafana/grafana/pull/15759), [@dprokop](https://github.com/dprokop)
+
# 6.0.1 (2019-03-06)
### Bug Fixes
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 044d8e59a7a..bb415721391 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -106,6 +106,17 @@ path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
+# memcache: 127.0.0.1:11211
+connstr =
+
#################################### Session #############################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@@ -146,6 +157,9 @@ logging = false
# How long the data proxy should wait before timing out default is 30 (seconds)
timeout = 30
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+send_user_header = false
+
#################################### Analytics ###########################
[analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@@ -245,7 +259,7 @@ external_manage_info =
viewers_can_edit = false
# Editors can administrate dashboard, folders and teams they create
-editors_can_own = false
+editors_can_admin = false
[auth]
# Login cookie name
diff --git a/conf/sample.ini b/conf/sample.ini
index dc1e4fbde8e..321c1120693 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -102,6 +102,17 @@ log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+;type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
+# memcache: 127.0.0.1:11211
+;connstr =
+
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@@ -133,6 +144,9 @@ log_queries =
# How long the data proxy should wait before timing out default is 30 (seconds)
;timeout = 30
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+;send_user_header = false
+
#################################### Analytics ####################################
[analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@@ -225,7 +239,7 @@ log_queries =
;viewers_can_edit = false
# Editors can administrate dashboard, folders and teams they create
-;editors_can_own = false
+;editors_can_admin = false
[auth]
# Login cookie name
diff --git a/devenv/docker/blocks/redis/docker-compose.yaml b/devenv/docker/blocks/redis/docker-compose.yaml
index 65071d4966b..fb56afaac1c 100644
--- a/devenv/docker/blocks/redis/docker-compose.yaml
+++ b/devenv/docker/blocks/redis/docker-compose.yaml
@@ -1,4 +1,4 @@
- memcached:
+ redis:
image: redis:latest
ports:
- "6379:6379"
diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md
index 5d524dcbee2..421e94cbf9f 100644
--- a/docs/sources/enterprise/index.md
+++ b/docs/sources/enterprise/index.md
@@ -38,6 +38,8 @@ With a Grafana Enterprise license you will get access to premium plugins, includ
* [DataDog](https://grafana.com/plugins/grafana-datadog-datasource)
* [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource)
* [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource)
+* [Amazon Timestream](https://grafana.com/plugins/grafana-timestream-datasource)
+* [Oracle Database](https://grafana.com/plugins/grafana-oracle-datasource)
## Try Grafana Enterprise
diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md
index a27fd2aac14..c2d540c452b 100644
--- a/docs/sources/http_api/admin.md
+++ b/docs/sources/http_api/admin.md
@@ -341,3 +341,105 @@ Content-Type: application/json
{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
```
+
+## Auth tokens for User
+
+`GET /api/admin/users/:id/auth-tokens`
+
+Return a list of all auth tokens (devices) that the user currently have logged in from.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+GET /api/admin/users/1/auth-tokens HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+ {
+ "id": 361,
+ "isActive": false,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+ "createdAt": "2019-03-05T21:22:54+01:00",
+ "seenAt": "2019-03-06T19:41:06+01:00"
+ },
+ {
+ "id": 364,
+ "isActive": false,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+ "createdAt": "2019-03-06T19:41:19+01:00",
+ "seenAt": "2019-03-06T19:41:21+01:00"
+ }
+]
+```
+
+## Revoke auth token for User
+
+`POST /api/admin/users/:id/revoke-auth-token`
+
+Revokes the given auth token (device) for the user. User of issued auth token (device) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+POST /api/admin/users/1/revoke-auth-token HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+ "authTokenId": 364
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "message": "User auth token revoked"
+}
+```
+
+## Logout User
+
+`POST /api/admin/users/:id/logout`
+
+Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+POST /api/admin/users/1/logout HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "message": "User auth token revoked"
+}
+```
diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md
index 669e8003247..a81f608c2f5 100644
--- a/docs/sources/http_api/user.md
+++ b/docs/sources/http_api/user.md
@@ -478,3 +478,75 @@ Content-Type: application/json
{"message":"Dashboard unstarred"}
```
+
+## Auth tokens of the actual User
+
+`GET /api/user/auth-tokens`
+
+Return a list of all auth tokens (devices) that the actual user currently have logged in from.
+
+**Example Request**:
+
+```http
+GET /api/user/auth-tokens HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+ {
+ "id": 361,
+ "isActive": true,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+ "createdAt": "2019-03-05T21:22:54+01:00",
+ "seenAt": "2019-03-06T19:41:06+01:00"
+ },
+ {
+ "id": 364,
+ "isActive": false,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+ "createdAt": "2019-03-06T19:41:19+01:00",
+ "seenAt": "2019-03-06T19:41:21+01:00"
+ }
+]
+```
+
+## Revoke an auth token of the actual User
+
+`POST /api/user/revoke-auth-token`
+
+Revokes the given auth token (device) for the actual user. User of issued auth token (device) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+**Example Request**:
+
+```http
+POST /api/user/revoke-auth-token HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+ "authTokenId": 364
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "message": "User auth token revoked"
+}
+```
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 3d1b25979c3..a3dc0c13cf3 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -179,7 +179,6 @@ Path to the certificate key file (if `protocol` is set to `https`).
Set to true for Grafana to log all HTTP requests (not just errors). These are logged as Info level events
to grafana log.
-
@@ -262,6 +261,19 @@ Set to `true` to log the sql calls and execution times.
For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared)
Defaults to private.
+
+
+## [remote_cache]
+
+### type
+
+Either `redis`, `memcached` or `database` default is `database`
+
+### connstr
+
+The remote cache connection string. Leave empty when using `database` since it will use the primary database.
+Redis example config: `addr=127.0.0.1:6379,pool_size=100,db=grafana`
+Memcache example: `127.0.0.1:11211`
@@ -342,6 +354,11 @@ options are `Admin` and `Editor`. e.g. :
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
Defaults to `false`.
+### editors_can_admin
+
+Editors can administrate dashboards, folders and teams they create.
+Defaults to `false`.
+
### login_hint
Text used as placeholder text on login page for login/username input.
@@ -399,6 +416,22 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours).
+## [dataproxy]
+
+### logging
+
+This enables data proxy logging, default is false.
+
+### timeout
+
+How long the data proxy should wait before timing out default is 30 (seconds)
+
+### send_user_header
+
+If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+
+
+
## [analytics]
### reporting_enabled
diff --git a/docs/sources/permissions/organization_roles.md b/docs/sources/permissions/organization_roles.md
index 626d79fad87..257982afdc4 100644
--- a/docs/sources/permissions/organization_roles.md
+++ b/docs/sources/permissions/organization_roles.md
@@ -28,6 +28,9 @@ Can do everything scoped to the organization. For example:
- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards.
- **Cannot** create or edit data sources nor invite new users.
+This role can be tweaked via Grafana server setting [editors_can_admin]({{< relref "installation/configuration.md#editors_can_admin" >}}). If you set this to true users
+with **Editor** can also administrate dashboards, folders and teams they create. Useful for enabling self organizing teams.
+
## Viewer Role
- View any dashboard. This can be disabled on specific folders and dashboards.
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index b00e44943ef..2be2b6863a9 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -110,7 +110,7 @@ Formats single & multi valued variables for use in URL parameters.
```bash
servers = ['foo()bar BAZ', 'test2']
-String to interpolate: '${servers:lucene}'
+String to interpolate: '${servers:percentencode}'
Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2'
```
diff --git a/latest.json b/latest.json
index e19a0f8550d..bc1ac9d90cc 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
- "stable": "6.0.1",
- "testing": "6.0.1"
+ "stable": "6.0.2",
+ "testing": "6.0.2"
}
diff --git a/package.json b/package.json
index 7f8161416de..bfdae59bdcf 100644
--- a/package.json
+++ b/package.json
@@ -28,12 +28,13 @@
"@types/jquery": "^1.10.35",
"@types/node": "^8.0.31",
"@types/papaparse": "^4.5.9",
- "@types/react": "^16.7.6",
- "@types/react-dom": "^16.0.9",
+ "@types/react": "^16.8.8",
+ "@types/react-dom": "^16.8.2",
"@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-transition-group": "^2.0.15",
"@types/react-virtualized": "^9.18.12",
+ "@types/clipboard": "^2.0.1",
"angular-mocks": "1.6.6",
"autoprefixer": "^9.4.10",
"axios": "^0.18.0",
@@ -177,7 +178,7 @@
"baron": "^3.0.3",
"brace": "^0.10.0",
"classnames": "^2.2.6",
- "clipboard": "^1.7.1",
+ "clipboard": "^2.0.4",
"d3": "^4.11.0",
"d3-scale-chromatic": "^1.3.0",
"eventemitter3": "^2.0.3",
@@ -193,8 +194,8 @@
"prismjs": "^1.6.0",
"prop-types": "^15.6.2",
"rc-cascader": "^0.14.0",
- "react": "^16.6.3",
- "react-dom": "^16.6.3",
+ "react": "^16.8.4",
+ "react-dom": "^16.8.4",
"react-grid-layout": "0.16.6",
"react-highlight-words": "0.11.0",
"react-popper": "^1.3.0",
@@ -221,7 +222,7 @@
},
"resolutions": {
"caniuse-db": "1.0.30000772",
- "**/@types/react": "16.7.6"
+ "**/@types/react": "16.8.8"
},
"workspaces": {
"packages": [
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index aec32fd9282..bfe5d48cbf6 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -25,10 +25,10 @@
"lodash": "^4.17.10",
"moment": "^2.22.2",
"papaparse": "^4.6.3",
- "react": "^16.6.3",
+ "react": "^16.8.4",
"react-color": "^2.17.0",
"react-custom-scrollbars": "^4.2.1",
- "react-dom": "^16.6.3",
+ "react-dom": "^16.8.4",
"react-highlight-words": "0.11.0",
"react-popper": "^1.3.0",
"react-transition-group": "^2.2.1",
@@ -48,7 +48,7 @@
"@types/lodash": "^4.14.119",
"@types/node": "^10.12.18",
"@types/papaparse": "^4.5.9",
- "@types/react": "^16.7.6",
+ "@types/react": "^16.8.8",
"@types/react-custom-scrollbars": "^4.0.5",
"@types/react-test-renderer": "^16.0.3",
"@types/react-transition-group": "^2.0.15",
diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
index c7a53af5ccf..6754d43a5cc 100644
--- a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
+++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
@@ -1,6 +1,7 @@
import { storiesOf } from '@storybook/react';
-import { number, text } from '@storybook/addon-knobs';
+import { number, text, boolean } from '@storybook/addon-knobs';
import { BarGauge } from './BarGauge';
+import { VizOrientation } from '../../types';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
@@ -15,6 +16,8 @@ const getKnobs = () => {
threshold2Color: text('threshold2Color', 'red'),
unit: text('unit', 'ms'),
decimals: number('decimals', 1),
+ horizontal: boolean('horizontal', false),
+ lcd: boolean('lcd', false),
};
};
@@ -22,7 +25,7 @@ const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
BarGaugeStories.addDecorator(withCenteredStory);
-BarGaugeStories.add('Vertical, with basic thresholds', () => {
+BarGaugeStories.add('Simple with basic thresholds', () => {
const {
value,
minValue,
@@ -33,11 +36,13 @@ BarGaugeStories.add('Vertical, with basic thresholds', () => {
threshold2Value,
unit,
decimals,
+ horizontal,
+ lcd,
} = getKnobs();
return renderComponentWithTheme(BarGauge, {
- width: 200,
- height: 400,
+ width: 700,
+ height: 700,
value: value,
minValue: minValue,
maxValue: maxValue,
@@ -45,6 +50,8 @@ BarGaugeStories.add('Vertical, with basic thresholds', () => {
prefix: '',
postfix: '',
decimals: decimals,
+ orientation: horizontal ? VizOrientation.Horizontal : VizOrientation.Vertical,
+ displayMode: lcd ? 'lcd' : 'simple',
thresholds: [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: threshold1Value, color: threshold1Color },
diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
index 8fa0b2846a5..69cdbf5bff1 100644
--- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
+++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
@@ -11,16 +11,15 @@ jest.mock('jquery', () => ({
const setup = (propOverrides?: object) => {
const props: Props = {
maxValue: 100,
- valueMappings: [],
minValue: 0,
- prefix: '',
- suffix: '',
+ displayMode: 'basic',
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
- unit: 'none',
height: 300,
width: 300,
- value: 25,
- decimals: 0,
+ value: {
+ text: '25',
+ numeric: 25,
+ },
theme: getTheme(),
orientation: VizOrientation.Horizontal,
};
diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
index 97cc4792785..99d2d061a11 100644
--- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
+++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
@@ -1,55 +1,54 @@
// Library
-import React, { PureComponent, CSSProperties } from 'react';
+import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import tinycolor from 'tinycolor2';
// Utils
-import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+import { getColorFromHexRgbOrName, getThresholdForValue, DisplayValue } from '../../utils';
// Types
-import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
+import { Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
const BAR_SIZE_RATIO = 0.8;
export interface Props extends Themeable {
height: number;
- unit: string;
width: number;
thresholds: Threshold[];
- valueMappings: ValueMapping[];
- value: TimeSeriesValue;
+ value: DisplayValue;
maxValue: number;
minValue: number;
orientation: VizOrientation;
- prefix?: string;
- suffix?: string;
- decimals?: number;
+ displayMode: 'basic' | 'lcd' | 'gradient';
}
-/*
- * This visualization is still in POC state, needed more tests & better structure
- */
export class BarGauge extends PureComponent {
static defaultProps: Partial = {
maxValue: 100,
minValue: 0,
- value: 100,
- unit: 'none',
+ value: {
+ text: '100',
+ numeric: 100,
+ },
+ displayMode: 'lcd',
orientation: VizOrientation.Horizontal,
thresholds: [],
- valueMappings: [],
};
- getNumericValue(): number {
- if (Number.isFinite(this.props.value as number)) {
- return this.props.value as number;
+ render() {
+ switch (this.props.displayMode) {
+ case 'lcd':
+ return this.renderRetroBars();
+ case 'basic':
+ case 'gradient':
+ default:
+ return this.renderBasicAndGradientBars();
}
- return 0;
}
getValueColors(): BarColors {
const { thresholds, theme, value } = this.props;
- const activeThreshold = getThresholdForValue(thresholds, value);
+ const activeThreshold = getThresholdForValue(thresholds, value.numeric);
if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
@@ -57,41 +56,19 @@ export class BarGauge extends PureComponent {
return {
value: color,
border: color,
- bar: tinycolor(color)
- .setAlpha(0.3)
+ background: tinycolor(color)
+ .setAlpha(0.15)
.toRgbString(),
};
}
return {
value: getColorFromHexRgbOrName('gray', theme.type),
- bar: getColorFromHexRgbOrName('gray', theme.type),
+ background: getColorFromHexRgbOrName('gray', theme.type),
border: getColorFromHexRgbOrName('gray', theme.type),
};
}
- getCellColor(positionValue: TimeSeriesValue): string {
- const { thresholds, theme, value } = this.props;
- const activeThreshold = getThresholdForValue(thresholds, positionValue);
-
- if (activeThreshold !== null) {
- const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
-
- // if we are past real value the cell is not "on"
- if (value === null || (positionValue !== null && positionValue > value)) {
- return tinycolor(color)
- .setAlpha(0.15)
- .toRgbString();
- } else {
- return tinycolor(color)
- .setAlpha(0.7)
- .toRgbString();
- }
- }
-
- return 'gray';
- }
-
getValueStyles(value: string, color: string, width: number): CSSProperties {
const guess = width / (value.length * 1.1);
const fontSize = Math.min(Math.max(guess, 14), 40);
@@ -102,107 +79,205 @@ export class BarGauge extends PureComponent {
};
}
- renderVerticalBar(valueFormatted: string, valuePercent: number) {
+ /*
+ * Return width or height depending on viz orientation
+ * */
+ get size() {
const { height, width } = this.props;
+ return this.isVertical ? height : width;
+ }
- const maxHeight = height * BAR_SIZE_RATIO;
- const barHeight = Math.max(valuePercent * maxHeight, 0);
+ get isVertical() {
+ return this.props.orientation === VizOrientation.Vertical;
+ }
+
+ getBarGradient(maxSize: number): string {
+ const { minValue, maxValue, thresholds, value } = this.props;
+ const cssDirection = this.isVertical ? '0deg' : '90deg';
+
+ let gradient = '';
+ let lastpos = 0;
+
+ for (let i = 0; i < thresholds.length; i++) {
+ const threshold = thresholds[i];
+ const color = getColorFromHexRgbOrName(threshold.color);
+ const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1);
+ const pos = valuePercent * maxSize;
+ const offset = Math.round(pos - (pos - lastpos) / 2);
+
+ if (gradient === '') {
+ gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
+ } else if (value.numeric < threshold.value) {
+ break;
+ } else {
+ lastpos = pos;
+ gradient += ` ${offset}px, ${color}`;
+ }
+ }
+
+ return gradient + ')';
+ }
+
+ renderBasicAndGradientBars(): ReactNode {
+ const { height, width, displayMode, maxValue, minValue, value } = this.props;
+
+ const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
+ const maxSize = this.size * BAR_SIZE_RATIO;
+ const barSize = Math.max(valuePercent * maxSize, 0);
const colors = this.getValueColors();
- const valueStyles = this.getValueStyles(valueFormatted, colors.value, width);
+ const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
+ const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
+ const isBasic = displayMode === 'basic';
const containerStyles: CSSProperties = {
width: `${width}px`,
height: `${height}px`,
display: 'flex',
- flexDirection: 'column',
- justifyContent: 'flex-end',
};
const barStyles: CSSProperties = {
- height: `${barHeight}px`,
- width: `${width}px`,
- backgroundColor: colors.bar,
- borderTop: `1px solid ${colors.border}`,
+ borderRadius: '3px',
};
+ if (this.isVertical) {
+ // Custom styles for vertical orientation
+ containerStyles.flexDirection = 'column';
+ containerStyles.justifyContent = 'flex-end';
+ barStyles.transition = 'height 1s';
+ barStyles.height = `${barSize}px`;
+ barStyles.width = `${width}px`;
+ if (isBasic) {
+ // Basic styles
+ barStyles.background = `${colors.background}`;
+ barStyles.border = `1px solid ${colors.border}`;
+ barStyles.boxShadow = `0 0 4px ${colors.border}`;
+ } else {
+ // Gradient styles
+ barStyles.background = this.getBarGradient(maxSize);
+ }
+ } else {
+ // Custom styles for horizontal orientation
+ containerStyles.flexDirection = 'row-reverse';
+ containerStyles.justifyContent = 'flex-end';
+ containerStyles.alignItems = 'center';
+ barStyles.transition = 'width 1s';
+ barStyles.height = `${height}px`;
+ barStyles.width = `${barSize}px`;
+ barStyles.marginRight = '10px';
+
+ if (isBasic) {
+ // Basic styles
+ barStyles.background = `${colors.background}`;
+ barStyles.border = `1px solid ${colors.border}`;
+ barStyles.boxShadow = `0 0 4px ${colors.border}`;
+ } else {
+ // Gradient styles
+ barStyles.background = this.getBarGradient(maxSize);
+ }
+ }
+
return (
- {valueFormatted}
+ {value.text}
);
}
- renderHorizontalBar(valueFormatted: string, valuePercent: number) {
- const { height, width } = this.props;
+ getCellColor(positionValue: TimeSeriesValue): CellColors {
+ const { thresholds, theme, value } = this.props;
+ const activeThreshold = getThresholdForValue(thresholds, positionValue);
- const maxWidth = width * BAR_SIZE_RATIO;
- const barWidth = Math.max(valuePercent * maxWidth, 0);
- const colors = this.getValueColors();
- const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
+ if (activeThreshold !== null) {
+ const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
- valueStyles.marginLeft = '8px';
+ // if we are past real value the cell is not "on"
+ if (value === null || (positionValue !== null && positionValue > value.numeric)) {
+ return {
+ background: tinycolor(color)
+ .setAlpha(0.15)
+ .toRgbString(),
+ border: 'transparent',
+ isLit: false,
+ };
+ } else {
+ return {
+ background: tinycolor(color)
+ .setAlpha(0.85)
+ .toRgbString(),
+ backgroundShade: tinycolor(color)
+ .setAlpha(0.55)
+ .toRgbString(),
+ border: tinycolor(color)
+ .setAlpha(0.9)
+ .toRgbString(),
+ isLit: true,
+ };
+ }
+ }
- const containerStyles: CSSProperties = {
- width: `${width}px`,
- height: `${height}px`,
- display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
+ return {
+ background: 'gray',
+ border: 'gray',
};
-
- const barStyles = {
- height: `${height}px`,
- width: `${barWidth}px`,
- backgroundColor: colors.bar,
- borderRight: `1px solid ${colors.border}`,
- };
-
- return (
-
-
-
- {valueFormatted}
-
-
- );
}
- renderHorizontalLCD(valueFormatted: string, valuePercent: number) {
- const { height, width, maxValue, minValue } = this.props;
+ renderRetroBars(): ReactNode {
+ const { height, width, maxValue, minValue, value } = this.props;
const valueRange = maxValue - minValue;
- const maxWidth = width * BAR_SIZE_RATIO;
- const cellSpacing = 4;
- const cellCount = 30;
- const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount;
+ const maxSize = this.size * BAR_SIZE_RATIO;
+ const cellSpacing = 5;
+ const cellCount = maxSize / 20;
+ const cellSize = (maxSize - cellSpacing * cellCount) / cellCount;
const colors = this.getValueColors();
- const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
- valueStyles.marginLeft = '8px';
+ const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
+ const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
const containerStyles: CSSProperties = {
width: `${width}px`,
height: `${height}px`,
display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
};
+ if (this.isVertical) {
+ containerStyles.flexDirection = 'column-reverse';
+ containerStyles.alignItems = 'center';
+ valueStyles.marginBottom = '20px';
+ } else {
+ containerStyles.flexDirection = 'row';
+ containerStyles.alignItems = 'center';
+ valueStyles.marginLeft = '20px';
+ }
+
const cells: JSX.Element[] = [];
for (let i = 0; i < cellCount; i++) {
const currentValue = (valueRange / cellCount) * i;
const cellColor = this.getCellColor(currentValue);
const cellStyles: CSSProperties = {
- width: `${cellWidth}px`,
- backgroundColor: cellColor,
- marginRight: '4px',
- height: `${height}px`,
borderRadius: '2px',
};
+ if (cellColor.isLit) {
+ cellStyles.boxShadow = `0 0 4px ${cellColor.border}`;
+ cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`;
+ } else {
+ cellStyles.backgroundColor = cellColor.background;
+ }
+
+ if (this.isVertical) {
+ cellStyles.height = `${cellSize}px`;
+ cellStyles.width = `${width}px`;
+ cellStyles.marginTop = `${cellSpacing}px`;
+ } else {
+ cellStyles.width = `${cellSize}px`;
+ cellStyles.height = `${height}px`;
+ cellStyles.marginRight = `${cellSpacing}px`;
+ }
+
cells.push(
);
}
@@ -210,30 +285,22 @@ export class BarGauge extends PureComponent {
{cells}
- {valueFormatted}
+ {value.text}
);
}
-
- render() {
- const { maxValue, minValue, orientation, unit, decimals } = this.props;
-
- const numericValue = this.getNumericValue();
- const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
-
- const formatFunc = getValueFormat(unit);
- const valueFormatted = formatFunc(numericValue, decimals);
- const vertical = orientation === 'vertical';
-
- return vertical
- ? this.renderVerticalBar(valueFormatted, valuePercent)
- : this.renderHorizontalLCD(valueFormatted, valuePercent);
- }
}
interface BarColors {
value: string;
- bar: string;
+ background: string;
border: string;
}
+
+interface CellColors {
+ background: string;
+ backgroundShade?: string;
+ border: string;
+ isLit?: boolean;
+}
diff --git a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap
index 65c647bd90c..25a2e79b647 100644
--- a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap
+++ b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap
@@ -6,353 +6,37 @@ exports[`Render BarGauge with basic options should render 1`] = `
Object {
"alignItems": "center",
"display": "flex",
- "flexDirection": "row",
+ "flexDirection": "row-reverse",
"height": "300px",
+ "justifyContent": "flex-end",
"width": "300px",
}
}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
25
+
`;
diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx
index df65d156ab3..d262c821968 100644
--- a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx
+++ b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx
@@ -2,6 +2,7 @@ import React, { PureComponent, SyntheticEvent } from 'react';
interface Props {
onConfirm(): void;
+ disabled?: boolean;
}
interface State {
@@ -33,25 +34,22 @@ export class DeleteButton extends PureComponent {
};
render() {
- const { onConfirm } = this.props;
- let showConfirm;
- let showDeleteButton;
-
- if (this.state.showConfirm) {
- showConfirm = 'show';
- showDeleteButton = 'hide';
- } else {
- showConfirm = 'hide';
- showDeleteButton = 'show';
- }
+ const { onConfirm, disabled } = this.props;
+ const showConfirmClass = this.state.showConfirm ? 'show' : 'hide';
+ const showDeleteButtonClass = this.state.showConfirm ? 'hide' : 'show';
+ const disabledClass = disabled ? 'disabled btn-inverse' : '';
+ const onClick = disabled ? () => {} : this.onClickDelete;
return (
-
+
-
+
Cancel
diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
index 70e29abc221..c6a49eb5b55 100644
--- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
@@ -2,7 +2,6 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
-import { ValueMapping, MappingType } from '../../types';
import { getTheme } from '../../themes';
jest.mock('jquery', () => ({
@@ -12,19 +11,16 @@ jest.mock('jquery', () => ({
const setup = (propOverrides?: object) => {
const props: Props = {
maxValue: 100,
- valueMappings: [],
minValue: 0,
- prefix: '',
showThresholdMarkers: true,
showThresholdLabels: false,
- suffix: '',
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
- unit: 'none',
- stat: 'avg',
height: 300,
width: 300,
- value: 25,
- decimals: 0,
+ value: {
+ text: '25',
+ numeric: 25,
+ },
theme: getTheme(),
};
@@ -39,38 +35,6 @@ const setup = (propOverrides?: object) => {
};
};
-describe('Get font color', () => {
- it('should get first threshold color when only one threshold', () => {
- const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
-
- expect(instance.getFontColor(49)).toEqual('#7EB26D');
- });
-
- it('should get the threshold color if value is same as a threshold', () => {
- const { instance } = setup({
- thresholds: [
- { index: 2, value: 75, color: '#6ED0E0' },
- { index: 1, value: 50, color: '#EAB839' },
- { index: 0, value: -Infinity, color: '#7EB26D' },
- ],
- });
-
- expect(instance.getFontColor(50)).toEqual('#EAB839');
- });
-
- it('should get the nearest threshold color between thresholds', () => {
- const { instance } = setup({
- thresholds: [
- { index: 2, value: 75, color: '#6ED0E0' },
- { index: 1, value: 50, color: '#EAB839' },
- { index: 0, value: -Infinity, color: '#7EB26D' },
- ],
- });
-
- expect(instance.getFontColor(55)).toEqual('#EAB839');
- });
-});
-
describe('Get thresholds formatted', () => {
it('should return first thresholds color for min and max', () => {
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
@@ -98,51 +62,3 @@ describe('Get thresholds formatted', () => {
]);
});
});
-
-describe('Format value', () => {
- it('should return if value isNaN', () => {
- const valueMappings: ValueMapping[] = [];
- const value = 'N/A';
- const { instance } = setup({ valueMappings });
-
- const result = instance.formatValue(value);
-
- expect(result).toEqual('N/A');
- });
-
- it('should return formatted value if there are no value mappings', () => {
- const valueMappings: ValueMapping[] = [];
- const value = '6';
- const { instance } = setup({ valueMappings, decimals: 1 });
-
- const result = instance.formatValue(value);
-
- expect(result).toEqual('6.0');
- });
-
- it('should return formatted value if there are no matching value mappings', () => {
- 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, decimals: 1 });
-
- const result = instance.formatValue(value);
-
- expect(result).toEqual('10.0');
- });
-
- it('should return mapped value if there are matching value mappings', () => {
- 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 = '11';
- const { instance } = setup({ valueMappings, decimals: 1 });
-
- const result = instance.formatValue(value);
-
- expect(result).toEqual('1-20');
- });
-});
diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
index 3d1567d4937..f812cc971bc 100644
--- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
@@ -1,28 +1,20 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
-import { ValueMapping, Threshold, GrafanaThemeType } from '../../types';
-import { getMappedValue } from '../../utils/valueMappings';
-import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+import { Threshold, GrafanaThemeType } from '../../types';
+import { getColorFromHexRgbOrName } from '../../utils';
import { Themeable } from '../../index';
-
-type GaugeValue = string | number | null;
+import { DisplayValue } from '../../utils/displayValue';
export interface Props extends Themeable {
- decimals?: number | null;
height: number;
- valueMappings: ValueMapping[];
maxValue: number;
minValue: number;
- prefix: string;
thresholds: Threshold[];
showThresholdMarkers: boolean;
showThresholdLabels: boolean;
- stat: string;
- suffix: string;
- unit: string;
width: number;
- value: number;
+ value: DisplayValue;
}
const FONT_SCALE = 1;
@@ -32,15 +24,10 @@ export class Gauge extends PureComponent {
static defaultProps: Partial = {
maxValue: 100,
- valueMappings: [],
minValue: 0,
- prefix: '',
showThresholdMarkers: true,
showThresholdLabels: false,
- suffix: '',
thresholds: [],
- unit: 'none',
- stat: 'avg',
};
componentDidMount() {
@@ -51,39 +38,6 @@ export class Gauge extends PureComponent {
this.draw();
}
- formatValue(value: GaugeValue) {
- const { decimals, valueMappings, prefix, suffix, unit } = this.props;
-
- if (isNaN(value as number)) {
- return value;
- }
-
- if (valueMappings.length > 0) {
- const valueMappedValue = getMappedValue(valueMappings, value);
- if (valueMappedValue) {
- return `${prefix && prefix + ' '}${valueMappedValue.text}${suffix && ' ' + suffix}`;
- }
- }
-
- const formatFunc = getValueFormat(unit);
- const formattedValue = formatFunc(value as number, decimals);
- const handleNoValueValue = formattedValue || 'no value';
-
- return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
- }
-
- getFontColor(value: GaugeValue): string {
- const { thresholds, theme } = this.props;
-
- const activeThreshold = getThresholdForValue(thresholds, value);
-
- if (activeThreshold !== null) {
- return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
- }
-
- return '';
- }
-
getFormattedThresholds() {
const { maxValue, minValue, thresholds, theme } = this.props;
@@ -112,15 +66,13 @@ export class Gauge extends PureComponent {
draw() {
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
- const formattedValue = this.formatValue(value) as string;
const dimension = Math.min(width, height * 1.3);
const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3;
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
const thresholdMarkersWidth = gaugeWidth / 5;
- const fontSize =
- Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
+ const fontSize = Math.min(dimension / 5, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
const thresholdLabelFontSize = fontSize / 2.5;
const options: any = {
@@ -149,9 +101,9 @@ export class Gauge extends PureComponent {
width: thresholdMarkersWidth,
},
value: {
- color: this.getFontColor(value),
+ color: value.color,
formatter: () => {
- return formattedValue;
+ return value.text;
},
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
},
@@ -160,7 +112,7 @@ export class Gauge extends PureComponent {
},
};
- const plotSeries = { data: [[0, value]] };
+ const plotSeries = { data: [[0, value.numeric]] };
try {
$.plot(this.canvasElement, [plotSeries], options);
diff --git a/public/app/core/components/Form/Input.test.tsx b/packages/grafana-ui/src/components/Input/Input.test.tsx
similarity index 83%
rename from public/app/core/components/Form/Input.test.tsx
rename to packages/grafana-ui/src/components/Input/Input.test.tsx
index 9e903208e80..1d39b594b1c 100644
--- a/public/app/core/components/Form/Input.test.tsx
+++ b/packages/grafana-ui/src/components/Input/Input.test.tsx
@@ -1,18 +1,16 @@
-import React from 'react';
+import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
-import { Input, EventsWithValidation } from './Input';
-import { ValidationEvents } from 'app/types';
+import { Input } from './Input';
+import { EventsWithValidation } from '../../utils';
+import { ValidationEvents } from '../../types';
const TEST_ERROR_MESSAGE = 'Value must be empty or less than 3 chars';
const testBlurValidation: ValidationEvents = {
[EventsWithValidation.onBlur]: [
{
rule: (value: string) => {
- if (!value || value.length < 3) {
- return true;
- }
- return false;
+ return !value || value.length < 3;
},
errorMessage: TEST_ERROR_MESSAGE,
},
diff --git a/public/app/core/components/Form/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx
similarity index 66%
rename from public/app/core/components/Form/Input.tsx
rename to packages/grafana-ui/src/components/Input/Input.tsx
index 7940f3b1104..f5f59e265c0 100644
--- a/public/app/core/components/Form/Input.tsx
+++ b/packages/grafana-ui/src/components/Input/Input.tsx
@@ -1,26 +1,13 @@
-import React, { PureComponent } from 'react';
+import React, { PureComponent, ChangeEvent } from 'react';
import classNames from 'classnames';
-import { ValidationEvents, ValidationRule } from 'app/types';
-import { validate, hasValidationEvent } from 'app/core/utils/validate';
+import { validate, EventsWithValidation, hasValidationEvent } from '../../utils';
+import { ValidationEvents, ValidationRule } from '../../types';
export enum InputStatus {
Invalid = 'invalid',
Valid = 'valid',
}
-export enum InputTypes {
- Text = 'text',
- Number = 'number',
- Password = 'password',
- Email = 'email',
-}
-
-export enum EventsWithValidation {
- onBlur = 'onBlur',
- onFocus = 'onFocus',
- onChange = 'onChange',
-}
-
interface Props extends React.HTMLProps {
validationEvents?: ValidationEvents;
hideErrorMessage?: boolean;
@@ -28,7 +15,7 @@ interface Props extends React.HTMLProps {
// Override event props and append status as argument
onBlur?: (event: React.FocusEvent, status?: InputStatus) => void;
onFocus?: (event: React.FocusEvent, status?: InputStatus) => void;
- onChange?: (event: React.FormEvent, status?: InputStatus) => void;
+ onChange?: (event: React.ChangeEvent, status?: InputStatus) => void;
}
export class Input extends PureComponent {
@@ -49,24 +36,24 @@ export class Input extends PureComponent {
}
validatorAsync = (validationRules: ValidationRule[]) => {
- return evt => {
+ return (evt: ChangeEvent) => {
const errors = validate(evt.target.value, validationRules);
this.setState(prevState => {
- return {
- ...prevState,
- error: errors ? errors[0] : null,
- };
+ return { ...prevState, error: errors ? errors[0] : null };
});
};
};
- populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => {
+ populateEventPropsWithStatus = (restProps: any, validationEvents: ValidationEvents | undefined) => {
const inputElementProps = { ...restProps };
- Object.keys(EventsWithValidation).forEach((eventName: EventsWithValidation) => {
- if (hasValidationEvent(eventName, validationEvents) || restProps[eventName]) {
- inputElementProps[eventName] = async evt => {
+ if (!validationEvents) {
+ return inputElementProps;
+ }
+ Object.keys(EventsWithValidation).forEach(eventName => {
+ if (hasValidationEvent(eventName as EventsWithValidation, validationEvents) || restProps[eventName]) {
+ inputElementProps[eventName] = async (evt: ChangeEvent) => {
evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling
- if (hasValidationEvent(eventName, validationEvents)) {
+ if (hasValidationEvent(eventName as EventsWithValidation, validationEvents)) {
await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
}
if (restProps[eventName]) {
diff --git a/public/app/core/components/Form/__snapshots__/Input.test.tsx.snap b/packages/grafana-ui/src/components/Input/__snapshots__/Input.test.tsx.snap
similarity index 100%
rename from public/app/core/components/Form/__snapshots__/Input.test.tsx.snap
rename to packages/grafana-ui/src/components/Input/__snapshots__/Input.test.tsx.snap
diff --git a/packages/grafana-ui/src/components/Select/SelectOption.test.tsx b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx
index 0a5a8864d64..d91bc9ee6af 100644
--- a/packages/grafana-ui/src/components/Select/SelectOption.test.tsx
+++ b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx
@@ -25,6 +25,7 @@ const model: OptionProps = {
key: '',
onClick: jest.fn(),
onMouseOver: jest.fn(),
+ onMouseMove: jest.fn(),
tabIndex: 1,
},
label: 'Option label',
diff --git a/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap b/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap
index c52be902edd..95c15ab013f 100644
--- a/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap
+++ b/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap
@@ -4,6 +4,7 @@ exports[`SelectOption renders correctly 1`] = `
diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx
index 03765730343..8ab53a1ac77 100644
--- a/packages/grafana-ui/src/components/Table/Table.story.tsx
+++ b/packages/grafana-ui/src/components/Table/Table.story.tsx
@@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
const suffix = (rowId + 1).toString();
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
}),
- type: 'table',
- columnMap: {},
};
}
diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx
index a551d6d221d..c32c46f8b88 100644
--- a/packages/grafana-ui/src/components/Table/Table.tsx
+++ b/packages/grafana-ui/src/components/Table/Table.tsx
@@ -8,6 +8,7 @@ import {
CellMeasurerCache,
CellMeasurer,
GridCellProps,
+ Index,
} from 'react-virtualized';
import { Themeable } from '../../types/theme';
@@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index';
export interface Props extends Themeable {
data: TableData;
+ minColumnWidth: number;
showHeader: boolean;
fixedHeader: boolean;
fixedColumns: number;
@@ -46,6 +48,7 @@ interface State {
interface ColumnRenderInfo {
header: string;
+ width: number;
builder: TableCellBuilder;
}
@@ -64,6 +67,7 @@ export class Table extends Component
{
fixedHeader: true,
fixedColumns: 0,
rotate: false,
+ minColumnWidth: 150,
};
constructor(props: Props) {
@@ -76,7 +80,7 @@ export class Table extends Component {
this.renderer = this.initColumns(props);
this.measurer = new CellMeasurerCache({
defaultHeight: 30,
- defaultWidth: 150,
+ fixedWidth: true,
});
}
@@ -110,7 +114,8 @@ export class Table extends Component {
/** Given the configuration, setup how each column gets rendered */
initColumns(props: Props): ColumnRenderInfo[] {
- const { styles, data } = props;
+ const { styles, data, width, minColumnWidth } = props;
+ const columnWidth = Math.max(width / data.columns.length, minColumnWidth);
return data.columns.map((col, index) => {
let title = col.text;
@@ -131,6 +136,7 @@ export class Table extends Component {
return {
header: title,
+ width: columnWidth,
builder: getCellBuilder(col, style, this.props),
};
});
@@ -228,6 +234,10 @@ export class Table extends Component {
);
};
+ getColumnWidth = (col: Index): number => {
+ return this.renderer[col.index].width;
+ };
+
render() {
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
const { data } = this.state;
@@ -269,7 +279,7 @@ export class Table extends Component {
rowCount={rowCount}
overscanColumnCount={8}
overscanRowCount={8}
- columnWidth={this.measurer.columnWidth}
+ columnWidth={this.getColumnWidth}
deferredMeasurementCache={this.measurer}
cellRenderer={this.cellRenderer}
rowHeight={this.measurer.rowHeight}
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx
new file mode 100644
index 00000000000..8d6112130e7
--- /dev/null
+++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+
+import { ThresholdsEditor } from './ThresholdsEditor';
+
+const ThresholdsEditorStories = storiesOf('UI/ThresholdsEditor', module);
+const thresholds = [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 50, color: 'red' }];
+
+ThresholdsEditorStories.add('default', () => {
+ return ;
+});
+
+ThresholdsEditorStories.add('with thresholds', () => {
+ return ;
+});
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
index ea94537c429..db494053d6e 100644
--- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
+++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
@@ -1,6 +1,7 @@
import React, { ChangeEvent } from 'react';
import { mount } from 'enzyme';
import { ThresholdsEditor, Props } from './ThresholdsEditor';
+import { colors } from '../../utils';
const setup = (propOverrides?: Partial) => {
const props: Props = {
@@ -31,7 +32,7 @@ describe('Initialization', () => {
it('should add a base threshold if missing', () => {
const { instance } = setup();
- expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
+ expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
});
});
@@ -41,7 +42,7 @@ describe('Add threshold', () => {
instance.onAddThreshold(0);
- expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
+ expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
});
it('should add threshold', () => {
@@ -50,41 +51,41 @@ describe('Add threshold', () => {
instance.onAddThreshold(1);
expect(instance.state.thresholds).toEqual([
- { index: 0, value: -Infinity, color: '#7EB26D' },
- { index: 1, value: 50, color: '#EAB839' },
+ { index: 0, value: -Infinity, color: colors[0] },
+ { index: 1, value: 50, color: colors[2] },
]);
});
it('should add another threshold above a first', () => {
const { instance } = setup({
- thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
+ thresholds: [{ index: 0, value: -Infinity, color: colors[0] }, { index: 1, value: 50, color: colors[2] }],
});
instance.onAddThreshold(2);
expect(instance.state.thresholds).toEqual([
- { index: 0, value: -Infinity, color: '#7EB26D' },
- { index: 1, value: 50, color: '#EAB839' },
- { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 0, value: -Infinity, color: colors[0] },
+ { index: 1, value: 50, color: colors[2] },
+ { index: 2, value: 75, color: colors[3] },
]);
});
it('should add another threshold between first and second index', () => {
const { instance } = setup({
thresholds: [
- { index: 0, value: -Infinity, color: '#7EB26D' },
- { index: 1, value: 50, color: '#EAB839' },
- { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 0, value: -Infinity, color: colors[0] },
+ { index: 1, value: 50, color: colors[2] },
+ { index: 2, value: 75, color: colors[3] },
],
});
instance.onAddThreshold(2);
expect(instance.state.thresholds).toEqual([
- { index: 0, value: -Infinity, color: '#7EB26D' },
- { index: 1, value: 50, color: '#EAB839' },
- { index: 2, value: 62.5, color: '#EF843C' },
- { index: 3, value: 75, color: '#6ED0E0' },
+ { index: 0, value: -Infinity, color: colors[0] },
+ { index: 1, value: 50, color: colors[2] },
+ { index: 2, value: 62.5, color: colors[4] },
+ { index: 3, value: 75, color: colors[3] },
]);
});
});
@@ -161,7 +162,7 @@ describe('change threshold value', () => {
});
describe('on blur threshold value', () => {
- it.only('should resort rows and update indexes', () => {
+ it('should resort rows and update indexes', () => {
const { instance } = setup();
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
index 3361e1bee46..daecff37dc8 100644
--- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
+++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
@@ -3,8 +3,8 @@ import { Threshold } from '../../types';
import { ColorPicker } from '..';
import { PanelOptionsGroup } from '..';
import { colors } from '../../utils';
-import { ThemeContext } from '../../themes/ThemeContext';
-import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
+import { ThemeContext } from '../../themes';
+import { getColorFromHexRgbOrName } from '../../utils';
export interface Props {
thresholds: Threshold[];
@@ -166,7 +166,11 @@ export class ThresholdsEditor extends PureComponent {
{threshold.color && (
- this.onChangeThresholdColor(threshold, color)} />
+ this.onChangeThresholdColor(threshold, color)}
+ enableNamedColors={true}
+ />
)}
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap
index b0dc025090b..2bc5ee56d4d 100644
--- a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap
+++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap
@@ -1,7 +1,450 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render with base threshold 1`] = `
-
-
-
+
+
+
+
+
+ Thresholds
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ hideAfter={300}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index b8c8d66cead..e20a52f6485 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -25,6 +25,7 @@ export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
export { Switch } from './Switch/Switch';
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
export { UnitPicker } from './UnitPicker/UnitPicker';
+export { Input, InputStatus } from './Input/Input';
// Visualizations
export { Gauge } from './Gauge/Gauge';
diff --git a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
index 97ade6da7a5..16f4a20d139 100644
--- a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
+++ b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
@@ -120,7 +120,7 @@ $headings-line-height: ${theme.typography.lineHeight.sm} !default;
$border-width: ${theme.border.width.sm} !default;
$border-radius: ${theme.border.radius.md} !default;
-$border-radius-lg: ${theme.border.radius.lg}!default;
+$border-radius-lg: ${theme.border.radius.lg} !default;
$border-radius-sm: ${theme.border.radius.sm} !default;
// Page
@@ -191,7 +191,6 @@ $btn-padding-y-lg: 11px !default;
$btn-padding-x-xl: 21px !default;
$btn-padding-y-xl: 11px !default;
-
$btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
// sidemenu
diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts
index b36e23ae6c0..ab177eb8888 100644
--- a/packages/grafana-ui/src/types/data.ts
+++ b/packages/grafana-ui/src/types/data.ts
@@ -51,27 +51,13 @@ export enum NullValueMode {
export type TimeSeriesVMs = TimeSeriesVM[];
export interface Column {
- text: string;
- title?: string;
- type?: string;
- sort?: boolean;
- desc?: boolean;
- filterable?: boolean;
+ text: string; // The column name
+ type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove?
+ filterable?: boolean; // currently only set by elasticsearch, and used in the table panel
unit?: string;
}
export interface TableData {
columns: Column[];
rows: any[];
- type: string;
- columnMap: any;
-}
-
-export type SingleStatValue = number | string | null;
-
-/*
- * So we can add meta info like tags & series name
- */
-export interface SingleStatValueInfo {
- value: SingleStatValue;
}
diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts
index b09d88bab4d..c0aede431d0 100644
--- a/packages/grafana-ui/src/types/index.ts
+++ b/packages/grafana-ui/src/types/index.ts
@@ -5,3 +5,4 @@ export * from './plugin';
export * from './datasource';
export * from './theme';
export * from './threshold';
+export * from './input';
diff --git a/public/app/types/form.ts b/packages/grafana-ui/src/types/input.ts
similarity index 100%
rename from public/app/types/form.ts
rename to packages/grafana-ui/src/types/input.ts
diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts
index 889e4ab310e..6c42ecfceb9 100644
--- a/packages/grafana-ui/src/types/panel.ts
+++ b/packages/grafana-ui/src/types/panel.ts
@@ -1,12 +1,12 @@
import { ComponentClass } from 'react';
-import { TimeSeries, LoadingState, TableData } from './data';
+import { LoadingState, TableData } from './data';
import { TimeRange } from './time';
import { ScopedVars } from './datasource';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelProps {
- panelData: PanelData;
+ data?: TableData[];
timeRange: TimeRange;
loading: LoadingState;
options: T;
@@ -16,23 +16,26 @@ export interface PanelProps {
replaceVariables: InterpolateFunction;
}
-export interface PanelData {
- timeSeries?: TimeSeries[];
- tableData?: TableData;
-}
-
export interface PanelEditorProps {
options: T;
onOptionsChange: (options: T) => void;
}
-export type PreservePanelOptionsHandler = (pluginId: string, prevOptions: any) => Partial;
+/**
+ * Called before a panel is initalized
+ */
+export type PanelTypeChangedHook = (
+ options: Partial,
+ prevPluginId?: string,
+ prevOptions?: any
+) => Partial;
export class ReactPanelPlugin {
panel: ComponentClass>;
editor?: ComponentClass>;
defaults?: TOptions;
- preserveOptions?: PreservePanelOptionsHandler;
+
+ panelTypeChangedHook?: PanelTypeChangedHook;
constructor(panel: ComponentClass>) {
this.panel = panel;
@@ -46,8 +49,12 @@ export class ReactPanelPlugin {
this.defaults = defaults;
}
- setPreserveOptionsHandler(handler: PreservePanelOptionsHandler) {
- this.preserveOptions = handler;
+ /**
+ * Called when the visualization changes.
+ * Lets you keep whatever settings made sense in the previous panel
+ */
+ setPanelTypeChangedHook(v: PanelTypeChangedHook) {
+ this.panelTypeChangedHook = v;
}
}
diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts
index e2dda8ad407..bb1794a5154 100644
--- a/packages/grafana-ui/src/types/plugin.ts
+++ b/packages/grafana-ui/src/types/plugin.ts
@@ -91,6 +91,7 @@ export interface PluginMeta {
includes: PluginInclude[];
// Datasource-specific
+ builtIn?: boolean;
metrics?: boolean;
tables?: boolean;
logs?: boolean;
diff --git a/packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap b/packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
index 42e25cb38e4..f894917b7e0 100644
--- a/packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
+++ b/packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
@@ -2,7 +2,6 @@
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
Object {
- "columnMap": Object {},
"columns": Array [
Object {
"text": "Column 1",
@@ -31,13 +30,11 @@ Object {
null,
],
],
- "type": "table",
}
`;
exports[`processTableData basic processing should read header and two rows 1`] = `
Object {
- "columnMap": Object {},
"columns": Array [
Object {
"text": "a",
@@ -61,6 +58,5 @@ Object {
6,
],
],
- "type": "table",
}
`;
diff --git a/packages/grafana-ui/src/utils/displayValue.test.ts b/packages/grafana-ui/src/utils/displayValue.test.ts
new file mode 100644
index 00000000000..bf0e7a89bba
--- /dev/null
+++ b/packages/grafana-ui/src/utils/displayValue.test.ts
@@ -0,0 +1,157 @@
+import { getDisplayProcessor, getColorFromThreshold, DisplayProcessor, DisplayValue } from './displayValue';
+import { MappingType, ValueMapping } from '../types/panel';
+
+function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
+ processors.forEach(processor => {
+ const value = processor(input);
+ expect(value.text).toEqual(match.text);
+ if (match.hasOwnProperty('numeric')) {
+ expect(value.numeric).toEqual(match.numeric);
+ }
+ });
+}
+
+describe('Process simple display values', () => {
+ // Don't test float values here since the decimal formatting changes
+ const processors = [
+ // Without options, this shortcuts to a much easier implementation
+ getDisplayProcessor(),
+
+ // Add a simple option that is not used (uses a different base class)
+ getDisplayProcessor({ color: '#FFF' }),
+
+ // Add a simple option that is not used (uses a different base class)
+ getDisplayProcessor({ unit: 'locale' }),
+ ];
+
+ it('support null', () => {
+ assertSame(null, processors, { text: '', numeric: NaN });
+ });
+
+ it('support undefined', () => {
+ assertSame(undefined, processors, { text: '', numeric: NaN });
+ });
+
+ it('support NaN', () => {
+ assertSame(NaN, processors, { text: 'NaN', numeric: NaN });
+ });
+
+ it('Integer', () => {
+ assertSame(3, processors, { text: '3', numeric: 3 });
+ });
+
+ it('Text to number', () => {
+ assertSame('3', processors, { text: '3', numeric: 3 });
+ });
+
+ it('Simple String', () => {
+ assertSame('hello', processors, { text: 'hello', numeric: NaN });
+ });
+
+ it('empty array', () => {
+ assertSame([], processors, { text: '', numeric: NaN });
+ });
+
+ it('array of text', () => {
+ assertSame(['a', 'b', 'c'], processors, { text: 'a,b,c', numeric: NaN });
+ });
+
+ it('array of numbers', () => {
+ assertSame([1, 2, 3], processors, { text: '1,2,3', numeric: NaN });
+ });
+
+ it('empty object', () => {
+ assertSame({}, processors, { text: '[object Object]', numeric: NaN });
+ });
+
+ it('boolean true', () => {
+ assertSame(true, processors, { text: 'true', numeric: 1 });
+ });
+
+ it('boolean false', () => {
+ assertSame(false, processors, { text: 'false', numeric: 0 });
+ });
+});
+
+describe('Processor with more configs', () => {
+ it('support prefix & suffix', () => {
+ const processor = getDisplayProcessor({
+ prefix: 'AA_',
+ suffix: '_ZZ',
+ });
+
+ expect(processor('XXX').text).toEqual('AA_XXX_ZZ');
+ });
+});
+
+describe('Get color from threshold', () => {
+ it('should get first threshold color when only one threshold', () => {
+ const thresholds = [{ index: 0, value: -Infinity, color: '#7EB26D' }];
+ expect(getColorFromThreshold(49, thresholds)).toEqual('#7EB26D');
+ });
+
+ it('should get the threshold color if value is same as a threshold', () => {
+ const thresholds = [
+ { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 1, value: 50, color: '#EAB839' },
+ { index: 0, value: -Infinity, color: '#7EB26D' },
+ ];
+ expect(getColorFromThreshold(50, thresholds)).toEqual('#EAB839');
+ });
+
+ it('should get the nearest threshold color between thresholds', () => {
+ const thresholds = [
+ { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 1, value: 50, color: '#EAB839' },
+ { index: 0, value: -Infinity, color: '#7EB26D' },
+ ];
+ expect(getColorFromThreshold(55, thresholds)).toEqual('#EAB839');
+ });
+});
+
+describe('Format value', () => {
+ it('should return if value isNaN', () => {
+ const valueMappings: ValueMapping[] = [];
+ const value = 'N/A';
+ const instance = getDisplayProcessor({ mappings: valueMappings });
+
+ const result = instance(value);
+
+ expect(result.text).toEqual('N/A');
+ });
+
+ it('should return formatted value if there are no value mappings', () => {
+ const valueMappings: ValueMapping[] = [];
+ const value = '6';
+
+ const instance = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
+
+ const result = instance(value);
+
+ expect(result.text).toEqual('6.0');
+ });
+
+ it('should return formatted value if there are no matching value mappings', () => {
+ 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 = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
+
+ const result = instance(value);
+
+ expect(result.text).toEqual('10.0');
+ });
+
+ it('should return mapped value if there are matching value mappings', () => {
+ 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 = '11';
+ const instance = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
+
+ expect(instance(value).text).toEqual('1-20');
+ });
+});
diff --git a/packages/grafana-ui/src/utils/displayValue.ts b/packages/grafana-ui/src/utils/displayValue.ts
new file mode 100644
index 00000000000..75191f34434
--- /dev/null
+++ b/packages/grafana-ui/src/utils/displayValue.ts
@@ -0,0 +1,145 @@
+import { ValueMapping, Threshold } from '../types';
+import _ from 'lodash';
+import { getValueFormat, DecimalCount } from './valueFormats/valueFormats';
+import { getMappedValue } from './valueMappings';
+import { GrafanaTheme, GrafanaThemeType } from '../types';
+import { getColorFromHexRgbOrName } from './namedColorsPalette';
+import moment from 'moment';
+
+export interface DisplayValue {
+ text: string; // Show in the UI
+ numeric: number; // Use isNaN to check if it is a real number
+ color?: string; // color based on configs or Threshold
+}
+
+export interface DisplayValueOptions {
+ unit?: string;
+ decimals?: DecimalCount;
+ scaledDecimals?: DecimalCount;
+ dateFormat?: string; // If set try to convert numbers to date
+
+ color?: string;
+ mappings?: ValueMapping[];
+ thresholds?: Threshold[];
+ prefix?: string;
+ suffix?: string;
+
+ // Alternative to empty string
+ noValue?: string;
+
+ // Context
+ isUtc?: boolean;
+ theme?: GrafanaTheme; // Will pick 'dark' if not defined
+}
+
+export type DisplayProcessor = (value: any) => DisplayValue;
+
+export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProcessor {
+ if (options && !_.isEmpty(options)) {
+ const formatFunc = getValueFormat(options.unit || 'none');
+ return (value: any) => {
+ const { prefix, suffix, mappings, thresholds, theme } = options;
+ let color = options.color;
+
+ let text = _.toString(value);
+ let numeric = toNumber(value);
+
+ let shouldFormat = true;
+ if (mappings && mappings.length > 0) {
+ const mappedValue = getMappedValue(mappings, value);
+ if (mappedValue) {
+ text = mappedValue.text;
+ const v = toNumber(text);
+ if (!isNaN(v)) {
+ numeric = v;
+ }
+ shouldFormat = false;
+ }
+ }
+
+ if (options.dateFormat) {
+ const date = toMoment(value, numeric, options.dateFormat);
+ if (date.isValid()) {
+ text = date.format(options.dateFormat);
+ shouldFormat = false;
+ }
+ }
+
+ if (!isNaN(numeric)) {
+ if (shouldFormat && !_.isBoolean(value)) {
+ text = formatFunc(numeric, options.decimals, options.scaledDecimals, options.isUtc);
+ }
+ if (thresholds && thresholds.length > 0) {
+ color = getColorFromThreshold(numeric, thresholds, theme);
+ }
+ }
+
+ if (!text) {
+ text = options.noValue ? options.noValue : '';
+ }
+ if (prefix) {
+ text = prefix + text;
+ }
+ if (suffix) {
+ text = text + suffix;
+ }
+ return { text, numeric, color };
+ };
+ }
+ return toStringProcessor;
+}
+
+function toMoment(value: any, numeric: number, format: string): moment.Moment {
+ if (!isNaN(numeric)) {
+ const v = moment(numeric);
+ if (v.isValid()) {
+ return v;
+ }
+ }
+ const v = moment(value, format);
+ if (v.isValid) {
+ return v;
+ }
+ return moment(value); // moment will try to parse the format
+}
+
+/** Will return any value as a number or NaN */
+function toNumber(value: any): number {
+ if (typeof value === 'number') {
+ return value;
+ }
+ if (value === null || value === undefined || Array.isArray(value)) {
+ return NaN; // lodash calls them 0
+ }
+ if (typeof value === 'boolean') {
+ return value ? 1 : 0;
+ }
+ return _.toNumber(value);
+}
+
+function toStringProcessor(value: any): DisplayValue {
+ return { text: _.toString(value), numeric: toNumber(value) };
+}
+
+export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string {
+ const themeType = theme ? theme.type : GrafanaThemeType.Dark;
+
+ if (thresholds.length === 1) {
+ return getColorFromHexRgbOrName(thresholds[0].color, themeType);
+ }
+
+ const atThreshold = thresholds.filter(threshold => value === threshold.value)[0];
+ if (atThreshold) {
+ return getColorFromHexRgbOrName(atThreshold.color, themeType);
+ }
+
+ const belowThreshold = thresholds.filter(threshold => value > threshold.value);
+
+ if (belowThreshold.length > 0) {
+ const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
+ return getColorFromHexRgbOrName(nearestThreshold.color, themeType);
+ }
+
+ // Use the first threshold as the default color
+ return getColorFromHexRgbOrName(thresholds[0].color, themeType);
+}
diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts
index a08b9ce1a89..7e7c2e30ebf 100644
--- a/packages/grafana-ui/src/utils/index.ts
+++ b/packages/grafana-ui/src/utils/index.ts
@@ -1,9 +1,11 @@
export * from './processTimeSeries';
-export * from './singlestat';
+export * from './processTableData';
export * from './valueFormats/valueFormats';
export * from './colors';
export * from './namedColorsPalette';
export * from './thresholds';
export * from './string';
+export * from './displayValue';
export * from './deprecationWarning';
export { getMappedValue } from './valueMappings';
+export * from './validate';
diff --git a/packages/grafana-ui/src/utils/processTableData.test.ts b/packages/grafana-ui/src/utils/processTableData.test.ts
index fc2545b1706..ba9edcacebd 100644
--- a/packages/grafana-ui/src/utils/processTableData.test.ts
+++ b/packages/grafana-ui/src/utils/processTableData.test.ts
@@ -1,4 +1,4 @@
-import { parseCSV } from './processTableData';
+import { parseCSV, toTableData } from './processTableData';
describe('processTableData', () => {
describe('basic processing', () => {
@@ -18,3 +18,41 @@ describe('processTableData', () => {
});
});
});
+
+describe('toTableData', () => {
+ it('converts timeseries to table skipping nulls', () => {
+ const input1 = {
+ target: 'Field Name',
+ datapoints: [[100, 1], [200, 2]],
+ };
+ const input2 = {
+ // without target
+ target: '',
+ datapoints: [[100, 1], [200, 2]],
+ };
+ const data = toTableData([null, input1, input2, null, null]);
+ expect(data.length).toBe(2);
+ expect(data[0].columns[0].text).toBe(input1.target);
+ expect(data[0].rows).toBe(input1.datapoints);
+
+ // Default name
+ expect(data[1].columns[0].text).toEqual('Value');
+ });
+
+ it('keeps tableData unchanged', () => {
+ const input = {
+ columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
+ rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
+ };
+ const data = toTableData([null, input, null, null]);
+ expect(data.length).toBe(1);
+ expect(data[0]).toBe(input);
+ });
+
+ it('supports null values OK', () => {
+ expect(toTableData([null, null, null, null])).toEqual([]);
+ expect(toTableData(undefined)).toEqual([]);
+ expect(toTableData((null as unknown) as any[])).toEqual([]);
+ expect(toTableData([])).toEqual([]);
+ });
+});
diff --git a/packages/grafana-ui/src/utils/processTableData.ts b/packages/grafana-ui/src/utils/processTableData.ts
index d65e42827e0..d4a91efd622 100644
--- a/packages/grafana-ui/src/utils/processTableData.ts
+++ b/packages/grafana-ui/src/utils/processTableData.ts
@@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
import Papa, { ParseError, ParseMeta } from 'papaparse';
// Types
-import { TableData, Column } from '../types';
+import { TableData, Column, TimeSeries } from '../types';
// Subset of all parse options
export interface TableParseOptions {
@@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
return {
columns,
rows: fixedRows,
- type: table.type,
- columnMap: table.columnMap,
};
}
@@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return {
columns: [],
rows: [],
- type: 'table',
- columnMap: {},
};
}
@@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return matchRowSizes({
columns: makeColumns(header),
rows: results.data,
- type: 'table',
- columnMap: {},
});
}
+function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
+ return {
+ columns: [
+ {
+ text: timeSeries.target || 'Value',
+ unit: timeSeries.unit,
+ },
+ {
+ text: 'Time',
+ type: 'time',
+ unit: 'dateTimeAsIso',
+ },
+ ],
+ rows: timeSeries.datapoints,
+ };
+}
+
+export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
+
+export const toTableData = (results?: any[]): TableData[] => {
+ if (!results) {
+ return [];
+ }
+
+ return results
+ .filter(d => !!d)
+ .map(data => {
+ if (data.hasOwnProperty('columns')) {
+ return data as TableData;
+ }
+ if (data.hasOwnProperty('datapoints')) {
+ return convertTimeSeriesToTableData(data);
+ }
+ // TODO, try to convert JSON to table?
+ console.warn('Can not convert', data);
+ throw new Error('Unsupported data format');
+ });
+};
+
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
if (isNumber(sortIndex)) {
const copy = {
diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts
index f5e9f96efba..9a6aeac6e69 100644
--- a/packages/grafana-ui/src/utils/processTimeSeries.ts
+++ b/packages/grafana-ui/src/utils/processTimeSeries.ts
@@ -4,17 +4,36 @@ import isNumber from 'lodash/isNumber';
import { colors } from './colors';
// Types
-import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
+import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
interface Options {
- timeSeries: TimeSeries[];
+ data: TableData[];
+ xColumn?: number; // Time (or null to guess)
+ yColumn?: number; // Value (or null to guess)
nullValueMode: NullValueMode;
}
-export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
- const vmSeries = timeSeries.map((item, index) => {
+// NOTE: this should move to processTableData.ts
+// I left it as is so the merge changes are more clear.
+export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs {
+ const vmSeries = data.map((item, index) => {
+ if (!isNumber(xColumn)) {
+ xColumn = 1; // Default timeseries colum. TODO, find first time field!
+ }
+ if (!isNumber(yColumn)) {
+ yColumn = 0; // TODO, find first non-time field
+ }
+
+ // TODO? either % or throw error?
+ if (xColumn >= item.columns.length) {
+ throw new Error('invalid colum: ' + xColumn);
+ }
+ if (yColumn >= item.columns.length) {
+ throw new Error('invalid colum: ' + yColumn);
+ }
+
const colorIndex = index % colors.length;
- const label = item.target;
+ const label = item.columns[yColumn].text;
const result = [];
// stat defaults
@@ -42,9 +61,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
let previousValue = 0;
let previousDeltaUp = true;
- for (let i = 0; i < item.datapoints.length; i++) {
- currentValue = item.datapoints[i][0];
- currentTime = item.datapoints[i][1];
+ for (let i = 0; i < item.rows.length; i++) {
+ currentValue = item.rows[i][yColumn];
+ currentTime = item.rows[i][xColumn];
if (typeof currentTime !== 'number') {
continue;
@@ -95,7 +114,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
if (previousValue > currentValue) {
// counter reset
previousDeltaUp = false;
- if (i === item.datapoints.length - 1) {
+ if (i === item.rows.length - 1) {
// reset on last
delta += currentValue;
}
diff --git a/packages/grafana-ui/src/utils/singlestat.ts b/packages/grafana-ui/src/utils/singlestat.ts
deleted file mode 100644
index 5f5fbb8f247..00000000000
--- a/packages/grafana-ui/src/utils/singlestat.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
-import { processTimeSeries } from './processTimeSeries';
-
-export interface SingleStatProcessingOptions {
- panelData: PanelData;
- stat: string;
-}
-
-//
-// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
-//
-export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
- const { panelData, stat } = options;
-
- if (panelData.timeSeries) {
- const timeSeries = processTimeSeries({
- timeSeries: panelData.timeSeries,
- nullValueMode: NullValueMode.Null,
- });
-
- return timeSeries.map((series, index) => {
- const value = stat !== 'name' ? series.stats[stat] : series.label;
-
- return {
- value: value,
- };
- });
- } else if (panelData.tableData) {
- throw { message: 'Panel data not supported' };
- }
-
- return [];
-}
diff --git a/packages/grafana-ui/src/utils/validate.ts b/packages/grafana-ui/src/utils/validate.ts
new file mode 100644
index 00000000000..286ec700577
--- /dev/null
+++ b/packages/grafana-ui/src/utils/validate.ts
@@ -0,0 +1,24 @@
+import { ValidationRule, ValidationEvents } from '../types/input';
+
+export enum EventsWithValidation {
+ onBlur = 'onBlur',
+ onFocus = 'onFocus',
+ onChange = 'onChange',
+}
+
+export const validate = (value: string, validationRules: ValidationRule[]) => {
+ const errors = validationRules.reduce(
+ (acc, currRule) => {
+ if (!currRule.rule(value)) {
+ return acc.concat(currRule.errorMessage);
+ }
+ return acc;
+ },
+ [] as string[]
+ );
+ return errors.length > 0 ? errors : null;
+};
+
+export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents | undefined) => {
+ return validationEvents && validationEvents[event];
+};
diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go
index c16c2f126f8..4ad8a2b84ab 100644
--- a/pkg/api/admin_users.go
+++ b/pkg/api/admin_users.go
@@ -110,3 +110,26 @@ func AdminDeleteUser(c *m.ReqContext) {
c.JsonOK("User deleted")
}
+
+// POST /api/admin/users/:id/logout
+func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response {
+ userID := c.ParamsInt64(":id")
+
+ if c.UserId == userID {
+ return Error(400, "You cannot logout yourself", nil)
+ }
+
+ return server.logoutUserFromAllDevicesInternal(userID)
+}
+
+// GET /api/admin/users/:id/auth-tokens
+func (server *HTTPServer) AdminGetUserAuthTokens(c *m.ReqContext) Response {
+ userID := c.ParamsInt64(":id")
+ return server.getUserAuthTokensInternal(c, userID)
+}
+
+// POST /api/admin/users/:id/revoke-auth-token
+func (server *HTTPServer) AdminRevokeUserAuthToken(c *m.ReqContext, cmd m.RevokeAuthTokenCmd) Response {
+ userID := c.ParamsInt64(":id")
+ return server.revokeUserAuthTokenInternal(c, userID, cmd)
+}
diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go
index 0b94a64b3fb..b94f09b0b75 100644
--- a/pkg/api/admin_users_test.go
+++ b/pkg/api/admin_users_test.go
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/auth"
. "github.com/smartystreets/goconvey/convey"
)
@@ -27,6 +28,62 @@ func TestAdminApiEndpoint(t *testing.T) {
So(sc.resp.Code, ShouldEqual, 400)
})
})
+
+ Convey("When a server admin attempts to logout himself from all devices", t, func() {
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ cmd.Result = &m.User{Id: TestUserID}
+ return nil
+ })
+
+ adminLogoutUserScenario("Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 400)
+ })
+ })
+
+ Convey("When a server admin attempts to logout a non-existing user from all devices", t, func() {
+ userId := int64(0)
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ userId = cmd.Id
+ return m.ErrUserNotFound
+ })
+
+ adminLogoutUserScenario("Should return not found when calling POST on", "/api/admin/users/200/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 404)
+ So(userId, ShouldEqual, 200)
+ })
+ })
+
+ Convey("When a server admin attempts to revoke an auth token for a non-existing user", t, func() {
+ userId := int64(0)
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ userId = cmd.Id
+ return m.ErrUserNotFound
+ })
+
+ cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+
+ adminRevokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) {
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 404)
+ So(userId, ShouldEqual, 200)
+ })
+ })
+
+ Convey("When a server admin gets auth tokens for a non-existing user", t, func() {
+ userId := int64(0)
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ userId = cmd.Id
+ return m.ErrUserNotFound
+ })
+
+ adminGetUserAuthTokensScenario("Should return not found when calling GET on", "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) {
+ sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 404)
+ So(userId, ShouldEqual, 200)
+ })
+ })
}
func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
@@ -48,3 +105,84 @@ func putAdminScenario(desc string, url string, routePattern string, role m.RoleT
fn(sc)
})
}
+
+func adminLogoutUserScenario(desc string, url string, routePattern string, fn scenarioFunc) {
+ Convey(desc+" "+url, func() {
+ defer bus.ClearBusHandlers()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: auth.NewFakeUserAuthTokenService(),
+ }
+
+ sc := setupScenarioContext(url)
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = TestUserID
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+
+ return hs.AdminLogoutUser(c)
+ })
+
+ sc.m.Post(routePattern, sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
+func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, fn scenarioFunc) {
+ Convey(desc+" "+url, func() {
+ defer bus.ClearBusHandlers()
+
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: fakeAuthTokenService,
+ }
+
+ sc := setupScenarioContext(url)
+ sc.userAuthTokenService = fakeAuthTokenService
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = TestUserID
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+
+ return hs.AdminRevokeUserAuthToken(c, cmd)
+ })
+
+ sc.m.Post(routePattern, sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
+func adminGetUserAuthTokensScenario(desc string, url string, routePattern string, fn scenarioFunc) {
+ Convey(desc+" "+url, func() {
+ defer bus.ClearBusHandlers()
+
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: fakeAuthTokenService,
+ }
+
+ sc := setupScenarioContext(url)
+ sc.userAuthTokenService = fakeAuthTokenService
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = TestUserID
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+
+ return hs.AdminGetUserAuthTokens(c)
+ })
+
+ sc.m.Get(routePattern, sc.defaultHandler)
+
+ fn(sc)
+ })
+}
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 81ea83eae61..86bc83f558b 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -14,6 +14,7 @@ func (hs *HTTPServer) registerRoutes() {
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
+ reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
quota := middleware.Quota(hs.QuotaService)
@@ -41,8 +42,8 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/org/users", reqOrgAdmin, hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
- r.Get("/org/teams", reqOrgAdmin, hs.Index)
- r.Get("/org/teams/*", reqOrgAdmin, hs.Index)
+ r.Get("/org/teams", reqCanAccessTeams, hs.Index)
+ r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
@@ -133,6 +134,9 @@ func (hs *HTTPServer) registerRoutes() {
userRoute.Get("/preferences", Wrap(GetUserPreferences))
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
+
+ userRoute.Get("/auth-tokens", Wrap(hs.GetUserAuthTokens))
+ userRoute.Post("/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.RevokeUserAuthToken))
})
// users (admin permission required)
@@ -150,20 +154,21 @@ func (hs *HTTPServer) registerRoutes() {
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
- teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(CreateTeam))
- teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam))
- teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID))
+ teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam))
+ teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam))
+ teamsRoute.Delete("/:teamId", Wrap(hs.DeleteTeamByID))
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
- teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
- teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
- teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences))
- teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences))
- }, reqOrgAdmin)
+ teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember))
+ teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember))
+ teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember))
+ teamsRoute.Get("/:teamId/preferences", Wrap(hs.GetTeamPreferences))
+ teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(hs.UpdateTeamPreferences))
+ }, reqCanAccessTeams)
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
- teamsRoute.Get("/search", Wrap(SearchTeams))
+ teamsRoute.Get("/search", Wrap(hs.SearchTeams))
})
// org information available to all users.
@@ -262,7 +267,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
folderRoute.Get("/", Wrap(GetFolders))
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
- folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(CreateFolder))
+ folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", Wrap(GetFolderByUID))
@@ -375,6 +380,10 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
adminRoute.Get("/stats", AdminGetStats)
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
+
+ adminRoute.Post("/users/:id/logout", Wrap(hs.AdminLogoutUser))
+ adminRoute.Get("/users/:id/auth-tokens", Wrap(hs.AdminGetUserAuthTokens))
+ adminRoute.Post("/users/:id/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken))
}, reqGrafanaAdmin)
// rendering
diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go
index d77d9d87b4a..abee55711b0 100644
--- a/pkg/api/app_routes.go
+++ b/pkg/api/app_routes.go
@@ -48,18 +48,18 @@ func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
}
}
- handlers = append(handlers, AppPluginRoute(route, plugin.Id))
+ handlers = append(handlers, AppPluginRoute(route, plugin.Id, hs))
r.Route(url, route.Method, handlers...)
log.Debug("Plugins: Adding proxy route %s", url)
}
}
}
-func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler {
+func AppPluginRoute(route *plugins.AppPluginRoute, appID string, hs *HTTPServer) macaron.Handler {
return func(c *m.ReqContext) {
path := c.Params("*")
- proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID)
+ proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID, hs.Cfg)
proxy.Transport = pluginProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
}
diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go
index 3f3a50aae69..4e0b0dcd998 100644
--- a/pkg/api/common_test.go
+++ b/pkg/api/common_test.go
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/auth"
"gopkg.in/macaron.v1"
. "github.com/smartystreets/goconvey/convey"
@@ -94,13 +95,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 *auth.FakeUserAuthTokenService
}
func (sc *scenarioContext) exec() {
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 07c4f75778d..c47e8f31ccc 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -213,7 +213,8 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand)
dash := cmd.GetDashboardModel()
- if dash.Id == 0 && dash.Uid == "" {
+ newDashboard := dash.Id == 0 && dash.Uid == ""
+ if newDashboard {
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
if err != nil {
return Error(500, "failed to get quota", err)
@@ -276,6 +277,15 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand)
return Error(500, "Failed to save dashboard", err)
}
+ if hs.Cfg.EditorsCanAdmin && newDashboard {
+ inFolder := cmd.FolderId > 0
+ err := dashboards.MakeUserAdmin(hs.Bus, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
+ if err != nil {
+ hs.log.Error("Could not make user admin", "dashboard", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err)
+ return Error(500, "Failed to make user admin of dashboard", err)
+ }
+ }
+
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return JSON(200, util.DynMap{
"status": "success",
diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go
index 923bf57ce8a..ea69c049115 100644
--- a/pkg/api/dashboard_test.go
+++ b/pkg/api/dashboard_test.go
@@ -974,6 +974,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
hs := HTTPServer{
Bus: bus.GetBus(),
+ Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(url)
@@ -1024,6 +1025,7 @@ func restoreDashboardVersionScenario(desc string, url string, routePattern strin
defer bus.ClearBusHandlers()
hs := HTTPServer{
+ Cfg: setting.NewCfg(),
Bus: bus.GetBus(),
}
diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go
index 54a744fccdc..48f9c73c934 100644
--- a/pkg/api/dataproxy.go
+++ b/pkg/api/dataproxy.go
@@ -31,7 +31,7 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
// macaron does not include trailing slashes when resolving a wildcard path
proxyPath := ensureProxyPathTrailingSlash(c.Req.URL.Path, c.Params("*"))
- proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
+ proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath, hs.Cfg)
proxy.HandleRequest()
}
diff --git a/pkg/api/dtos/user_token.go b/pkg/api/dtos/user_token.go
new file mode 100644
index 00000000000..1542421e2f6
--- /dev/null
+++ b/pkg/api/dtos/user_token.go
@@ -0,0 +1,12 @@
+package dtos
+
+import "time"
+
+type UserToken struct {
+ Id int64 `json:"id"`
+ IsActive bool `json:"isActive"`
+ ClientIp string `json:"clientIp"`
+ UserAgent string `json:"userAgent"`
+ CreatedAt time.Time `json:"createdAt"`
+ SeenAt time.Time `json:"seenAt"`
+}
diff --git a/pkg/api/folder.go b/pkg/api/folder.go
index 0e08343b556..0a9a2671071 100644
--- a/pkg/api/folder.go
+++ b/pkg/api/folder.go
@@ -54,13 +54,20 @@ func GetFolderByID(c *m.ReqContext) Response {
return JSON(200, toFolderDto(g, folder))
}
-func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
+func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
err := s.CreateFolder(&cmd)
if err != nil {
return toFolderError(err)
}
+ if hs.Cfg.EditorsCanAdmin {
+ if err := dashboards.MakeUserAdmin(hs.Bus, c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil {
+ hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err)
+ return Error(500, "Failed to make user admin of folder", err)
+ }
+ }
+
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return JSON(200, toFolderDto(g, cmd.Result))
}
diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go
index 880de338c8f..5e7184ae0c9 100644
--- a/pkg/api/folder_test.go
+++ b/pkg/api/folder_test.go
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -141,12 +142,17 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ Cfg: setting.NewCfg(),
+ }
+
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
- return CreateFolder(c, cmd)
+ return hs.CreateFolder(c, cmd)
})
origNewFolderService := dashboards.NewFolderService
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 67a511b8b4d..cd61c2f3ebc 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -167,7 +167,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"viewersCanEdit": setting.ViewersCanEdit,
- "editorsCanOwn": hs.Cfg.EditorsCanOwn,
+ "editorsCanAdmin": hs.Cfg.EditorsCanAdmin,
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 904a885b171..e7555e14621 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -327,6 +327,27 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
})
}
+ if (c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_VIEWER) && hs.Cfg.EditorsCanAdmin {
+ cfgNode := &dtos.NavLink{
+ Id: "cfg",
+ Text: "Configuration",
+ SubTitle: "Organization: " + c.OrgName,
+ Icon: "gicon gicon-cog",
+ Url: setting.AppSubUrl + "/org/teams",
+ Children: []*dtos.NavLink{
+ {
+ Text: "Teams",
+ Id: "teams",
+ Description: "Manage org groups",
+ Icon: "gicon gicon-team",
+ Url: setting.AppSubUrl + "/org/teams",
+ },
+ },
+ }
+
+ data.NavTree = append(data.NavTree, cfgNode)
+ }
+
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index b1950998297..3aec988f9e3 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -34,13 +34,14 @@ type DataSourceProxy struct {
proxyPath string
route *plugins.AppPluginRoute
plugin *plugins.DataSourcePlugin
+ cfg *setting.Cfg
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
-func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
+func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string, cfg *setting.Cfg) *DataSourceProxy {
targetURL, _ := url.Parse(ds.Url)
return &DataSourceProxy{
@@ -49,6 +50,7 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
ctx: ctx,
proxyPath: proxyPath,
targetUrl: targetURL,
+ cfg: cfg,
}
}
@@ -170,6 +172,10 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Add("Authorization", dsAuth)
}
+ if proxy.cfg.SendUserHeader && !proxy.ctx.SignedInUser.IsAnonymous {
+ req.Header.Add("X-Grafana-User", proxy.ctx.SignedInUser.Login)
+ }
+
// clear cookie header, except for whitelisted cookies
var keptCookies []*http.Cookie
if proxy.ds.JsonData != nil {
diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index c9be169565f..bfad7d5670d 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -81,7 +81,7 @@ func TestDSRouteRule(t *testing.T) {
}
Convey("When matching route path", func() {
- proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{})
proxy.route = plugin.Routes[0]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
@@ -92,7 +92,7 @@ func TestDSRouteRule(t *testing.T) {
})
Convey("When matching route path and has dynamic url", func() {
- proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", &setting.Cfg{})
proxy.route = plugin.Routes[3]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
@@ -104,20 +104,20 @@ func TestDSRouteRule(t *testing.T) {
Convey("Validating request", func() {
Convey("plugin route with valid role", func() {
- proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{})
err := proxy.validateRequest()
So(err, ShouldBeNil)
})
Convey("plugin route with admin role and user is editor", func() {
- proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{})
err := proxy.validateRequest()
So(err, ShouldNotBeNil)
})
Convey("plugin route with admin role and user is admin", func() {
ctx.SignedInUser.OrgRole = m.ROLE_ADMIN
- proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{})
err := proxy.validateRequest()
So(err, ShouldBeNil)
})
@@ -186,7 +186,7 @@ func TestDSRouteRule(t *testing.T) {
So(err, ShouldBeNil)
client = newFakeHTTPClient(json)
- proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
+ proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{})
proxy1.route = plugin.Routes[0]
ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds)
@@ -200,7 +200,7 @@ func TestDSRouteRule(t *testing.T) {
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
client = newFakeHTTPClient(json2)
- proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2")
+ proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", &setting.Cfg{})
proxy2.route = plugin.Routes[1]
ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds)
@@ -215,7 +215,7 @@ func TestDSRouteRule(t *testing.T) {
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
client = newFakeHTTPClient([]byte{})
- proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
+ proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{})
proxy3.route = plugin.Routes[0]
ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds)
@@ -236,7 +236,7 @@ func TestDSRouteRule(t *testing.T) {
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
ctx := &m.ReqContext{}
- proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
So(err, ShouldBeNil)
@@ -261,7 +261,7 @@ func TestDSRouteRule(t *testing.T) {
}
ctx := &m.ReqContext{}
- proxy := NewDataSourceProxy(ds, plugin, ctx, "")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
So(err, ShouldBeNil)
@@ -291,7 +291,7 @@ func TestDSRouteRule(t *testing.T) {
}
ctx := &m.ReqContext{}
- proxy := NewDataSourceProxy(ds, plugin, ctx, "")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL, Header: make(http.Header)}
@@ -317,7 +317,7 @@ func TestDSRouteRule(t *testing.T) {
}
ctx := &m.ReqContext{}
- proxy := NewDataSourceProxy(ds, plugin, ctx, "")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL, Header: make(http.Header)}
@@ -347,7 +347,7 @@ func TestDSRouteRule(t *testing.T) {
}
ctx := &m.ReqContext{}
- proxy := NewDataSourceProxy(ds, plugin, ctx, "")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL, Header: make(http.Header)}
@@ -369,7 +369,7 @@ func TestDSRouteRule(t *testing.T) {
Url: "http://host/root/",
}
ctx := &m.ReqContext{}
- proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/")
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{})
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req.Header.Add("Origin", "grafana.com")
req.Header.Add("Referer", "grafana.com")
@@ -388,9 +388,68 @@ func TestDSRouteRule(t *testing.T) {
So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere")
})
})
+
+ Convey("When SendUserHeader config is enabled", func() {
+ req := getDatasourceProxiedRequest(
+ &m.ReqContext{
+ SignedInUser: &m.SignedInUser{
+ Login: "test_user",
+ },
+ },
+ &setting.Cfg{SendUserHeader: true},
+ )
+ Convey("Should add header with username", func() {
+ So(req.Header.Get("X-Grafana-User"), ShouldEqual, "test_user")
+ })
+ })
+
+ Convey("When SendUserHeader config is disabled", func() {
+ req := getDatasourceProxiedRequest(
+ &m.ReqContext{
+ SignedInUser: &m.SignedInUser{
+ Login: "test_user",
+ },
+ },
+ &setting.Cfg{SendUserHeader: false},
+ )
+ Convey("Should not add header with username", func() {
+ // Get will return empty string even if header is not set
+ So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
+ })
+ })
+
+ Convey("When SendUserHeader config is enabled but user is anonymous", func() {
+ req := getDatasourceProxiedRequest(
+ &m.ReqContext{
+ SignedInUser: &m.SignedInUser{IsAnonymous: true},
+ },
+ &setting.Cfg{SendUserHeader: true},
+ )
+ Convey("Should not add header with username", func() {
+ // Get will return empty string even if header is not set
+ So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
+ })
+ })
})
}
+// getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
+func getDatasourceProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request {
+ plugin := &plugins.DataSourcePlugin{}
+
+ ds := &m.DataSource{
+ Type: "custom",
+ Url: "http://host/root/",
+ }
+
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "", cfg)
+ req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+ So(err, ShouldBeNil)
+
+ proxy.getDirector()(req)
+ return req
+}
+
type httpClientStub struct {
fakeBody []byte
}
diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go
index 4cf1dbd7cde..5ee59017a82 100644
--- a/pkg/api/pluginproxy/pluginproxy.go
+++ b/pkg/api/pluginproxy/pluginproxy.go
@@ -2,6 +2,7 @@ package pluginproxy
import (
"encoding/json"
+ "github.com/grafana/grafana/pkg/setting"
"net"
"net/http"
"net/http/httputil"
@@ -37,7 +38,7 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.
return result, err
}
-func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string) *httputil.ReverseProxy {
+func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy {
targetURL, _ := url.Parse(route.Url)
director := func(req *http.Request) {
@@ -79,6 +80,10 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
req.Header.Add("X-Grafana-Context", string(ctxJson))
+ if cfg.SendUserHeader && !ctx.SignedInUser.IsAnonymous {
+ req.Header.Add("X-Grafana-User", ctx.SignedInUser.Login)
+ }
+
if len(route.Headers) > 0 {
headers, err := getHeaders(route, ctx.OrgId, appID)
if err != nil {
diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go
index 424c3fd670c..e4a4fdb25ba 100644
--- a/pkg/api/pluginproxy/pluginproxy_test.go
+++ b/pkg/api/pluginproxy/pluginproxy_test.go
@@ -1,6 +1,7 @@
package pluginproxy
import (
+ "net/http"
"testing"
"github.com/grafana/grafana/pkg/bus"
@@ -44,4 +45,59 @@ func TestPluginProxy(t *testing.T) {
})
})
+ Convey("When SendUserHeader config is enabled", t, func() {
+ req := getPluginProxiedRequest(
+ &m.ReqContext{
+ SignedInUser: &m.SignedInUser{
+ Login: "test_user",
+ },
+ },
+ &setting.Cfg{SendUserHeader: true},
+ )
+
+ Convey("Should add header with username", func() {
+ // Get will return empty string even if header is not set
+ So(req.Header.Get("X-Grafana-User"), ShouldEqual, "test_user")
+ })
+ })
+
+ Convey("When SendUserHeader config is disabled", t, func() {
+ req := getPluginProxiedRequest(
+ &m.ReqContext{
+ SignedInUser: &m.SignedInUser{
+ Login: "test_user",
+ },
+ },
+ &setting.Cfg{SendUserHeader: false},
+ )
+ Convey("Should not add header with username", func() {
+ // Get will return empty string even if header is not set
+ So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
+ })
+ })
+
+ Convey("When SendUserHeader config is enabled but user is anonymous", t, func() {
+ req := getPluginProxiedRequest(
+ &m.ReqContext{
+ SignedInUser: &m.SignedInUser{IsAnonymous: true},
+ },
+ &setting.Cfg{SendUserHeader: true},
+ )
+
+ Convey("Should not add header with username", func() {
+ // Get will return empty string even if header is not set
+ So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
+ })
+ })
+}
+
+// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
+func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request {
+ route := &plugins.AppPluginRoute{}
+ proxy := NewApiPluginProxy(ctx, "", route, "", cfg)
+
+ req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+ So(err, ShouldBeNil)
+ proxy.Director(req)
+ return req
}
diff --git a/pkg/api/team.go b/pkg/api/team.go
index 32265e5d018..ecfd8028c1b 100644
--- a/pkg/api/team.go
+++ b/pkg/api/team.go
@@ -4,19 +4,38 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/teamguardian"
"github.com/grafana/grafana/pkg/util"
)
// POST /api/teams
-func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
+func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
cmd.OrgId = c.OrgId
- if err := bus.Dispatch(&cmd); err != nil {
+
+ if c.OrgRole == m.ROLE_VIEWER {
+ return Error(403, "Not allowed to create team.", nil)
+ }
+
+ if err := hs.Bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
return Error(409, "Team name taken", err)
}
return Error(500, "Failed to create Team", err)
}
+ if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanAdmin {
+ addMemberCmd := m.AddTeamMemberCommand{
+ UserId: c.SignedInUser.UserId,
+ OrgId: cmd.OrgId,
+ TeamId: cmd.Result.Id,
+ Permission: m.PERMISSION_ADMIN,
+ }
+
+ if err := hs.Bus.Dispatch(&addMemberCmd); err != nil {
+ c.Logger.Error("Could not add creator to team.", "error", err)
+ }
+ }
+
return JSON(200, &util.DynMap{
"teamId": cmd.Result.Id,
"message": "Team created",
@@ -24,10 +43,15 @@ func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
}
// PUT /api/teams/:teamId
-func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
+func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":teamId")
- if err := bus.Dispatch(&cmd); err != nil {
+
+ if err := teamguardian.CanAdmin(hs.Bus, cmd.OrgId, cmd.Id, c.SignedInUser); err != nil {
+ return Error(403, "Not allowed to update team", err)
+ }
+
+ if err := hs.Bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
return Error(400, "Team name taken", err)
}
@@ -38,18 +62,26 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
}
// DELETE /api/teams/:teamId
-func DeleteTeamByID(c *m.ReqContext) Response {
- if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
+func (hs *HTTPServer) DeleteTeamByID(c *m.ReqContext) Response {
+ orgId := c.OrgId
+ teamId := c.ParamsInt64(":teamId")
+ user := c.SignedInUser
+
+ if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, user); err != nil {
+ return Error(403, "Not allowed to delete team", err)
+ }
+
+ if err := hs.Bus.Dispatch(&m.DeleteTeamCommand{OrgId: orgId, Id: teamId}); err != nil {
if err == m.ErrTeamNotFound {
return Error(404, "Failed to delete Team. ID not found", nil)
}
- return Error(500, "Failed to update Team", err)
+ return Error(500, "Failed to delete Team", err)
}
return Success("Team deleted")
}
// GET /api/teams/search
-func SearchTeams(c *m.ReqContext) Response {
+func (hs *HTTPServer) SearchTeams(c *m.ReqContext) Response {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
@@ -59,12 +91,18 @@ func SearchTeams(c *m.ReqContext) Response {
page = 1
}
+ var userIdFilter int64
+ if hs.Cfg.EditorsCanAdmin && c.OrgRole != m.ROLE_ADMIN {
+ userIdFilter = c.SignedInUser.UserId
+ }
+
query := m.SearchTeamsQuery{
- OrgId: c.OrgId,
- Query: c.Query("query"),
- Name: c.Query("name"),
- Page: page,
- Limit: perPage,
+ OrgId: c.OrgId,
+ Query: c.Query("query"),
+ Name: c.Query("name"),
+ UserIdFilter: userIdFilter,
+ Page: page,
+ Limit: perPage,
}
if err := bus.Dispatch(&query); err != nil {
@@ -98,11 +136,25 @@ func GetTeamByID(c *m.ReqContext) Response {
}
// GET /api/teams/:teamId/preferences
-func GetTeamPreferences(c *m.ReqContext) Response {
- return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"))
+func (hs *HTTPServer) GetTeamPreferences(c *m.ReqContext) Response {
+ teamId := c.ParamsInt64(":teamId")
+ orgId := c.OrgId
+
+ if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
+ return Error(403, "Not allowed to view team preferences.", err)
+ }
+
+ return getPreferencesFor(orgId, 0, teamId)
}
// PUT /api/teams/:teamId/preferences
-func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
- return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd)
+func (hs *HTTPServer) UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
+ teamId := c.ParamsInt64(":teamId")
+ orgId := c.OrgId
+
+ if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
+ return Error(403, "Not allowed to update team preferences.", err)
+ }
+
+ return updatePreferencesFor(orgId, 0, teamId, &dtoCmd)
}
diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go
index 5b5970de6ad..54a4d8220e5 100644
--- a/pkg/api/team_members.go
+++ b/pkg/api/team_members.go
@@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/teamguardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -29,11 +30,15 @@ func GetTeamMembers(c *m.ReqContext) Response {
}
// POST /api/teams/:teamId/members
-func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
- cmd.TeamId = c.ParamsInt64(":teamId")
+func (hs *HTTPServer) AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
cmd.OrgId = c.OrgId
+ cmd.TeamId = c.ParamsInt64(":teamId")
- if err := bus.Dispatch(&cmd); err != nil {
+ if err := teamguardian.CanAdmin(hs.Bus, cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil {
+ return Error(403, "Not allowed to add team member", err)
+ }
+
+ if err := hs.Bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNotFound {
return Error(404, "Team not found", nil)
}
@@ -50,9 +55,48 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
})
}
+// PUT /:teamId/members/:userId
+func (hs *HTTPServer) UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response {
+ teamId := c.ParamsInt64(":teamId")
+ orgId := c.OrgId
+
+ if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
+ return Error(403, "Not allowed to update team member", err)
+ }
+
+ if c.OrgRole != m.ROLE_ADMIN {
+ cmd.ProtectLastAdmin = true
+ }
+
+ cmd.TeamId = teamId
+ cmd.UserId = c.ParamsInt64(":userId")
+ cmd.OrgId = orgId
+
+ if err := hs.Bus.Dispatch(&cmd); err != nil {
+ if err == m.ErrTeamMemberNotFound {
+ return Error(404, "Team member not found.", nil)
+ }
+ return Error(500, "Failed to update team member.", err)
+ }
+ return Success("Team member updated")
+}
+
// DELETE /api/teams/:teamId/members/:userId
-func RemoveTeamMember(c *m.ReqContext) Response {
- if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
+func (hs *HTTPServer) RemoveTeamMember(c *m.ReqContext) Response {
+ orgId := c.OrgId
+ teamId := c.ParamsInt64(":teamId")
+ userId := c.ParamsInt64(":userId")
+
+ if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
+ return Error(403, "Not allowed to remove team member", err)
+ }
+
+ protectLastAdmin := false
+ if c.OrgRole != m.ROLE_ADMIN {
+ protectLastAdmin = true
+ }
+
+ if err := hs.Bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId, ProtectLastAdmin: protectLastAdmin}); err != nil {
if err == m.ErrTeamNotFound {
return Error(404, "Team not found", nil)
}
diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go
index a1984288870..cab59cc5f98 100644
--- a/pkg/api/team_test.go
+++ b/pkg/api/team_test.go
@@ -3,6 +3,8 @@ package api
import (
"testing"
+ "github.com/grafana/grafana/pkg/setting"
+
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
@@ -20,6 +22,10 @@ func TestTeamApiEndpoint(t *testing.T) {
TotalCount: 2,
}
+ hs := &HTTPServer{
+ Cfg: setting.NewCfg(),
+ }
+
Convey("When searching with no parameters", func() {
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
var sentLimit int
@@ -33,7 +39,7 @@ func TestTeamApiEndpoint(t *testing.T) {
return nil
})
- sc.handlerFunc = SearchTeams
+ sc.handlerFunc = hs.SearchTeams
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
@@ -60,7 +66,7 @@ func TestTeamApiEndpoint(t *testing.T) {
return nil
})
- sc.handlerFunc = SearchTeams
+ sc.handlerFunc = hs.SearchTeams
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go
new file mode 100644
index 00000000000..2f74eedea5d
--- /dev/null
+++ b/pkg/api/user_token.go
@@ -0,0 +1,110 @@
+package api
+
+import (
+ "time"
+
+ "github.com/grafana/grafana/pkg/api/dtos"
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/util"
+)
+
+// GET /api/user/auth-tokens
+func (server *HTTPServer) GetUserAuthTokens(c *models.ReqContext) Response {
+ return server.getUserAuthTokensInternal(c, c.UserId)
+}
+
+// POST /api/user/revoke-auth-token
+func (server *HTTPServer) RevokeUserAuthToken(c *models.ReqContext, cmd models.RevokeAuthTokenCmd) Response {
+ return server.revokeUserAuthTokenInternal(c, c.UserId, cmd)
+}
+
+func (server *HTTPServer) logoutUserFromAllDevicesInternal(userID int64) Response {
+ userQuery := models.GetUserByIdQuery{Id: userID}
+
+ if err := bus.Dispatch(&userQuery); err != nil {
+ if err == models.ErrUserNotFound {
+ return Error(404, "User not found", err)
+ }
+ return Error(500, "Could not read user from database", err)
+ }
+
+ err := server.AuthTokenService.RevokeAllUserTokens(userID)
+ if err != nil {
+ return Error(500, "Failed to logout user", err)
+ }
+
+ return JSON(200, util.DynMap{
+ "message": "User logged out",
+ })
+}
+
+func (server *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) Response {
+ userQuery := models.GetUserByIdQuery{Id: userID}
+
+ if err := bus.Dispatch(&userQuery); err != nil {
+ if err == models.ErrUserNotFound {
+ return Error(404, "User not found", err)
+ }
+ return Error(500, "Failed to get user", err)
+ }
+
+ tokens, err := server.AuthTokenService.GetUserTokens(userID)
+ if err != nil {
+ return Error(500, "Failed to get user auth tokens", err)
+ }
+
+ result := []*dtos.UserToken{}
+ for _, token := range tokens {
+ isActive := false
+ if c.UserToken != nil && c.UserToken.Id == token.Id {
+ isActive = true
+ }
+
+ result = append(result, &dtos.UserToken{
+ Id: token.Id,
+ IsActive: isActive,
+ ClientIp: token.ClientIp,
+ UserAgent: token.UserAgent,
+ CreatedAt: time.Unix(token.CreatedAt, 0),
+ SeenAt: time.Unix(token.SeenAt, 0),
+ })
+ }
+
+ return JSON(200, result)
+}
+
+func (server *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) Response {
+ userQuery := models.GetUserByIdQuery{Id: userID}
+
+ if err := bus.Dispatch(&userQuery); err != nil {
+ if err == models.ErrUserNotFound {
+ return Error(404, "User not found", err)
+ }
+ return Error(500, "Failed to get user", err)
+ }
+
+ token, err := server.AuthTokenService.GetUserToken(userID, cmd.AuthTokenId)
+ if err != nil {
+ if err == models.ErrUserTokenNotFound {
+ return Error(404, "User auth token not found", err)
+ }
+ return Error(500, "Failed to get user auth token", err)
+ }
+
+ if c.UserToken != nil && c.UserToken.Id == token.Id {
+ return Error(400, "Cannot revoke active user auth token", nil)
+ }
+
+ err = server.AuthTokenService.RevokeToken(token)
+ if err != nil {
+ if err == models.ErrUserTokenNotFound {
+ return Error(404, "User auth token not found", err)
+ }
+ return Error(500, "Failed to revoke user auth token", err)
+ }
+
+ return JSON(200, util.DynMap{
+ "message": "User auth token revoked",
+ })
+}
diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go
new file mode 100644
index 00000000000..111070dca92
--- /dev/null
+++ b/pkg/api/user_token_test.go
@@ -0,0 +1,294 @@
+package api
+
+import (
+ "testing"
+ "time"
+
+ "github.com/grafana/grafana/pkg/bus"
+ m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/auth"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserTokenApiEndpoint(t *testing.T) {
+ Convey("When current user attempts to revoke an auth token for a non-existing user", t, func() {
+ userId := int64(0)
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ userId = cmd.Id
+ return m.ErrUserNotFound
+ })
+
+ cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+
+ revokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) {
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 404)
+ So(userId, ShouldEqual, 200)
+ })
+ })
+
+ Convey("When current user gets auth tokens for a non-existing user", t, func() {
+ userId := int64(0)
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ userId = cmd.Id
+ return m.ErrUserNotFound
+ })
+
+ getUserAuthTokensScenario("Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) {
+ sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 404)
+ So(userId, ShouldEqual, 200)
+ })
+ })
+
+ Convey("When logout an existing user from all devices", t, func() {
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ cmd.Result = &m.User{Id: 200}
+ return nil
+ })
+
+ logoutUserFromAllDevicesInternalScenario("Should be successful", 1, func(sc *scenarioContext) {
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 200)
+ })
+ })
+
+ Convey("When logout a non-existing user from all devices", t, func() {
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ return m.ErrUserNotFound
+ })
+
+ logoutUserFromAllDevicesInternalScenario("Should return not found", TestUserID, func(sc *scenarioContext) {
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 404)
+ })
+ })
+
+ Convey("When revoke an auth token for a user", t, func() {
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ cmd.Result = &m.User{Id: 200}
+ return nil
+ })
+
+ cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+ token := &m.UserToken{Id: 1}
+
+ revokeUserAuthTokenInternalScenario("Should be successful", cmd, 200, token, func(sc *scenarioContext) {
+ sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) {
+ return &m.UserToken{Id: 2}, nil
+ }
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 200)
+ })
+ })
+
+ Convey("When revoke the active auth token used by himself", t, func() {
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ cmd.Result = &m.User{Id: TestUserID}
+ return nil
+ })
+
+ cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+ token := &m.UserToken{Id: 2}
+
+ revokeUserAuthTokenInternalScenario("Should not be successful", cmd, TestUserID, token, func(sc *scenarioContext) {
+ sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) {
+ return token, nil
+ }
+ sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+ So(sc.resp.Code, ShouldEqual, 400)
+ })
+ })
+
+ Convey("When gets auth tokens for a user", t, func() {
+ bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+ cmd.Result = &m.User{Id: TestUserID}
+ return nil
+ })
+
+ currentToken := &m.UserToken{Id: 1}
+
+ getUserAuthTokensInternalScenario("Should be successful", currentToken, func(sc *scenarioContext) {
+ tokens := []*m.UserToken{
+ {
+ Id: 1,
+ ClientIp: "127.0.0.1",
+ UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+ CreatedAt: time.Now().Unix(),
+ SeenAt: time.Now().Unix(),
+ },
+ {
+ Id: 2,
+ ClientIp: "127.0.0.2",
+ UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+ CreatedAt: time.Now().Unix(),
+ SeenAt: time.Now().Unix(),
+ },
+ }
+ sc.userAuthTokenService.GetUserTokensProvider = func(userId int64) ([]*m.UserToken, error) {
+ return tokens, nil
+ }
+ sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+ So(sc.resp.Code, ShouldEqual, 200)
+ result := sc.ToJSON()
+ So(result.MustArray(), ShouldHaveLength, 2)
+
+ resultOne := result.GetIndex(0)
+ So(resultOne.Get("id").MustInt64(), ShouldEqual, tokens[0].Id)
+ So(resultOne.Get("isActive").MustBool(), ShouldBeTrue)
+ So(resultOne.Get("clientIp").MustString(), ShouldEqual, "127.0.0.1")
+ So(resultOne.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36")
+ So(resultOne.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339))
+ So(resultOne.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339))
+
+ resultTwo := result.GetIndex(1)
+ So(resultTwo.Get("id").MustInt64(), ShouldEqual, tokens[1].Id)
+ So(resultTwo.Get("isActive").MustBool(), ShouldBeFalse)
+ So(resultTwo.Get("clientIp").MustString(), ShouldEqual, "127.0.0.2")
+ So(resultTwo.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")
+ So(resultTwo.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339))
+ So(resultTwo.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[1].SeenAt, 0).Format(time.RFC3339))
+ })
+ })
+}
+
+func revokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, userId int64, fn scenarioFunc) {
+ Convey(desc+" "+url, func() {
+ defer bus.ClearBusHandlers()
+
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: fakeAuthTokenService,
+ }
+
+ sc := setupScenarioContext(url)
+ sc.userAuthTokenService = fakeAuthTokenService
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = userId
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+
+ return hs.RevokeUserAuthToken(c, cmd)
+ })
+
+ sc.m.Post(routePattern, sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
+func getUserAuthTokensScenario(desc string, url string, routePattern string, userId int64, fn scenarioFunc) {
+ Convey(desc+" "+url, func() {
+ defer bus.ClearBusHandlers()
+
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: fakeAuthTokenService,
+ }
+
+ sc := setupScenarioContext(url)
+ sc.userAuthTokenService = fakeAuthTokenService
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = userId
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+
+ return hs.GetUserAuthTokens(c)
+ })
+
+ sc.m.Get(routePattern, sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
+func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scenarioFunc) {
+ Convey(desc, func() {
+ defer bus.ClearBusHandlers()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: auth.NewFakeUserAuthTokenService(),
+ }
+
+ sc := setupScenarioContext("/")
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = TestUserID
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+
+ return hs.logoutUserFromAllDevicesInternal(userId)
+ })
+
+ sc.m.Post("/", sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
+func revokeUserAuthTokenInternalScenario(desc string, cmd m.RevokeAuthTokenCmd, userId int64, token *m.UserToken, fn scenarioFunc) {
+ Convey(desc, func() {
+ defer bus.ClearBusHandlers()
+
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: fakeAuthTokenService,
+ }
+
+ sc := setupScenarioContext("/")
+ sc.userAuthTokenService = fakeAuthTokenService
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = TestUserID
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+ sc.context.UserToken = token
+
+ return hs.revokeUserAuthTokenInternal(c, userId, cmd)
+ })
+
+ sc.m.Post("/", sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
+func getUserAuthTokensInternalScenario(desc string, token *m.UserToken, fn scenarioFunc) {
+ Convey(desc, func() {
+ defer bus.ClearBusHandlers()
+
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+ hs := HTTPServer{
+ Bus: bus.GetBus(),
+ AuthTokenService: fakeAuthTokenService,
+ }
+
+ sc := setupScenarioContext("/")
+ sc.userAuthTokenService = fakeAuthTokenService
+ sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+ sc.context = c
+ sc.context.UserId = TestUserID
+ sc.context.OrgId = TestOrgID
+ sc.context.OrgRole = m.ROLE_ADMIN
+ sc.context.UserToken = token
+
+ return hs.getUserAuthTokensInternal(c, TestUserID)
+ })
+
+ sc.m.Get("/", sc.defaultHandler)
+
+ fn(sc)
+ })
+}
diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go
index 53218147ae0..c10212329cf 100644
--- a/pkg/cmd/grafana-server/server.go
+++ b/pkg/cmd/grafana-server/server.go
@@ -29,6 +29,7 @@ import (
// self registering services
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/infra/metrics"
+ _ "github.com/grafana/grafana/pkg/infra/remotecache"
_ "github.com/grafana/grafana/pkg/infra/serverlock"
_ "github.com/grafana/grafana/pkg/infra/tracing"
_ "github.com/grafana/grafana/pkg/infra/usagestats"
diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go
new file mode 100644
index 00000000000..1c39d74d800
--- /dev/null
+++ b/pkg/infra/remotecache/database_storage.go
@@ -0,0 +1,126 @@
+package remotecache
+
+import (
+ "context"
+ "time"
+
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+)
+
+var getTime = time.Now
+
+const databaseCacheType = "database"
+
+type databaseCache struct {
+ SQLStore *sqlstore.SqlStore
+ log log.Logger
+}
+
+func newDatabaseCache(sqlstore *sqlstore.SqlStore) *databaseCache {
+ dc := &databaseCache{
+ SQLStore: sqlstore,
+ log: log.New("remotecache.database"),
+ }
+
+ return dc
+}
+
+func (dc *databaseCache) Run(ctx context.Context) error {
+ ticker := time.NewTicker(time.Minute * 10)
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-ticker.C:
+ dc.internalRunGC()
+ }
+ }
+}
+
+func (dc *databaseCache) internalRunGC() {
+ now := getTime().Unix()
+ sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0`
+
+ _, err := dc.SQLStore.NewSession().Exec(sql, now)
+ if err != nil {
+ dc.log.Error("failed to run garbage collect", "error", err)
+ }
+}
+
+func (dc *databaseCache) Get(key string) (interface{}, error) {
+ cacheHit := CacheData{}
+ session := dc.SQLStore.NewSession()
+ defer session.Close()
+
+ exist, err := session.Where("cache_key= ?", key).Get(&cacheHit)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if !exist {
+ return nil, ErrCacheItemNotFound
+ }
+
+ if cacheHit.Expires > 0 {
+ existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires
+ if existedButExpired {
+ _ = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway
+ return nil, ErrCacheItemNotFound
+ }
+ }
+
+ item := &cachedItem{}
+ if err = decodeGob(cacheHit.Data, item); err != nil {
+ return nil, err
+ }
+
+ return item.Val, nil
+}
+
+func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration) error {
+ item := &cachedItem{Val: value}
+ data, err := encodeGob(item)
+ if err != nil {
+ return err
+ }
+
+ session := dc.SQLStore.NewSession()
+
+ var cacheHit CacheData
+ has, err := session.Where("cache_key = ?", key).Get(&cacheHit)
+ if err != nil {
+ return err
+ }
+
+ var expiresInSeconds int64
+ if expire != 0 {
+ expiresInSeconds = int64(expire) / int64(time.Second)
+ }
+
+ // insert or update depending on if item already exist
+ if has {
+ sql := `UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'`
+ _, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
+ } else {
+ sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
+ _, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds)
+ }
+
+ return err
+}
+
+func (dc *databaseCache) Delete(key string) error {
+ sql := "DELETE FROM cache_data WHERE cache_key=?"
+ _, err := dc.SQLStore.NewSession().Exec(sql, key)
+
+ return err
+}
+
+type CacheData struct {
+ CacheKey string
+ Data []byte
+ Expires int64
+ CreatedAt int64
+}
diff --git a/pkg/infra/remotecache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go
new file mode 100644
index 00000000000..d15e26fd07f
--- /dev/null
+++ b/pkg/infra/remotecache/database_storage_test.go
@@ -0,0 +1,56 @@
+package remotecache
+
+import (
+ "testing"
+ "time"
+
+ "github.com/bmizerany/assert"
+
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+)
+
+func TestDatabaseStorageGarbageCollection(t *testing.T) {
+ sqlstore := sqlstore.InitTestDB(t)
+
+ db := &databaseCache{
+ SQLStore: sqlstore,
+ log: log.New("remotecache.database"),
+ }
+
+ obj := &CacheableStruct{String: "foolbar"}
+
+ //set time.now to 2 weeks ago
+ var err error
+ getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
+ err = db.Set("key1", obj, 1000*time.Second)
+ assert.Equal(t, err, nil)
+
+ err = db.Set("key2", obj, 1000*time.Second)
+ assert.Equal(t, err, nil)
+
+ err = db.Set("key3", obj, 1000*time.Second)
+ assert.Equal(t, err, nil)
+
+ // insert object that should never expire
+ db.Set("key4", obj, 0)
+
+ getTime = time.Now
+ db.Set("key5", obj, 1000*time.Second)
+
+ //run GC
+ db.internalRunGC()
+
+ //try to read values
+ _, err = db.Get("key1")
+ assert.Equal(t, err, ErrCacheItemNotFound, "expected cache item not found. got: ", err)
+ _, err = db.Get("key2")
+ assert.Equal(t, err, ErrCacheItemNotFound)
+ _, err = db.Get("key3")
+ assert.Equal(t, err, ErrCacheItemNotFound)
+
+ _, err = db.Get("key4")
+ assert.Equal(t, err, nil)
+ _, err = db.Get("key5")
+ assert.Equal(t, err, nil)
+}
diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go
new file mode 100644
index 00000000000..5424a05ad02
--- /dev/null
+++ b/pkg/infra/remotecache/memcached_storage.go
@@ -0,0 +1,71 @@
+package remotecache
+
+import (
+ "time"
+
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+const memcachedCacheType = "memcached"
+
+type memcachedStorage struct {
+ c *memcache.Client
+}
+
+func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage {
+ return &memcachedStorage{
+ c: memcache.New(opts.ConnStr),
+ }
+}
+
+func newItem(sid string, data []byte, expire int32) *memcache.Item {
+ return &memcache.Item{
+ Key: sid,
+ Value: data,
+ Expiration: expire,
+ }
+}
+
+// Set sets value to given key in the cache.
+func (s *memcachedStorage) Set(key string, val interface{}, expires time.Duration) error {
+ item := &cachedItem{Val: val}
+ bytes, err := encodeGob(item)
+ if err != nil {
+ return err
+ }
+
+ var expiresInSeconds int64
+ if expires != 0 {
+ expiresInSeconds = int64(expires) / int64(time.Second)
+ }
+
+ memcachedItem := newItem(key, bytes, int32(expiresInSeconds))
+ return s.c.Set(memcachedItem)
+}
+
+// Get gets value by given key in the cache.
+func (s *memcachedStorage) Get(key string) (interface{}, error) {
+ memcachedItem, err := s.c.Get(key)
+ if err != nil && err.Error() == "memcache: cache miss" {
+ return nil, ErrCacheItemNotFound
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ item := &cachedItem{}
+
+ err = decodeGob(memcachedItem.Value, item)
+ if err != nil {
+ return nil, err
+ }
+
+ return item.Val, nil
+}
+
+// Delete delete a key from the cache
+func (s *memcachedStorage) Delete(key string) error {
+ return s.c.Delete(key)
+}
diff --git a/pkg/infra/remotecache/memcached_storage_integration_test.go b/pkg/infra/remotecache/memcached_storage_integration_test.go
new file mode 100644
index 00000000000..d1d82468644
--- /dev/null
+++ b/pkg/infra/remotecache/memcached_storage_integration_test.go
@@ -0,0 +1,15 @@
+// +build memcached
+
+package remotecache
+
+import (
+ "testing"
+
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+func TestMemcachedCacheStorage(t *testing.T) {
+ opts := &setting.RemoteCacheOptions{Name: memcachedCacheType, ConnStr: "localhost:11211"}
+ client := createTestClient(t, opts, nil)
+ runTestsForClient(t, client)
+}
diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go
new file mode 100644
index 00000000000..bd54b843119
--- /dev/null
+++ b/pkg/infra/remotecache/redis_storage.go
@@ -0,0 +1,62 @@
+package remotecache
+
+import (
+ "time"
+
+ "github.com/grafana/grafana/pkg/setting"
+ redis "gopkg.in/redis.v2"
+)
+
+const redisCacheType = "redis"
+
+type redisStorage struct {
+ c *redis.Client
+}
+
+func newRedisStorage(opts *setting.RemoteCacheOptions) *redisStorage {
+ opt := &redis.Options{
+ Network: "tcp",
+ Addr: opts.ConnStr,
+ }
+ return &redisStorage{c: redis.NewClient(opt)}
+}
+
+// Set sets value to given key in session.
+func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) error {
+ item := &cachedItem{Val: val}
+ value, err := encodeGob(item)
+ if err != nil {
+ return err
+ }
+
+ status := s.c.SetEx(key, expires, string(value))
+ return status.Err()
+}
+
+// Get gets value by given key in session.
+func (s *redisStorage) Get(key string) (interface{}, error) {
+ v := s.c.Get(key)
+
+ item := &cachedItem{}
+ err := decodeGob([]byte(v.Val()), item)
+
+ if err == nil {
+ return item.Val, nil
+ }
+
+ if err.Error() == "EOF" {
+ return nil, ErrCacheItemNotFound
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return item.Val, nil
+}
+
+// Delete delete a key from session.
+func (s *redisStorage) Delete(key string) error {
+ cmd := s.c.Del(key)
+ return cmd.Err()
+}
diff --git a/pkg/infra/remotecache/redis_storage_integration_test.go b/pkg/infra/remotecache/redis_storage_integration_test.go
new file mode 100644
index 00000000000..8d54fc9ff14
--- /dev/null
+++ b/pkg/infra/remotecache/redis_storage_integration_test.go
@@ -0,0 +1,16 @@
+// +build redis
+
+package remotecache
+
+import (
+ "testing"
+
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+func TestRedisCacheStorage(t *testing.T) {
+
+ opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: "localhost:6379"}
+ client := createTestClient(t, opts, nil)
+ runTestsForClient(t, client)
+}
diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go
new file mode 100644
index 00000000000..9219fa33a08
--- /dev/null
+++ b/pkg/infra/remotecache/remotecache.go
@@ -0,0 +1,133 @@
+package remotecache
+
+import (
+ "bytes"
+ "context"
+ "encoding/gob"
+ "errors"
+ "time"
+
+ "github.com/grafana/grafana/pkg/setting"
+
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+
+ "github.com/grafana/grafana/pkg/registry"
+)
+
+var (
+ // ErrCacheItemNotFound is returned if cache does not exist
+ ErrCacheItemNotFound = errors.New("cache item not found")
+
+ // ErrInvalidCacheType is returned if the type is invalid
+ ErrInvalidCacheType = errors.New("invalid remote cache name")
+
+ defaultMaxCacheExpiration = time.Hour * 24
+)
+
+func init() {
+ registry.RegisterService(&RemoteCache{})
+}
+
+// CacheStorage allows the caller to set, get and delete items in the cache.
+// Cached items are stored as byte arrays and marshalled using "encoding/gob"
+// so any struct added to the cache needs to be registred with `remotecache.Register`
+// ex `remotecache.Register(CacheableStruct{})``
+type CacheStorage interface {
+ // Get reads object from Cache
+ Get(key string) (interface{}, error)
+
+ // Set sets an object into the cache. if `expire` is set to zero it will default to 24h
+ Set(key string, value interface{}, expire time.Duration) error
+
+ // Delete object from cache
+ Delete(key string) error
+}
+
+// RemoteCache allows Grafana to cache data outside its own process
+type RemoteCache struct {
+ log log.Logger
+ client CacheStorage
+ SQLStore *sqlstore.SqlStore `inject:""`
+ Cfg *setting.Cfg `inject:""`
+}
+
+// Get reads object from Cache
+func (ds *RemoteCache) Get(key string) (interface{}, error) {
+ return ds.client.Get(key)
+}
+
+// Set sets an object into the cache. if `expire` is set to zero it will default to 24h
+func (ds *RemoteCache) Set(key string, value interface{}, expire time.Duration) error {
+ if expire == 0 {
+ expire = defaultMaxCacheExpiration
+ }
+
+ return ds.client.Set(key, value, expire)
+}
+
+// Delete object from cache
+func (ds *RemoteCache) Delete(key string) error {
+ return ds.client.Delete(key)
+}
+
+// Init initializes the service
+func (ds *RemoteCache) Init() error {
+ ds.log = log.New("cache.remote")
+ var err error
+ ds.client, err = createClient(ds.Cfg.RemoteCacheOptions, ds.SQLStore)
+ return err
+}
+
+// Run start the backend processes for cache clients
+func (ds *RemoteCache) Run(ctx context.Context) error {
+ //create new interface if more clients need GC jobs
+ backgroundjob, ok := ds.client.(registry.BackgroundService)
+ if ok {
+ return backgroundjob.Run(ctx)
+ }
+
+ <-ctx.Done()
+ return ctx.Err()
+}
+
+func createClient(opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) (CacheStorage, error) {
+ if opts.Name == redisCacheType {
+ return newRedisStorage(opts), nil
+ }
+
+ if opts.Name == memcachedCacheType {
+ return newMemcachedStorage(opts), nil
+ }
+
+ if opts.Name == databaseCacheType {
+ return newDatabaseCache(sqlstore), nil
+ }
+
+ return nil, ErrInvalidCacheType
+}
+
+// Register records a type, identified by a value for that type, under its
+// internal type name. That name will identify the concrete type of a value
+// sent or received as an interface variable. Only types that will be
+// transferred as implementations of interface values need to be registered.
+// Expecting to be used only during initialization, it panics if the mapping
+// between types and names is not a bijection.
+func Register(value interface{}) {
+ gob.Register(value)
+}
+
+type cachedItem struct {
+ Val interface{}
+}
+
+func encodeGob(item *cachedItem) ([]byte, error) {
+ buf := bytes.NewBuffer(nil)
+ err := gob.NewEncoder(buf).Encode(item)
+ return buf.Bytes(), err
+}
+
+func decodeGob(data []byte, out *cachedItem) error {
+ buf := bytes.NewBuffer(data)
+ return gob.NewDecoder(buf).Decode(&out)
+}
diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go
new file mode 100644
index 00000000000..bf1675ec87c
--- /dev/null
+++ b/pkg/infra/remotecache/remotecache_test.go
@@ -0,0 +1,93 @@
+package remotecache
+
+import (
+ "testing"
+ "time"
+
+ "github.com/bmizerany/assert"
+
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+type CacheableStruct struct {
+ String string
+ Int64 int64
+}
+
+func init() {
+ Register(CacheableStruct{})
+}
+
+func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) CacheStorage {
+ t.Helper()
+
+ dc := &RemoteCache{
+ SQLStore: sqlstore,
+ Cfg: &setting.Cfg{
+ RemoteCacheOptions: opts,
+ },
+ }
+
+ err := dc.Init()
+ if err != nil {
+ t.Fatalf("failed to init client for test. error: %v", err)
+ }
+
+ return dc
+}
+
+func TestCachedBasedOnConfig(t *testing.T) {
+
+ cfg := setting.NewCfg()
+ cfg.Load(&setting.CommandLineArgs{
+ HomePath: "../../../",
+ })
+
+ client := createTestClient(t, cfg.RemoteCacheOptions, sqlstore.InitTestDB(t))
+ runTestsForClient(t, client)
+}
+
+func TestInvalidCacheTypeReturnsError(t *testing.T) {
+ _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil)
+ assert.Equal(t, err, ErrInvalidCacheType)
+}
+
+func runTestsForClient(t *testing.T, client CacheStorage) {
+ canPutGetAndDeleteCachedObjects(t, client)
+ canNotFetchExpiredItems(t, client)
+}
+
+func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) {
+ cacheableStruct := CacheableStruct{String: "hej", Int64: 2000}
+
+ err := client.Set("key1", cacheableStruct, 0)
+ assert.Equal(t, err, nil, "expected nil. got: ", err)
+
+ data, err := client.Get("key1")
+ s, ok := data.(CacheableStruct)
+
+ assert.Equal(t, ok, true)
+ assert.Equal(t, s.String, "hej")
+ assert.Equal(t, s.Int64, int64(2000))
+
+ err = client.Delete("key1")
+ assert.Equal(t, err, nil)
+
+ _, err = client.Get("key1")
+ assert.Equal(t, err, ErrCacheItemNotFound)
+}
+
+func canNotFetchExpiredItems(t *testing.T, client CacheStorage) {
+ cacheableStruct := CacheableStruct{String: "hej", Int64: 2000}
+
+ err := client.Set("key1", cacheableStruct, time.Second)
+ assert.Equal(t, err, nil)
+
+ //not sure how this can be avoided when testing redis/memcached :/
+ <-time.After(time.Second + time.Millisecond)
+
+ // should not be able to read that value since its expired
+ _, err = client.Get("key1")
+ assert.Equal(t, err, ErrCacheItemNotFound)
+}
diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go
index e06409211eb..c00241ea34c 100644
--- a/pkg/middleware/auth.go
+++ b/pkg/middleware/auth.go
@@ -86,3 +86,20 @@ func Auth(options *AuthOptions) macaron.Handler {
}
}
}
+
+// AdminOrFeatureEnabled creates a middleware that allows access
+// if the signed in user is either an Org Admin or if the
+// feature flag is enabled.
+// Intended for when feature flags open up access to APIs that
+// are otherwise only available to admins.
+func AdminOrFeatureEnabled(enabled bool) macaron.Handler {
+ return func(c *m.ReqContext) {
+ if c.OrgRole == m.ROLE_ADMIN {
+ return
+ }
+
+ if !enabled {
+ accessForbidden(c)
+ }
+ }
+}
diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
index 1fbd303bebd..2fc8e0c456f 100644
--- a/pkg/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -11,6 +11,7 @@ import (
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/auth"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -155,7 +156,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
- sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+ sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: unhashedToken,
@@ -184,14 +185,14 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
- sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+ sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
- sc.userAuthTokenService.tryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
+ sc.userAuthTokenService.TryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
userToken.UnhashedToken = "rotated"
return true, nil
}
@@ -226,7 +227,7 @@ func TestMiddlewareContext(t *testing.T) {
middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
- sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+ sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return nil, m.ErrUserTokenNotFound
}
@@ -562,7 +563,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
}))
session.Init(&msession.Options{}, 0)
- sc.userAuthTokenService = newFakeUserAuthTokenService()
+ sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
@@ -595,7 +596,7 @@ type scenarioContext struct {
handlerFunc handlerFunc
defaultHandler macaron.Handler
url string
- userAuthTokenService *fakeUserAuthTokenService
+ userAuthTokenService *auth.FakeUserAuthTokenService
req *http.Request
}
@@ -676,57 +677,3 @@ func (sc *scenarioContext) exec() {
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext)
-
-type fakeUserAuthTokenService struct {
- createTokenProvider func(userId int64, clientIP, userAgent string) (*m.UserToken, error)
- tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
- lookupTokenProvider func(unhashedToken string) (*m.UserToken, error)
- revokeTokenProvider func(token *m.UserToken) error
- activeAuthTokenCount func() (int64, error)
-}
-
-func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
- return &fakeUserAuthTokenService{
- createTokenProvider: func(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
- return &m.UserToken{
- UserId: 0,
- UnhashedToken: "",
- }, nil
- },
- tryRotateTokenProvider: func(token *m.UserToken, clientIP, userAgent string) (bool, error) {
- return false, nil
- },
- lookupTokenProvider: func(unhashedToken string) (*m.UserToken, error) {
- return &m.UserToken{
- UserId: 0,
- UnhashedToken: "",
- }, nil
- },
- revokeTokenProvider: func(token *m.UserToken) error {
- return nil
- },
- activeAuthTokenCount: func() (int64, error) {
- return 10, nil
- },
- }
-}
-
-func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
- return s.createTokenProvider(userId, clientIP, userAgent)
-}
-
-func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (*m.UserToken, error) {
- return s.lookupTokenProvider(unhashedToken)
-}
-
-func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, userAgent string) (bool, error) {
- return s.tryRotateTokenProvider(token, clientIP, userAgent)
-}
-
-func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
- return s.revokeTokenProvider(token)
-}
-
-func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
- return s.activeAuthTokenCount()
-}
diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go
index e01d1a68d21..fe5b2736035 100644
--- a/pkg/middleware/org_redirect_test.go
+++ b/pkg/middleware/org_redirect_test.go
@@ -24,7 +24,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
return nil
})
- sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+ sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 0,
UnhashedToken: "",
@@ -50,7 +50,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
return nil
})
- sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+ sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go
index 52b696cf037..0ba42e708bc 100644
--- a/pkg/middleware/quota_test.go
+++ b/pkg/middleware/quota_test.go
@@ -3,6 +3,7 @@ package middleware
import (
"testing"
+ "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/bus"
@@ -36,7 +37,7 @@ func TestMiddlewareQuota(t *testing.T) {
},
}
- fakeAuthTokenService := newFakeUserAuthTokenService()
+ fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
qs := "a.QuotaService{
AuthTokenService: fakeAuthTokenService,
}
@@ -87,7 +88,7 @@ func TestMiddlewareQuota(t *testing.T) {
return nil
})
- sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+ sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go
index 6736d699a39..00f3b7a3032 100644
--- a/pkg/middleware/recovery_test.go
+++ b/pkg/middleware/recovery_test.go
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
macaron "gopkg.in/macaron.v1"
@@ -62,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
- sc.userAuthTokenService = newFakeUserAuthTokenService()
+ sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
sc.m.Use(OrgRedirect())
diff --git a/pkg/models/team.go b/pkg/models/team.go
index 61285db3a5f..bc8cbba8100 100644
--- a/pkg/models/team.go
+++ b/pkg/models/team.go
@@ -7,9 +7,12 @@ import (
// Typed errors
var (
- ErrTeamNotFound = errors.New("Team not found")
- ErrTeamNameTaken = errors.New("Team name is taken")
- ErrTeamMemberNotFound = errors.New("Team member not found")
+ ErrTeamNotFound = errors.New("Team not found")
+ ErrTeamNameTaken = errors.New("Team name is taken")
+ ErrTeamMemberNotFound = errors.New("Team member not found")
+ ErrLastTeamAdmin = errors.New("Not allowed to remove last admin")
+ ErrNotAllowedToUpdateTeam = errors.New("User not allowed to update team")
+ ErrNotAllowedToUpdateTeamInDifferentOrg = errors.New("User not allowed to update team in another org")
)
// Team model
@@ -59,22 +62,24 @@ type GetTeamsByUserQuery struct {
}
type SearchTeamsQuery struct {
- Query string
- Name string
- Limit int
- Page int
- OrgId int64
+ Query string
+ Name string
+ Limit int
+ Page int
+ OrgId int64
+ UserIdFilter int64
Result SearchTeamQueryResult
}
type TeamDTO struct {
- Id int64 `json:"id"`
- OrgId int64 `json:"orgId"`
- Name string `json:"name"`
- Email string `json:"email"`
- AvatarUrl string `json:"avatarUrl"`
- MemberCount int64 `json:"memberCount"`
+ Id int64 `json:"id"`
+ OrgId int64 `json:"orgId"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ AvatarUrl string `json:"avatarUrl"`
+ MemberCount int64 `json:"memberCount"`
+ Permission PermissionType `json:"permission"`
}
type SearchTeamQueryResult struct {
diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go
index dd64787f465..0b80aef8f44 100644
--- a/pkg/models/team_member.go
+++ b/pkg/models/team_member.go
@@ -12,11 +12,12 @@ var (
// TeamMember model
type TeamMember struct {
- Id int64
- OrgId int64
- TeamId int64
- UserId int64
- External bool
+ Id int64
+ OrgId int64
+ TeamId int64
+ UserId int64
+ External bool // Signals that the membership has been created by an external systems, such as LDAP
+ Permission PermissionType
Created time.Time
Updated time.Time
@@ -26,16 +27,26 @@ type TeamMember struct {
// COMMANDS
type AddTeamMemberCommand struct {
- UserId int64 `json:"userId" binding:"Required"`
- OrgId int64 `json:"-"`
- TeamId int64 `json:"-"`
- External bool `json:"-"`
+ UserId int64 `json:"userId" binding:"Required"`
+ OrgId int64 `json:"-"`
+ TeamId int64 `json:"-"`
+ External bool `json:"-"`
+ Permission PermissionType `json:"-"`
+}
+
+type UpdateTeamMemberCommand struct {
+ UserId int64 `json:"-"`
+ OrgId int64 `json:"-"`
+ TeamId int64 `json:"-"`
+ Permission PermissionType `json:"permission"`
+ ProtectLastAdmin bool `json:"-"`
}
type RemoveTeamMemberCommand struct {
- OrgId int64 `json:"-"`
- UserId int64
- TeamId int64
+ OrgId int64 `json:"-"`
+ UserId int64
+ TeamId int64
+ ProtectLastAdmin bool `json:"-"`
}
// ----------------------
@@ -53,12 +64,13 @@ type GetTeamMembersQuery struct {
// Projections and DTOs
type TeamMemberDTO struct {
- OrgId int64 `json:"orgId"`
- TeamId int64 `json:"teamId"`
- UserId int64 `json:"userId"`
- External bool `json:"-"`
- Email string `json:"email"`
- Login string `json:"login"`
- AvatarUrl string `json:"avatarUrl"`
- Labels []string `json:"labels"`
+ OrgId int64 `json:"orgId"`
+ TeamId int64 `json:"teamId"`
+ UserId int64 `json:"userId"`
+ External bool `json:"-"`
+ Email string `json:"email"`
+ Login string `json:"login"`
+ AvatarUrl string `json:"avatarUrl"`
+ Labels []string `json:"labels"`
+ Permission PermissionType `json:"permission"`
}
diff --git a/pkg/models/user_token.go b/pkg/models/user_token.go
index 388bc2dd4a2..8c3e7985995 100644
--- a/pkg/models/user_token.go
+++ b/pkg/models/user_token.go
@@ -1,6 +1,8 @@
package models
-import "errors"
+import (
+ "errors"
+)
// Typed errors
var (
@@ -23,11 +25,18 @@ type UserToken struct {
UnhashedToken string
}
+type RevokeAuthTokenCmd struct {
+ AuthTokenId int64 `json:"authTokenId"`
+}
+
// UserTokenService are used for generating and validating user tokens
type UserTokenService interface {
CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error)
LookupToken(unhashedToken string) (*UserToken, error)
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
RevokeToken(token *UserToken) error
+ RevokeAllUserTokens(userId int64) error
ActiveTokenCount() (int64, error)
+ GetUserToken(userId, userTokenId int64) (*UserToken, error)
+ GetUserTokens(userId int64) ([]*UserToken, error)
}
diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go
index 3093aec9957..f33b3f4019e 100644
--- a/pkg/services/alerting/notifiers/victorops.go
+++ b/pkg/services/alerting/notifiers/victorops.go
@@ -92,14 +92,29 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
messageType = AlertStateRecovery
}
+ fields := make(map[string]interface{}, 0)
+ fieldLimitCount := 4
+ for index, evt := range evalContext.EvalMatches {
+ fields[evt.Metric] = evt.Value
+ if index > fieldLimitCount {
+ break
+ }
+ }
+
bodyJSON := simplejson.New()
bodyJSON.Set("message_type", messageType)
bodyJSON.Set("entity_id", evalContext.Rule.Name)
+ bodyJSON.Set("entity_display_name", evalContext.GetNotificationTitle())
bodyJSON.Set("timestamp", time.Now().Unix())
bodyJSON.Set("state_start_time", evalContext.StartTime.Unix())
bodyJSON.Set("state_message", evalContext.Rule.Message)
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
bodyJSON.Set("alert_url", ruleUrl)
+ bodyJSON.Set("metrics", fields)
+
+ if evalContext.Error != nil {
+ bodyJSON.Set("error_message", evalContext.Error.Error())
+ }
if evalContext.ImagePublicUrl != "" {
bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go
index 648575d54cd..255866a9ba0 100644
--- a/pkg/services/auth/auth_token.go
+++ b/pkg/services/auth/auth_token.go
@@ -221,6 +221,57 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
return nil
}
+func (s *UserAuthTokenService) RevokeAllUserTokens(userId int64) error {
+ sql := `DELETE from user_auth_token WHERE user_id = ?`
+ res, err := s.SQLStore.NewSession().Exec(sql, userId)
+ if err != nil {
+ return err
+ }
+
+ affected, err := res.RowsAffected()
+ if err != nil {
+ return err
+ }
+
+ s.log.Debug("all user tokens for user revoked", "userId", userId, "count", affected)
+
+ return nil
+}
+
+func (s *UserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) {
+ var token userAuthToken
+ exists, err := s.SQLStore.NewSession().Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token)
+ if err != nil {
+ return nil, err
+ }
+
+ if !exists {
+ return nil, models.ErrUserTokenNotFound
+ }
+
+ var result models.UserToken
+ token.toUserToken(&result)
+
+ return &result, nil
+}
+
+func (s *UserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) {
+ var tokens []*userAuthToken
+ err := s.SQLStore.NewSession().Where("user_id = ? AND created_at > ? AND rotated_at > ?", userId, s.createdAfterParam(), s.rotatedAfterParam()).Find(&tokens)
+ if err != nil {
+ return nil, err
+ }
+
+ result := []*models.UserToken{}
+ for _, token := range tokens {
+ var userToken models.UserToken
+ token.toUserToken(&userToken)
+ result = append(result, &userToken)
+ }
+
+ return result, nil
+}
+
func (s *UserAuthTokenService) createdAfterParam() int64 {
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
return getTime().Add(-tokenMaxLifetime).Unix()
diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go
index 49e7acc3a5b..33eb309ad18 100644
--- a/pkg/services/auth/auth_token_test.go
+++ b/pkg/services/auth/auth_token_test.go
@@ -75,6 +75,47 @@ func TestUserAuthToken(t *testing.T) {
err = userAuthTokenService.RevokeToken(userToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
})
+
+ Convey("When creating an additional token", func() {
+ userToken2, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+ So(err, ShouldBeNil)
+ So(userToken2, ShouldNotBeNil)
+
+ Convey("Can get first user token", func() {
+ token, err := userAuthTokenService.GetUserToken(userID, userToken.Id)
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+ So(token.Id, ShouldEqual, userToken.Id)
+ })
+
+ Convey("Can get second user token", func() {
+ token, err := userAuthTokenService.GetUserToken(userID, userToken2.Id)
+ So(err, ShouldBeNil)
+ So(token, ShouldNotBeNil)
+ So(token.Id, ShouldEqual, userToken2.Id)
+ })
+
+ Convey("Can get user tokens", func() {
+ tokens, err := userAuthTokenService.GetUserTokens(userID)
+ So(err, ShouldBeNil)
+ So(tokens, ShouldHaveLength, 2)
+ So(tokens[0].Id, ShouldEqual, userToken.Id)
+ So(tokens[1].Id, ShouldEqual, userToken2.Id)
+ })
+
+ Convey("Can revoke all user tokens", func() {
+ err := userAuthTokenService.RevokeAllUserTokens(userID)
+ So(err, ShouldBeNil)
+
+ model, err := ctx.getAuthTokenByID(userToken.Id)
+ So(err, ShouldBeNil)
+ So(model, ShouldBeNil)
+
+ model2, err := ctx.getAuthTokenByID(userToken2.Id)
+ So(err, ShouldBeNil)
+ So(model2, ShouldBeNil)
+ })
+ })
})
Convey("expires correctly", func() {
diff --git a/pkg/services/auth/testing.go b/pkg/services/auth/testing.go
new file mode 100644
index 00000000000..68e65466c3d
--- /dev/null
+++ b/pkg/services/auth/testing.go
@@ -0,0 +1,81 @@
+package auth
+
+import "github.com/grafana/grafana/pkg/models"
+
+type FakeUserAuthTokenService struct {
+ CreateTokenProvider func(userId int64, clientIP, userAgent string) (*models.UserToken, error)
+ TryRotateTokenProvider func(token *models.UserToken, clientIP, userAgent string) (bool, error)
+ LookupTokenProvider func(unhashedToken string) (*models.UserToken, error)
+ RevokeTokenProvider func(token *models.UserToken) error
+ RevokeAllUserTokensProvider func(userId int64) error
+ ActiveAuthTokenCount func() (int64, error)
+ GetUserTokenProvider func(userId, userTokenId int64) (*models.UserToken, error)
+ GetUserTokensProvider func(userId int64) ([]*models.UserToken, error)
+}
+
+func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
+ return &FakeUserAuthTokenService{
+ CreateTokenProvider: func(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
+ return &models.UserToken{
+ UserId: 0,
+ UnhashedToken: "",
+ }, nil
+ },
+ TryRotateTokenProvider: func(token *models.UserToken, clientIP, userAgent string) (bool, error) {
+ return false, nil
+ },
+ LookupTokenProvider: func(unhashedToken string) (*models.UserToken, error) {
+ return &models.UserToken{
+ UserId: 0,
+ UnhashedToken: "",
+ }, nil
+ },
+ RevokeTokenProvider: func(token *models.UserToken) error {
+ return nil
+ },
+ RevokeAllUserTokensProvider: func(userId int64) error {
+ return nil
+ },
+ ActiveAuthTokenCount: func() (int64, error) {
+ return 10, nil
+ },
+ GetUserTokenProvider: func(userId, userTokenId int64) (*models.UserToken, error) {
+ return nil, nil
+ },
+ GetUserTokensProvider: func(userId int64) ([]*models.UserToken, error) {
+ return nil, nil
+ },
+ }
+}
+
+func (s *FakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
+ return s.CreateTokenProvider(userId, clientIP, userAgent)
+}
+
+func (s *FakeUserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) {
+ return s.LookupTokenProvider(unhashedToken)
+}
+
+func (s *FakeUserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) {
+ return s.TryRotateTokenProvider(token, clientIP, userAgent)
+}
+
+func (s *FakeUserAuthTokenService) RevokeToken(token *models.UserToken) error {
+ return s.RevokeTokenProvider(token)
+}
+
+func (s *FakeUserAuthTokenService) RevokeAllUserTokens(userId int64) error {
+ return s.RevokeAllUserTokensProvider(userId)
+}
+
+func (s *FakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
+ return s.ActiveAuthTokenCount()
+}
+
+func (s *FakeUserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) {
+ return s.GetUserTokenProvider(userId, userTokenId)
+}
+
+func (s *FakeUserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) {
+ return s.GetUserTokensProvider(userId)
+}
diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go
new file mode 100644
index 00000000000..864fbb80a6b
--- /dev/null
+++ b/pkg/services/dashboards/acl_service.go
@@ -0,0 +1,55 @@
+package dashboards
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/models"
+ "time"
+)
+
+func MakeUserAdmin(bus bus.Bus, orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error {
+ rtEditor := models.ROLE_EDITOR
+ rtViewer := models.ROLE_VIEWER
+
+ items := []*models.DashboardAcl{
+ {
+ OrgId: orgId,
+ DashboardId: dashboardId,
+ UserId: userId,
+ Permission: models.PERMISSION_ADMIN,
+ Created: time.Now(),
+ Updated: time.Now(),
+ },
+ }
+
+ if setViewAndEditPermissions {
+ items = append(items,
+ &models.DashboardAcl{
+ OrgId: orgId,
+ DashboardId: dashboardId,
+ Role: &rtEditor,
+ Permission: models.PERMISSION_EDIT,
+ Created: time.Now(),
+ Updated: time.Now(),
+ },
+ &models.DashboardAcl{
+ OrgId: orgId,
+ DashboardId: dashboardId,
+ Role: &rtViewer,
+ Permission: models.PERMISSION_VIEW,
+ Created: time.Now(),
+ Updated: time.Now(),
+ },
+ )
+ }
+
+ aclCmd := &models.UpdateDashboardAclCommand{
+ DashboardId: dashboardId,
+ Items: items,
+ }
+
+ if err := bus.Dispatch(aclCmd); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/pkg/services/sqlstore/migrations/cache_data_mig.go b/pkg/services/sqlstore/migrations/cache_data_mig.go
new file mode 100644
index 00000000000..3467b88962b
--- /dev/null
+++ b/pkg/services/sqlstore/migrations/cache_data_mig.go
@@ -0,0 +1,22 @@
+package migrations
+
+import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addCacheMigration(mg *migrator.Migrator) {
+ var cacheDataV1 = migrator.Table{
+ Name: "cache_data",
+ Columns: []*migrator.Column{
+ {Name: "cache_key", Type: migrator.DB_NVarchar, IsPrimaryKey: true, Length: 168},
+ {Name: "data", Type: migrator.DB_Blob},
+ {Name: "expires", Type: migrator.DB_Integer, Length: 255, Nullable: false},
+ {Name: "created_at", Type: migrator.DB_Integer, Length: 255, Nullable: false},
+ },
+ Indices: []*migrator.Index{
+ {Cols: []string{"cache_key"}, Type: migrator.UniqueIndex},
+ },
+ }
+
+ mg.AddMigration("create cache_data table", migrator.NewAddTableMigration(cacheDataV1))
+
+ mg.AddMigration("add unique index cache_data.cache_key", migrator.NewAddIndexMigration(cacheDataV1, cacheDataV1.Indices[0]))
+}
diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go
index 931259ec3ed..3e40c749f37 100644
--- a/pkg/services/sqlstore/migrations/migrations.go
+++ b/pkg/services/sqlstore/migrations/migrations.go
@@ -33,6 +33,7 @@ func AddMigrations(mg *Migrator) {
addUserAuthMigrations(mg)
addServerlockMigrations(mg)
addUserAuthTokenMigrations(mg)
+ addCacheMigration(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go
index 34c46ad13cf..981a4865caa 100644
--- a/pkg/services/sqlstore/migrations/team_mig.go
+++ b/pkg/services/sqlstore/migrations/team_mig.go
@@ -54,4 +54,8 @@ func addTeamMigrations(mg *Migrator) {
mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
Name: "external", Type: DB_Bool, Nullable: true,
}))
+
+ mg.AddMigration("Add column permission to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
+ Name: "permission", Type: DB_SmallInt, Nullable: true,
+ }))
}
diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go
index 83593e6f2d7..03fd2df78fc 100644
--- a/pkg/services/sqlstore/team.go
+++ b/pkg/services/sqlstore/team.go
@@ -18,17 +18,30 @@ func init() {
bus.AddHandler("sql", GetTeamsByUser)
bus.AddHandler("sql", AddTeamMember)
+ bus.AddHandler("sql", UpdateTeamMember)
bus.AddHandler("sql", RemoveTeamMember)
bus.AddHandler("sql", GetTeamMembers)
}
+func getTeamSearchSqlBase() string {
+ return `SELECT
+ team.id as id,
+ team.org_id,
+ team.name as name,
+ team.email as email,
+ (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count,
+ team_member.permission
+ FROM team as team
+ INNER JOIN team_member on team.id = team_member.team_id AND team_member.user_id = ? `
+}
+
func getTeamSelectSqlBase() string {
return `SELECT
team.id as id,
team.org_id,
team.name as name,
team.email as email,
- (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count
+ (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count
FROM team as team `
}
@@ -91,10 +104,8 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
// DeleteTeam will delete a team, its member and any permissions connected to the team
func DeleteTeam(cmd *m.DeleteTeamCommand) error {
return inTransaction(func(sess *DBSession) error {
- if teamExists, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
+ if _, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
return err
- } else if !teamExists {
- return m.ErrTeamNotFound
}
deletes := []string{
@@ -117,7 +128,7 @@ func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) {
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
return false, err
} else if len(res) != 1 {
- return false, nil
+ return false, m.ErrTeamNotFound
}
return true, nil
@@ -147,7 +158,12 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
- sql.WriteString(getTeamSelectSqlBase())
+ if query.UserIdFilter > 0 {
+ sql.WriteString(getTeamSearchSqlBase())
+ params = append(params, query.UserIdFilter)
+ } else {
+ sql.WriteString(getTeamSelectSqlBase())
+ }
sql.WriteString(` WHERE team.org_id = ?`)
params = append(params, query.OrgId)
@@ -233,19 +249,18 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
return m.ErrTeamMemberAlreadyAdded
}
- if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
+ if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
return err
- } else if !teamExists {
- return m.ErrTeamNotFound
}
entity := m.TeamMember{
- OrgId: cmd.OrgId,
- TeamId: cmd.TeamId,
- UserId: cmd.UserId,
- External: cmd.External,
- Created: time.Now(),
- Updated: time.Now(),
+ OrgId: cmd.OrgId,
+ TeamId: cmd.TeamId,
+ UserId: cmd.UserId,
+ External: cmd.External,
+ Created: time.Now(),
+ Updated: time.Now(),
+ Permission: cmd.Permission,
}
_, err := sess.Insert(&entity)
@@ -253,13 +268,59 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
})
}
+func getTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (m.TeamMember, error) {
+ rawSql := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?`
+ var member m.TeamMember
+ exists, err := sess.SQL(rawSql, orgId, teamId, userId).Get(&member)
+
+ if err != nil {
+ return member, err
+ }
+ if !exists {
+ return member, m.ErrTeamMemberNotFound
+ }
+
+ return member, nil
+}
+
+// UpdateTeamMember updates a team member
+func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error {
+ return inTransaction(func(sess *DBSession) error {
+ member, err := getTeamMember(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
+ if err != nil {
+ return err
+ }
+
+ if cmd.ProtectLastAdmin {
+ _, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
+ if err != nil {
+ return err
+ }
+ }
+
+ if cmd.Permission != m.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store
+ cmd.Permission = 0
+ }
+
+ member.Permission = cmd.Permission
+ _, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member)
+
+ return err
+ })
+}
+
// RemoveTeamMember removes a member from a team
func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
- if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
+ if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
return err
- } else if !teamExists {
- return m.ErrTeamNotFound
+ }
+
+ if cmd.ProtectLastAdmin {
+ _, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
+ if err != nil {
+ return err
+ }
}
var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
@@ -276,6 +337,29 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
})
}
+func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool, error) {
+ rawSql := "SELECT user_id FROM team_member WHERE org_id=? and team_id=? and permission=?"
+ userIds := []*int64{}
+ err := sess.SQL(rawSql, orgId, teamId, m.PERMISSION_ADMIN).Find(&userIds)
+ if err != nil {
+ return false, err
+ }
+
+ isAdmin := false
+ for _, adminId := range userIds {
+ if userId == *adminId {
+ isAdmin = true
+ break
+ }
+ }
+
+ if isAdmin && len(userIds) == 1 {
+ return true, m.ErrLastTeamAdmin
+ }
+
+ return false, err
+}
+
// GetTeamMembers return a list of members for the specified team
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
query.Result = make([]*m.TeamMemberDTO, 0)
@@ -293,7 +377,7 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
if query.External {
sess.Where("team_member.external=?", dialect.BooleanStr(true))
}
- sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external")
+ sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external", "team_member.permission")
sess.Asc("user.login", "user.email")
err := sess.Find(&query.Result)
diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go
index 8f243617262..7ac78733af7 100644
--- a/pkg/services/sqlstore/team_test.go
+++ b/pkg/services/sqlstore/team_test.go
@@ -75,6 +75,72 @@ func TestTeamCommandsAndQueries(t *testing.T) {
So(q2.Result[0].External, ShouldEqual, true)
})
+ Convey("Should be able to update users in a team", func() {
+ userId := userIds[0]
+ team := group1.Result
+ addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userId}
+ err = AddTeamMember(&addMemberCmd)
+ So(err, ShouldBeNil)
+
+ qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+ err = GetTeamMembers(qBeforeUpdate)
+ So(err, ShouldBeNil)
+ So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0)
+
+ err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
+ UserId: userId,
+ OrgId: testOrgId,
+ TeamId: team.Id,
+ Permission: m.PERMISSION_ADMIN,
+ })
+
+ So(err, ShouldBeNil)
+
+ qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+ err = GetTeamMembers(qAfterUpdate)
+ So(err, ShouldBeNil)
+ So(qAfterUpdate.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
+ })
+
+ Convey("Should default to member permission level when updating a user with invalid permission level", func() {
+ userID := userIds[0]
+ team := group1.Result
+ addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userID}
+ err = AddTeamMember(&addMemberCmd)
+ So(err, ShouldBeNil)
+
+ qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+ err = GetTeamMembers(qBeforeUpdate)
+ So(err, ShouldBeNil)
+ So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0)
+
+ invalidPermissionLevel := m.PERMISSION_EDIT
+ err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
+ UserId: userID,
+ OrgId: testOrgId,
+ TeamId: team.Id,
+ Permission: invalidPermissionLevel,
+ })
+
+ So(err, ShouldBeNil)
+
+ qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+ err = GetTeamMembers(qAfterUpdate)
+ So(err, ShouldBeNil)
+ So(qAfterUpdate.Result[0].Permission, ShouldEqual, 0)
+ })
+
+ Convey("Shouldn't be able to update a user not in the team.", func() {
+ err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
+ UserId: 1,
+ OrgId: testOrgId,
+ TeamId: group1.Result.Id,
+ Permission: m.PERMISSION_ADMIN,
+ })
+
+ So(err, ShouldEqual, m.ErrTeamMemberNotFound)
+ })
+
Convey("Should be able to search for teams", func() {
query := &m.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1}
err = SearchTeams(query)
@@ -114,6 +180,33 @@ func TestTeamCommandsAndQueries(t *testing.T) {
So(len(q2.Result), ShouldEqual, 0)
})
+ Convey("When ProtectLastAdmin is set to true", func() {
+ err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: m.PERMISSION_ADMIN})
+ So(err, ShouldBeNil)
+
+ Convey("A user should not be able to remove the last admin", func() {
+ err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
+ So(err, ShouldEqual, m.ErrLastTeamAdmin)
+ })
+
+ Convey("A user should be able to remove an admin if there are other admins", func() {
+ AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN})
+ err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
+ So(err, ShouldEqual, nil)
+ })
+
+ Convey("A user should not be able to remove the admin permission for the last admin", func() {
+ err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
+ So(err, ShouldEqual, m.ErrLastTeamAdmin)
+ })
+
+ Convey("A user should be able to remove the admin permission if there are other admins", func() {
+ AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN})
+ err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
+ So(err, ShouldEqual, nil)
+ })
+ })
+
Convey("Should be able to remove a group with users and permissions", func() {
groupId := group2.Result.Id
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]})
diff --git a/pkg/services/teamguardian/team.go b/pkg/services/teamguardian/team.go
new file mode 100644
index 00000000000..70053d12da1
--- /dev/null
+++ b/pkg/services/teamguardian/team.go
@@ -0,0 +1,34 @@
+package teamguardian
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+func CanAdmin(bus bus.Bus, orgId int64, teamId int64, user *m.SignedInUser) error {
+ if user.OrgRole == m.ROLE_ADMIN {
+ return nil
+ }
+
+ if user.OrgId != orgId {
+ return m.ErrNotAllowedToUpdateTeamInDifferentOrg
+ }
+
+ cmd := m.GetTeamMembersQuery{
+ OrgId: orgId,
+ TeamId: teamId,
+ UserId: user.UserId,
+ }
+
+ if err := bus.Dispatch(&cmd); err != nil {
+ return err
+ }
+
+ for _, member := range cmd.Result {
+ if member.UserId == user.UserId && member.Permission == m.PERMISSION_ADMIN {
+ return nil
+ }
+ }
+
+ return m.ErrNotAllowedToUpdateTeam
+}
diff --git a/pkg/services/teamguardian/teams_test.go b/pkg/services/teamguardian/teams_test.go
new file mode 100644
index 00000000000..8af69569620
--- /dev/null
+++ b/pkg/services/teamguardian/teams_test.go
@@ -0,0 +1,87 @@
+package teamguardian
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ m "github.com/grafana/grafana/pkg/models"
+ . "github.com/smartystreets/goconvey/convey"
+ "testing"
+)
+
+func TestUpdateTeam(t *testing.T) {
+ Convey("Updating a team", t, func() {
+ bus.ClearBusHandlers()
+
+ admin := m.SignedInUser{
+ UserId: 1,
+ OrgId: 1,
+ OrgRole: m.ROLE_ADMIN,
+ }
+ editor := m.SignedInUser{
+ UserId: 2,
+ OrgId: 1,
+ OrgRole: m.ROLE_EDITOR,
+ }
+ testTeam := m.Team{
+ Id: 1,
+ OrgId: 1,
+ }
+
+ Convey("Given an editor and a team he isn't a member of", func() {
+ Convey("Should not be able to update the team", func() {
+ bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error {
+ cmd.Result = []*m.TeamMemberDTO{}
+ return nil
+ })
+
+ err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor)
+ So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam)
+ })
+ })
+
+ Convey("Given an editor and a team he is an admin in", func() {
+ Convey("Should be able to update the team", func() {
+ bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error {
+ cmd.Result = []*m.TeamMemberDTO{{
+ OrgId: testTeam.OrgId,
+ TeamId: testTeam.Id,
+ UserId: editor.UserId,
+ Permission: m.PERMISSION_ADMIN,
+ }}
+ return nil
+ })
+
+ err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor)
+ So(err, ShouldBeNil)
+ })
+ })
+
+ Convey("Given an editor and a team in another org", func() {
+ testTeamOtherOrg := m.Team{
+ Id: 1,
+ OrgId: 2,
+ }
+
+ Convey("Shouldn't be able to update the team", func() {
+ bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error {
+ cmd.Result = []*m.TeamMemberDTO{{
+ OrgId: testTeamOtherOrg.OrgId,
+ TeamId: testTeamOtherOrg.Id,
+ UserId: editor.UserId,
+ Permission: m.PERMISSION_ADMIN,
+ }}
+ return nil
+ })
+
+ err := CanAdmin(bus.GetBus(), testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor)
+ So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg)
+ })
+ })
+
+ Convey("Given an org admin and a team", func() {
+ Convey("Should be able to update the team", func() {
+ err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &admin)
+ So(err, ShouldBeNil)
+ })
+ })
+ })
+}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index 7b6e99255aa..8c6d8c54f11 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -239,8 +239,13 @@ type Cfg struct {
LoginMaxLifetimeDays int
TokenRotationIntervalMinutes int
- // User
- EditorsCanOwn bool
+ // Dataproxy
+ SendUserHeader bool
+
+ // DistributedCache
+ RemoteCacheOptions *RemoteCacheOptions
+
+ EditorsCanAdmin bool
}
type CommandLineArgs struct {
@@ -601,6 +606,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
dataproxy := iniFile.Section("dataproxy")
DataProxyLogging = dataproxy.Key("logging").MustBool(false)
DataProxyTimeout = dataproxy.Key("timeout").MustInt(30)
+ cfg.SendUserHeader = dataproxy.Key("send_user_header").MustBool(false)
// read security settings
security := iniFile.Section("security")
@@ -663,7 +669,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
ExternalUserMngInfo = users.Key("external_manage_info").String()
ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
- cfg.EditorsCanOwn = users.Key("editors_can_own").MustBool(false)
+ cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false)
// auth
auth := iniFile.Section("auth")
@@ -781,9 +787,20 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
enterprise := iniFile.Section("enterprise")
cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt"))
+ cacheServer := iniFile.Section("remote_cache")
+ cfg.RemoteCacheOptions = &RemoteCacheOptions{
+ Name: cacheServer.Key("type").MustString("database"),
+ ConnStr: cacheServer.Key("connstr").MustString(""),
+ }
+
return nil
}
+type RemoteCacheOptions struct {
+ Name string
+ ConnStr string
+}
+
func (cfg *Cfg) readSessionConfig() {
sec := cfg.Raw.Section("session")
SessionOptions = session.Options{}
diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go
index 12f2b6c03c9..c740d6cbe77 100644
--- a/pkg/tsdb/mssql/mssql.go
+++ b/pkg/tsdb/mssql/mssql.go
@@ -3,6 +3,7 @@ package mssql
import (
"database/sql"
"fmt"
+ "github.com/grafana/grafana/pkg/setting"
"strconv"
_ "github.com/denisenkom/go-mssqldb"
@@ -24,7 +25,9 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
if err != nil {
return nil, err
}
- logger.Debug("getEngine", "connection", cnnstr)
+ if setting.Env == setting.DEV {
+ logger.Debug("getEngine", "connection", cnnstr)
+ }
config := tsdb.SqlQueryEndpointConfiguration{
DriverName: "mssql",
diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go
index d307e12166c..0451f8f0dc1 100644
--- a/pkg/tsdb/mysql/mysql.go
+++ b/pkg/tsdb/mysql/mysql.go
@@ -3,6 +3,7 @@ package mysql
import (
"database/sql"
"fmt"
+ "github.com/grafana/grafana/pkg/setting"
"reflect"
"strconv"
"strings"
@@ -44,7 +45,9 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
cnnstr += "&tls=" + tlsConfigString
}
- logger.Debug("getEngine", "connection", cnnstr)
+ if setting.Env == setting.DEV {
+ logger.Debug("getEngine", "connection", cnnstr)
+ }
config := tsdb.SqlQueryEndpointConfiguration{
DriverName: "mysql",
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index 4bcf06638f4..ae6b165e731 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -2,6 +2,7 @@ package postgres
import (
"database/sql"
+ "github.com/grafana/grafana/pkg/setting"
"net/url"
"strconv"
@@ -19,7 +20,9 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp
logger := log.New("tsdb.postgres")
cnnstr := generateConnectionString(datasource)
- logger.Debug("getEngine", "connection", cnnstr)
+ if setting.Env == setting.DEV {
+ logger.Debug("getEngine", "connection", cnnstr)
+ }
config := tsdb.SqlQueryEndpointConfiguration{
DriverName: "postgres",
diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go
index 421a907b5e9..4780dd6e662 100644
--- a/pkg/tsdb/testdata/scenarios.go
+++ b/pkg/tsdb/testdata/scenarios.go
@@ -2,6 +2,7 @@ package testdata
import (
"encoding/json"
+ "math"
"math/rand"
"strconv"
"strings"
@@ -100,6 +101,15 @@ func init() {
},
})
+ registerScenario(&Scenario{
+ Id: "random_walk_table",
+ Name: "Random Walk Table",
+
+ Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
+ return getRandomWalkTable(query, context)
+ },
+ })
+
registerScenario(&Scenario{
Id: "slow_query",
Name: "Slow Query",
@@ -251,7 +261,7 @@ func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResu
series := newSeriesForQuery(query)
points := make(tsdb.TimeSeriesPoints, 0)
- walker := rand.Float64() * 100
+ walker := query.Model.Get("startValue").MustFloat64(rand.Float64() * 100)
for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
points = append(points, tsdb.NewTimePoint(null.FloatFrom(walker), float64(timeWalkerMs)))
@@ -267,6 +277,64 @@ func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResu
return queryRes
}
+func getRandomWalkTable(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult {
+ timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch()
+ to := tsdbQuery.TimeRange.GetToAsMsEpoch()
+
+ table := tsdb.Table{
+ Columns: []tsdb.TableColumn{
+ {Text: "Time"},
+ {Text: "Value"},
+ {Text: "Min"},
+ {Text: "Max"},
+ {Text: "Info"},
+ },
+ Rows: []tsdb.RowValues{},
+ }
+
+ withNil := query.Model.Get("withNil").MustBool(false)
+ walker := query.Model.Get("startValue").MustFloat64(rand.Float64() * 100)
+ spread := 2.5
+ var info strings.Builder
+
+ for i := int64(0); i < query.MaxDataPoints && timeWalkerMs < to; i++ {
+ delta := rand.Float64() - 0.5
+ walker += delta
+
+ info.Reset()
+ if delta > 0 {
+ info.WriteString("up")
+ } else {
+ info.WriteString("down")
+ }
+ if math.Abs(delta) > .4 {
+ info.WriteString(" fast")
+ }
+ row := tsdb.RowValues{
+ float64(timeWalkerMs),
+ walker,
+ walker - ((rand.Float64() * spread) + 0.01), // Min
+ walker + ((rand.Float64() * spread) + 0.01), // Max
+ info.String(),
+ }
+
+ // Add some random null values
+ if withNil && rand.Float64() > 0.8 {
+ for i := 1; i < 4; i++ {
+ if rand.Float64() > .2 {
+ row[i] = nil
+ }
+ }
+ }
+
+ table.Rows = append(table.Rows, row)
+ timeWalkerMs += query.IntervalMs
+ }
+ queryRes := tsdb.NewQueryResult()
+ queryRes.Tables = append(queryRes.Tables, &table)
+ return queryRes
+}
+
func registerScenario(scenario *Scenario) {
ScenarioRegistry[scenario.Id] = scenario
}
diff --git a/pkg/tsdb/testdata/scenarios_test.go b/pkg/tsdb/testdata/scenarios_test.go
new file mode 100644
index 00000000000..307f5e4affe
--- /dev/null
+++ b/pkg/tsdb/testdata/scenarios_test.go
@@ -0,0 +1,94 @@
+package testdata
+
+import (
+ "testing"
+ "time"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/tsdb"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestTestdataScenarios(t *testing.T) {
+ Convey("random walk ", t, func() {
+ scenario, _ := ScenarioRegistry["random_walk"]
+
+ Convey("Should start at the requested value", func() {
+ req := &tsdb.TsdbQuery{
+ TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()),
+ Queries: []*tsdb.Query{
+ {RefId: "A", IntervalMs: 100, MaxDataPoints: 100, Model: simplejson.New()},
+ },
+ }
+ query := req.Queries[0]
+ query.Model.Set("startValue", 1.234)
+
+ result := scenario.Handler(req.Queries[0], req)
+ points := result.Series[0].Points
+
+ So(result.Series, ShouldNotBeNil)
+ So(points[0][0].Float64, ShouldEqual, 1.234)
+ })
+ })
+
+ Convey("random walk table", t, func() {
+ scenario, _ := ScenarioRegistry["random_walk_table"]
+
+ Convey("Should return a table that looks like value/min/max", func() {
+ req := &tsdb.TsdbQuery{
+ TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()),
+ Queries: []*tsdb.Query{
+ {RefId: "A", IntervalMs: 100, MaxDataPoints: 100, Model: simplejson.New()},
+ },
+ }
+
+ result := scenario.Handler(req.Queries[0], req)
+ table := result.Tables[0]
+
+ So(len(table.Rows), ShouldBeGreaterThan, 50)
+ for _, row := range table.Rows {
+ value := row[1]
+ min := row[2]
+ max := row[3]
+
+ So(min, ShouldBeLessThan, value)
+ So(max, ShouldBeGreaterThan, value)
+ }
+ })
+
+ Convey("Should return a table with some nil values", func() {
+ req := &tsdb.TsdbQuery{
+ TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()),
+ Queries: []*tsdb.Query{
+ {RefId: "A", IntervalMs: 100, MaxDataPoints: 100, Model: simplejson.New()},
+ },
+ }
+ query := req.Queries[0]
+ query.Model.Set("withNil", true)
+
+ result := scenario.Handler(req.Queries[0], req)
+ table := result.Tables[0]
+
+ nil1 := false
+ nil2 := false
+ nil3 := false
+
+ So(len(table.Rows), ShouldBeGreaterThan, 50)
+ for _, row := range table.Rows {
+ if row[1] == nil {
+ nil1 = true
+ }
+ if row[2] == nil {
+ nil2 = true
+ }
+ if row[3] == nil {
+ nil3 = true
+ }
+ }
+
+ So(nil1, ShouldBeTrue)
+ So(nil2, ShouldBeTrue)
+ So(nil3, ShouldBeTrue)
+ })
+ })
+}
diff --git a/public/app/app.ts b/public/app/app.ts
index 03f332e357b..a54c9270e2c 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -17,12 +17,13 @@ import 'vendor/angular-other/angular-strap';
import $ from 'jquery';
import angular from 'angular';
import config from 'app/core/config';
+// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
import _ from 'lodash';
import moment from 'moment';
import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar';
// add move to lodash for backward compatabiltiy
-_.move = (array, fromIndex, toIndex) => {
+_.move = (array: [], fromIndex: number, toIndex: number) => {
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
return array;
};
@@ -36,7 +37,7 @@ import 'app/features/all';
// import symlinked extensions
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
-extensionsIndex.keys().forEach(key => {
+extensionsIndex.keys().forEach((key: any) => {
extensionsIndex(key);
});
@@ -52,7 +53,7 @@ export class GrafanaApp {
this.ngModuleDependencies = [];
}
- useModule(module) {
+ useModule(module: angular.IModule) {
if (this.preBootModules) {
this.preBootModules.push(module);
} else {
@@ -67,40 +68,49 @@ export class GrafanaApp {
moment.locale(config.bootData.user.locale);
- app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => {
- // pre assing bindings before constructor calls
- $compileProvider.preAssignBindingsEnabled(true);
+ app.config(
+ (
+ $locationProvider: angular.ILocationProvider,
+ $controllerProvider: angular.IControllerProvider,
+ $compileProvider: angular.ICompileProvider,
+ $filterProvider: angular.IFilterProvider,
+ $httpProvider: angular.IHttpProvider,
+ $provide: angular.auto.IProvideService
+ ) => {
+ // pre assing bindings before constructor calls
+ $compileProvider.preAssignBindingsEnabled(true);
- if (config.buildInfo.env !== 'development') {
- $compileProvider.debugInfoEnabled(false);
- }
+ if (config.buildInfo.env !== 'development') {
+ $compileProvider.debugInfoEnabled(false);
+ }
- $httpProvider.useApplyAsync(true);
+ $httpProvider.useApplyAsync(true);
- this.registerFunctions.controller = $controllerProvider.register;
- this.registerFunctions.directive = $compileProvider.directive;
- this.registerFunctions.factory = $provide.factory;
- this.registerFunctions.service = $provide.service;
- this.registerFunctions.filter = $filterProvider.register;
+ this.registerFunctions.controller = $controllerProvider.register;
+ this.registerFunctions.directive = $compileProvider.directive;
+ this.registerFunctions.factory = $provide.factory;
+ this.registerFunctions.service = $provide.service;
+ this.registerFunctions.filter = $filterProvider.register;
- $provide.decorator('$http', [
- '$delegate',
- '$templateCache',
- ($delegate, $templateCache) => {
- const get = $delegate.get;
- $delegate.get = (url, config) => {
- if (url.match(/\.html$/)) {
- // some template's already exist in the cache
- if (!$templateCache.get(url)) {
- url += '?v=' + new Date().getTime();
+ $provide.decorator('$http', [
+ '$delegate',
+ '$templateCache',
+ ($delegate: any, $templateCache: any) => {
+ const get = $delegate.get;
+ $delegate.get = (url: string, config: any) => {
+ if (url.match(/\.html$/)) {
+ // some template's already exist in the cache
+ if (!$templateCache.get(url)) {
+ url += '?v=' + new Date().getTime();
+ }
}
- }
- return get(url, config);
- };
- return $delegate;
- },
- ]);
- });
+ return get(url, config);
+ };
+ return $delegate;
+ },
+ ]);
+ }
+ );
this.ngModuleDependencies = [
'grafana.core',
@@ -116,7 +126,7 @@ export class GrafanaApp {
];
// makes it possible to add dynamic stuff
- _.each(angularModules, m => {
+ _.each(angularModules, (m: angular.IModule) => {
this.useModule(m);
});
@@ -129,7 +139,7 @@ export class GrafanaApp {
// bootstrap the app
angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
- _.each(this.preBootModules, module => {
+ _.each(this.preBootModules, (module: angular.IModule) => {
_.extend(module, this.registerFunctions);
});
diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx
index d667b54261e..a782a418b3c 100644
--- a/public/app/core/components/Animations/FadeIn.tsx
+++ b/public/app/core/components/Animations/FadeIn.tsx
@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC, CSSProperties } from 'react';
import Transition, { ExitHandler } from 'react-transition-group/Transition';
interface Props {
@@ -10,12 +10,12 @@ interface Props {
}
export const FadeIn: FC = props => {
- const defaultStyle = {
+ const defaultStyle: CSSProperties = {
transition: `opacity ${props.duration}ms linear`,
opacity: 0,
};
- const transitionStyles = {
+ const transitionStyles: { [str: string]: CSSProperties } = {
exited: { opacity: 0, display: 'none' },
entering: { opacity: 0 },
entered: { opacity: 1 },
diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx
index 6e8995298dc..c5cbf03b241 100644
--- a/public/app/core/components/Animations/SlideDown.tsx
+++ b/public/app/core/components/Animations/SlideDown.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { CSSProperties, FC } from 'react';
import Transition from 'react-transition-group/Transition';
interface Style {
@@ -16,11 +16,18 @@ export const defaultStyle: Style = {
overflow: 'hidden',
};
-export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
+export interface Props {
+ children: React.ReactNode;
+ in: boolean;
+ maxHeight?: number;
+ style?: CSSProperties;
+}
+
+export const SlideDown: FC = ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
// There are 4 main states a Transition can be in:
// ENTERING, ENTERED, EXITING, EXITED
- // https://reactcommunity.org/react-transition-group/
- const transitionStyles = {
+ // https://reactcommunity.or[g/react-transition-group/
+ const transitionStyles: { [str: string]: CSSProperties } = {
exited: { maxHeight: 0 },
entering: { maxHeight: maxHeight },
entered: { maxHeight: 'unset', overflow: 'visible' },
@@ -34,6 +41,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
style={{
...style,
...transitionStyles[state],
+ inProp,
}}
>
{children}
diff --git a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx
index ea63de58b47..bd9b51e6d4f 100644
--- a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx
+++ b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx
@@ -11,10 +11,10 @@ interface Props {
}
export class CopyToClipboard extends PureComponent {
- clipboardjs: any;
+ clipboardjs: ClipboardJS;
myRef: any;
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.myRef = React.createRef();
}
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
index 21700bb4d03..ac07b671c31 100644
--- a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
+++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import renderer from 'react-test-renderer';
+import { shallow } from 'enzyme';
import EmptyListCTA from './EmptyListCTA';
const model = {
@@ -16,7 +16,7 @@ const model = {
describe('EmptyListCTA', () => {
it('renders correctly', () => {
- const tree = renderer.create( ).toJSON();
+ const tree = shallow( );
expect(tree).toMatchSnapshot();
});
});
diff --git a/public/app/core/components/Form/index.ts b/public/app/core/components/Form/index.ts
deleted file mode 100644
index 6322cf3241a..00000000000
--- a/public/app/core/components/Form/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Input } from './Input';
diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx
index 73c055de94b..66f17444d1f 100644
--- a/public/app/core/components/JSONFormatter/JSONFormatter.tsx
+++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx
@@ -1,5 +1,4 @@
import React, { PureComponent, createRef } from 'react';
-// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
interface Props {
diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
index 171e0e8109e..a39eefcec4b 100644
--- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx
+++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import { FormLabel, Select } from '@grafana/ui';
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
-import { DashboardSearchHit } from 'app/types';
+import { DashboardSearchHit, DashboardSearchHitType } from 'app/types';
export interface Props {
resourceUri: string;
@@ -41,6 +41,21 @@ export class SharedPreferences extends PureComponent {
async componentDidMount() {
const prefs = await this.backendSrv.get(`/api/${this.props.resourceUri}/preferences`);
const dashboards = await this.backendSrv.search({ starred: true });
+ const defaultDashboardHit: DashboardSearchHit = {
+ id: 0,
+ title: 'Default',
+ tags: [],
+ type: '' as DashboardSearchHitType,
+ uid: '',
+ uri: '',
+ url: '',
+ folderId: 0,
+ folderTitle: '',
+ folderUid: '',
+ folderUrl: '',
+ isStarred: false,
+ slug: '',
+ };
if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) {
const missing = await this.backendSrv.search({ dashboardIds: [prefs.homeDashboardId] });
@@ -53,7 +68,7 @@ export class SharedPreferences extends PureComponent {
homeDashboardId: prefs.homeDashboardId,
theme: prefs.theme,
timezone: prefs.timezone,
- dashboards: [{ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' }, ...dashboards],
+ dashboards: [defaultDashboardHit, ...dashboards],
});
}
diff --git a/public/app/core/components/WithFeatureToggle.tsx b/public/app/core/components/WithFeatureToggle.tsx
new file mode 100644
index 00000000000..250697e0029
--- /dev/null
+++ b/public/app/core/components/WithFeatureToggle.tsx
@@ -0,0 +1,13 @@
+import React, { FunctionComponent } from 'react';
+
+export interface Props {
+ featureToggle: boolean;
+}
+
+export const WithFeatureToggle: FunctionComponent = ({ featureToggle, children }) => {
+ if (featureToggle === true) {
+ return <>{children}>;
+ }
+
+ return null;
+};
diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts
index 81d4b336443..b0c3e48b835 100644
--- a/public/app/core/components/form_dropdown/form_dropdown.ts
+++ b/public/app/core/components/form_dropdown/form_dropdown.ts
@@ -1,7 +1,8 @@
import _ from 'lodash';
import coreModule from '../../core_module';
+import { ISCEService, IQService } from 'angular';
-function typeaheadMatcher(this: any, item) {
+function typeaheadMatcher(this: any, item: string) {
let str = this.query;
if (str === '') {
return true;
@@ -16,8 +17,8 @@ function typeaheadMatcher(this: any, item) {
}
export class FormDropdownCtrl {
- inputElement: any;
- linkElement: any;
+ inputElement: JQLite;
+ linkElement: JQLite;
model: any;
display: any;
text: any;
@@ -37,7 +38,13 @@ export class FormDropdownCtrl {
debounce: number;
/** @ngInject */
- constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
+ constructor(
+ private $scope: any,
+ $element: JQLite,
+ private $sce: ISCEService,
+ private templateSrv: any,
+ private $q: IQService
+ ) {
this.inputElement = $element.find('input').first();
this.linkElement = $element.find('a').first();
this.linkMode = true;
@@ -99,7 +106,7 @@ export class FormDropdownCtrl {
}
}
- getOptionsInternal(query) {
+ getOptionsInternal(query: string) {
const result = this.getOptions({ $query: query });
if (this.isPromiseLike(result)) {
return result;
@@ -107,7 +114,7 @@ export class FormDropdownCtrl {
return this.$q.when(result);
}
- isPromiseLike(obj) {
+ isPromiseLike(obj: any) {
return obj && typeof obj.then === 'function';
}
@@ -117,7 +124,7 @@ export class FormDropdownCtrl {
} else {
// if we have text use it
if (this.lookupText) {
- this.getOptionsInternal('').then(options => {
+ this.getOptionsInternal('').then((options: any) => {
const item = _.find(options, { value: this.model });
this.updateDisplay(item ? item.text : this.model);
});
@@ -127,12 +134,12 @@ export class FormDropdownCtrl {
}
}
- typeaheadSource(query, callback) {
- this.getOptionsInternal(query).then(options => {
+ typeaheadSource(query: string, callback: (res: any) => void) {
+ this.getOptionsInternal(query).then((options: any) => {
this.optionCache = options;
// extract texts
- const optionTexts = _.map(options, op => {
+ const optionTexts = _.map(options, (op: any) => {
return _.escape(op.text);
});
@@ -147,7 +154,7 @@ export class FormDropdownCtrl {
});
}
- typeaheadUpdater(text) {
+ typeaheadUpdater(text: string) {
if (text === this.text) {
clearTimeout(this.cancelBlur);
this.inputElement.focus();
@@ -159,7 +166,7 @@ export class FormDropdownCtrl {
return text;
}
- switchToLink(fromClick) {
+ switchToLink(fromClick: boolean) {
if (this.linkMode && !fromClick) {
return;
}
@@ -178,7 +185,7 @@ export class FormDropdownCtrl {
this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
}
- updateValue(text) {
+ updateValue(text: string) {
text = _.unescape(text);
if (text === '' || this.text === text) {
@@ -214,7 +221,7 @@ export class FormDropdownCtrl {
});
}
- updateDisplay(text) {
+ updateDisplay(text: string) {
this.text = text;
this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
}
diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts
index e4c92f662b1..f17f0f7ad7f 100644
--- a/public/app/core/components/json_explorer/json_explorer.ts
+++ b/public/app/core/components/json_explorer/json_explorer.ts
@@ -232,7 +232,7 @@ export class JsonExplorer {
// some pretty handling of number arrays
if (this.isNumberArray()) {
- this.json.forEach((val, index) => {
+ this.json.forEach((val: any, index: number) => {
if (index > 0) {
arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
}
diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts
index b3f3cdc14d1..a72a2fd5b4a 100644
--- a/public/app/core/components/layout_selector/layout_selector.ts
+++ b/public/app/core/components/layout_selector/layout_selector.ts
@@ -16,7 +16,7 @@ export class LayoutSelectorCtrl {
mode: string;
/** @ngInject */
- constructor(private $rootScope) {
+ constructor(private $rootScope: any) {
this.mode = store.get('grafana.list.layout.mode') || 'grid';
}
@@ -46,18 +46,18 @@ export function layoutSelector() {
}
/** @ngInject */
-export function layoutMode($rootScope) {
+export function layoutMode($rootScope: any) {
return {
restrict: 'A',
scope: {},
- link: (scope, elem) => {
+ link: (scope: any, elem: any) => {
const layout = store.get('grafana.list.layout.mode') || 'grid';
let className = 'card-list-layout-' + layout;
elem.addClass(className);
$rootScope.onAppEvent(
'layout-mode-changed',
- (evt, newLayout) => {
+ (evt: any, newLayout: any) => {
elem.removeClass(className);
className = 'card-list-layout-' + newLayout;
elem.addClass(className);
diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts
index 25a69b1f5e4..5b5e299b1af 100644
--- a/public/app/core/components/manage_dashboards/manage_dashboards.ts
+++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts
@@ -1,7 +1,31 @@
+// @ts-ignore
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { SearchSrv } from 'app/core/services/search_srv';
+import { BackendSrv } from 'app/core/services/backend_srv';
+import { NavModelSrv } from 'app/core/nav_model_srv';
+import { ContextSrv } from 'app/core/services/context_srv';
+
+export interface Section {
+ id: number;
+ uid: string;
+ title: string;
+ expanded: boolean;
+ removable: boolean;
+ items: any[];
+ url: string;
+ icon: string;
+ score: number;
+ checked: boolean;
+ hideHeader: boolean;
+ toggle: Function;
+}
+
+export interface FoldersAndDashboardUids {
+ folderUids: string[];
+ dashboardUids: string[];
+}
class Query {
query: string;
@@ -14,7 +38,7 @@ class Query {
}
export class ManageDashboardsCtrl {
- sections: any[];
+ sections: Section[];
query: Query;
navModel: any;
@@ -45,7 +69,12 @@ export class ManageDashboardsCtrl {
hasEditPermissionInFolders: boolean;
/** @ngInject */
- constructor(private backendSrv, navModelSrv, private searchSrv: SearchSrv, private contextSrv) {
+ constructor(
+ private backendSrv: BackendSrv,
+ navModelSrv: NavModelSrv,
+ private searchSrv: SearchSrv,
+ private contextSrv: ContextSrv
+ ) {
this.isEditor = this.contextSrv.isEditor;
this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders;
@@ -73,7 +102,7 @@ export class ManageDashboardsCtrl {
refreshList() {
return this.searchSrv
.search(this.query)
- .then(result => {
+ .then((result: Section[]) => {
return this.initDashboardList(result);
})
.then(() => {
@@ -81,7 +110,7 @@ export class ManageDashboardsCtrl {
return;
}
- return this.backendSrv.getFolderByUid(this.folderUid).then(folder => {
+ return this.backendSrv.getFolderByUid(this.folderUid).then((folder: any) => {
this.canSave = folder.canSave;
if (!this.canSave) {
this.hasEditPermissionInFolders = false;
@@ -90,7 +119,7 @@ export class ManageDashboardsCtrl {
});
}
- initDashboardList(result: any) {
+ initDashboardList(result: Section[]) {
this.canMove = false;
this.canDelete = false;
this.selectAllChecked = false;
@@ -128,25 +157,25 @@ export class ManageDashboardsCtrl {
this.canDelete = selectedDashboards > 0 || selectedFolders > 0;
}
- getFoldersAndDashboardsToDelete() {
- const selectedDashboards = {
- folders: [],
- dashboards: [],
+ getFoldersAndDashboardsToDelete(): FoldersAndDashboardUids {
+ const selectedDashboards: FoldersAndDashboardUids = {
+ folderUids: [],
+ dashboardUids: [],
};
for (const section of this.sections) {
if (section.checked && section.id !== 0) {
- selectedDashboards.folders.push(section.uid);
+ selectedDashboards.folderUids.push(section.uid);
} else {
const selected = _.filter(section.items, { checked: true });
- selectedDashboards.dashboards.push(..._.map(selected, 'uid'));
+ selectedDashboards.dashboardUids.push(..._.map(selected, 'uid'));
}
}
return selectedDashboards;
}
- getFolderIds(sections) {
+ getFolderIds(sections: Section[]) {
const ids = [];
for (const s of sections) {
if (s.checked) {
@@ -158,8 +187,8 @@ export class ManageDashboardsCtrl {
delete() {
const data = this.getFoldersAndDashboardsToDelete();
- const folderCount = data.folders.length;
- const dashCount = data.dashboards.length;
+ const folderCount = data.folderUids.length;
+ const dashCount = data.dashboardUids.length;
let text = 'Do you want to delete the ';
let text2;
@@ -179,12 +208,12 @@ export class ManageDashboardsCtrl {
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
- this.deleteFoldersAndDashboards(data.folders, data.dashboards);
+ this.deleteFoldersAndDashboards(data.folderUids, data.dashboardUids);
},
});
}
- private deleteFoldersAndDashboards(folderUids, dashboardUids) {
+ private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
this.refreshList();
});
@@ -219,13 +248,13 @@ export class ManageDashboardsCtrl {
}
initTagFilter() {
- return this.searchSrv.getDashboardTags().then(results => {
+ return this.searchSrv.getDashboardTags().then((results: any) => {
this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results);
this.selectedTagFilter = this.tagFilterOptions[0];
});
}
- filterByTag(tag) {
+ filterByTag(tag: any) {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
}
@@ -243,7 +272,7 @@ export class ManageDashboardsCtrl {
return res;
}
- removeTag(tag, evt) {
+ removeTag(tag: any, evt: Event) {
this.query.tag = _.without(this.query.tag, tag);
this.refreshList();
if (evt) {
@@ -269,7 +298,7 @@ export class ManageDashboardsCtrl {
section.checked = this.selectAllChecked;
}
- section.items = _.map(section.items, item => {
+ section.items = _.map(section.items, (item: any) => {
item.checked = this.selectAllChecked;
return item;
});
diff --git a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx
index b52e5311dc5..5ba4503da13 100644
--- a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx
+++ b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx
@@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => {
{
link: {},
user: {
+ id: 1,
isGrafanaAdmin: false,
isSignedIn: false,
orgCount: 2,
diff --git a/public/app/core/config.ts b/public/app/core/config.ts
index bbf7fb88d62..fe9005973b8 100644
--- a/public/app/core/config.ts
+++ b/public/app/core/config.ts
@@ -13,7 +13,7 @@ export interface BuildInfo {
export class Settings {
datasources: any;
- panels: PanelPlugin[];
+ panels: { [key: string]: PanelPlugin };
appSubUrl: string;
windowTitlePrefix: string;
buildInfo: BuildInfo;
@@ -37,7 +37,7 @@ export class Settings {
passwordHint: any;
loginError: any;
viewersCanEdit: boolean;
- editorsCanOwn: boolean;
+ editorsCanAdmin: boolean;
disableSanitizeHtml: boolean;
theme: GrafanaTheme;
@@ -59,7 +59,7 @@ export class Settings {
isEnterprise: false,
},
viewersCanEdit: false,
- editorsCanOwn: false,
+ editorsCanAdmin: false,
disableSanitizeHtml: false,
};
diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts
index c73cc7661f5..0d7d098dcea 100644
--- a/public/app/core/services/backend_srv.ts
+++ b/public/app/core/services/backend_srv.ts
@@ -3,6 +3,7 @@ import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
+import { DashboardSearchHit } from 'app/types/search';
export class BackendSrv {
private inFlightRequests = {};
@@ -237,7 +238,7 @@ export class BackendSrv {
return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 });
}
- search(query) {
+ search(query): Promise {
return this.get('/api/search', query);
}
diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts
index e3b10f129d1..214c7efb782 100644
--- a/public/app/core/services/context_srv.ts
+++ b/public/app/core/services/context_srv.ts
@@ -3,6 +3,7 @@ import _ from 'lodash';
import coreModule from 'app/core/core_module';
export class User {
+ id: number;
isGrafanaAdmin: any;
isSignedIn: any;
orgRole: any;
diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts
index 22d33921ebd..068fe3ffbc3 100644
--- a/public/app/core/services/search_srv.ts
+++ b/public/app/core/services/search_srv.ts
@@ -1,21 +1,32 @@
+// @ts-ignore
import _ from 'lodash';
+// @ts-ignore
+import { IQService } from 'angular';
+
import coreModule from 'app/core/core_module';
import impressionSrv from 'app/core/services/impression_srv';
import store from 'app/core/store';
import { contextSrv } from 'app/core/services/context_srv';
+import { BackendSrv } from './backend_srv';
+import { Section } from '../components/manage_dashboards/manage_dashboards';
+import { DashboardSearchHit } from 'app/types/search';
+
+interface Sections {
+ [key: string]: Partial;
+}
export class SearchSrv {
recentIsOpen: boolean;
starredIsOpen: boolean;
/** @ngInject */
- constructor(private backendSrv, private $q) {
+ constructor(private backendSrv: BackendSrv, private $q: IQService) {
this.recentIsOpen = store.getBool('search.sections.recent', true);
this.starredIsOpen = store.getBool('search.sections.starred', true);
}
- private getRecentDashboards(sections) {
- return this.queryForRecentDashboards().then(result => {
+ private getRecentDashboards(sections: Sections) {
+ return this.queryForRecentDashboards().then((result: any[]) => {
if (result.length > 0) {
sections['recent'] = {
title: 'Recent',
@@ -30,8 +41,8 @@ export class SearchSrv {
});
}
- private queryForRecentDashboards() {
- const dashIds = _.take(impressionSrv.getDashboardOpened(), 30);
+ private queryForRecentDashboards(): Promise {
+ const dashIds: number[] = _.take(impressionSrv.getDashboardOpened(), 30);
if (dashIds.length === 0) {
return Promise.resolve([]);
}
@@ -45,7 +56,7 @@ export class SearchSrv {
});
}
- private toggleRecent(section) {
+ private toggleRecent(section: Section) {
this.recentIsOpen = section.expanded = !section.expanded;
store.set('search.sections.recent', this.recentIsOpen);
@@ -59,13 +70,13 @@ export class SearchSrv {
});
}
- private toggleStarred(section) {
+ private toggleStarred(section: Section) {
this.starredIsOpen = section.expanded = !section.expanded;
store.set('search.sections.starred', this.starredIsOpen);
return Promise.resolve(section);
}
- private getStarred(sections) {
+ private getStarred(sections: Sections) {
if (!contextSrv.isSignedIn) {
return Promise.resolve();
}
@@ -84,7 +95,7 @@ export class SearchSrv {
});
}
- search(options) {
+ search(options: any) {
const sections: any = {};
const promises = [];
const query = _.clone(options);
@@ -118,7 +129,7 @@ export class SearchSrv {
});
}
- private handleSearchResult(sections, results) {
+ private handleSearchResult(sections: Sections, results: DashboardSearchHit[]): any {
if (results.length === 0) {
return sections;
}
@@ -177,7 +188,7 @@ export class SearchSrv {
}
}
- private toggleFolder(section) {
+ private toggleFolder(section: Section) {
section.expanded = !section.expanded;
section.icon = section.expanded ? 'fa fa-folder-open' : 'fa fa-folder';
diff --git a/public/app/core/specs/manage_dashboards.test.ts b/public/app/core/specs/manage_dashboards.test.ts
index 5af0ebade02..5e94d26ea89 100644
--- a/public/app/core/specs/manage_dashboards.test.ts
+++ b/public/app/core/specs/manage_dashboards.test.ts
@@ -1,12 +1,40 @@
-import { ManageDashboardsCtrl } from 'app/core/components/manage_dashboards/manage_dashboards';
-import { SearchSrv } from 'app/core/services/search_srv';
+// @ts-ignore
import q from 'q';
+import {
+ ManageDashboardsCtrl,
+ Section,
+ FoldersAndDashboardUids,
+} from 'app/core/components/manage_dashboards/manage_dashboards';
+import { SearchSrv } from 'app/core/services/search_srv';
+import { BackendSrv } from '../services/backend_srv';
+import { NavModelSrv } from '../nav_model_srv';
+import { ContextSrv } from '../services/context_srv';
+
+const mockSection = (overides?: object): Section => {
+ const defaultSection: Section = {
+ id: 0,
+ items: [],
+ checked: false,
+ expanded: false,
+ removable: false,
+ hideHeader: false,
+ icon: '',
+ score: 0,
+ title: 'Some Section',
+ toggle: jest.fn(),
+ uid: 'someuid',
+ url: '/some/url/',
+ };
+
+ return { ...defaultSection, ...overides };
+};
describe('ManageDashboards', () => {
- let ctrl;
+ let ctrl: ManageDashboardsCtrl;
describe('when browsing dashboards', () => {
beforeEach(() => {
+ const tags: any[] = [];
const response = [
{
id: 410,
@@ -18,11 +46,11 @@ describe('ManageDashboards', () => {
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'fa fa-folder',
- tags: [],
+ tags,
isStarred: false,
},
],
- tags: [],
+ tags,
isStarred: false,
},
{
@@ -37,11 +65,11 @@ describe('ManageDashboards', () => {
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'fa fa-folder',
- tags: [],
+ tags,
isStarred: false,
},
],
- tags: [],
+ tags,
isStarred: false,
},
];
@@ -61,6 +89,7 @@ describe('ManageDashboards', () => {
describe('when browsing dashboards for a folder', () => {
beforeEach(() => {
+ const tags: any[] = [];
const response = [
{
id: 410,
@@ -72,11 +101,11 @@ describe('ManageDashboards', () => {
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'fa fa-folder',
- tags: [],
+ tags,
isStarred: false,
},
],
- tags: [],
+ tags,
isStarred: false,
},
];
@@ -92,6 +121,7 @@ describe('ManageDashboards', () => {
describe('when searching dashboards', () => {
beforeEach(() => {
+ const tags: any[] = [];
const response = [
{
checked: false,
@@ -103,7 +133,7 @@ describe('ManageDashboards', () => {
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'fa fa-folder',
- tags: [],
+ tags,
isStarred: false,
folderId: 410,
folderUid: 'uid',
@@ -115,7 +145,7 @@ describe('ManageDashboards', () => {
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'fa fa-folder',
- tags: [],
+ tags,
folderId: 499,
isStarred: false,
},
@@ -245,7 +275,7 @@ describe('ManageDashboards', () => {
});
describe('when selecting dashboards', () => {
- let ctrl;
+ let ctrl: ManageDashboardsCtrl;
beforeEach(() => {
ctrl = createCtrlWithStubs([]);
@@ -254,16 +284,16 @@ describe('ManageDashboards', () => {
describe('and no dashboards are selected', () => {
beforeEach(() => {
ctrl.sections = [
- {
+ mockSection({
id: 1,
items: [{ id: 2, checked: false }],
checked: false,
- },
- {
+ }),
+ mockSection({
id: 0,
items: [{ id: 3, checked: false }],
checked: false,
- },
+ }),
];
ctrl.selectionChanged();
});
@@ -302,16 +332,16 @@ describe('ManageDashboards', () => {
describe('and all folders and dashboards are selected', () => {
beforeEach(() => {
ctrl.sections = [
- {
+ mockSection({
id: 1,
items: [{ id: 2, checked: true }],
checked: true,
- },
- {
+ }),
+ mockSection({
id: 0,
items: [{ id: 3, checked: true }],
checked: true,
- },
+ }),
];
ctrl.selectionChanged();
});
@@ -350,18 +380,18 @@ describe('ManageDashboards', () => {
describe('and one dashboard in root is selected', () => {
beforeEach(() => {
ctrl.sections = [
- {
+ mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: false }],
checked: false,
- },
- {
+ }),
+ mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true }],
checked: false,
- },
+ }),
];
ctrl.selectionChanged();
});
@@ -378,18 +408,18 @@ describe('ManageDashboards', () => {
describe('and one child dashboard is selected', () => {
beforeEach(() => {
ctrl.sections = [
- {
+ mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: true }],
checked: false,
- },
- {
+ }),
+ mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: false }],
checked: false,
- },
+ }),
];
ctrl.selectionChanged();
@@ -407,18 +437,18 @@ describe('ManageDashboards', () => {
describe('and one child dashboard and one dashboard is selected', () => {
beforeEach(() => {
ctrl.sections = [
- {
+ mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: true }],
checked: false,
- },
- {
+ }),
+ mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true }],
checked: false,
- },
+ }),
];
ctrl.selectionChanged();
@@ -436,24 +466,24 @@ describe('ManageDashboards', () => {
describe('and one child dashboard and one folder is selected', () => {
beforeEach(() => {
ctrl.sections = [
- {
+ mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: false }],
checked: true,
- },
- {
+ }),
+ mockSection({
id: 3,
title: 'folder',
items: [{ id: 4, checked: true }],
checked: false,
- },
- {
+ }),
+ mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: false }],
checked: false,
- },
+ }),
];
ctrl.selectionChanged();
@@ -470,55 +500,55 @@ describe('ManageDashboards', () => {
});
describe('when deleting dashboards', () => {
- let toBeDeleted: any;
+ let toBeDeleted: FoldersAndDashboardUids;
beforeEach(() => {
ctrl = createCtrlWithStubs([]);
ctrl.sections = [
- {
+ mockSection({
id: 1,
uid: 'folder',
title: 'folder',
items: [{ id: 2, checked: true, uid: 'folder-dash' }],
checked: true,
- },
- {
+ }),
+ mockSection({
id: 3,
title: 'folder-2',
items: [{ id: 3, checked: true, uid: 'folder-2-dash' }],
checked: false,
uid: 'folder-2',
- },
- {
+ }),
+ mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true, uid: 'root-dash' }],
checked: true,
- },
+ }),
];
toBeDeleted = ctrl.getFoldersAndDashboardsToDelete();
});
it('should return 1 folder', () => {
- expect(toBeDeleted.folders.length).toEqual(1);
+ expect(toBeDeleted.folderUids.length).toEqual(1);
});
it('should return 2 dashboards', () => {
- expect(toBeDeleted.dashboards.length).toEqual(2);
+ expect(toBeDeleted.dashboardUids.length).toEqual(2);
});
it('should filter out children if parent is checked', () => {
- expect(toBeDeleted.folders[0]).toEqual('folder');
+ expect(toBeDeleted.folderUids[0]).toEqual('folder');
});
it('should not filter out children if parent not is checked', () => {
- expect(toBeDeleted.dashboards[0]).toEqual('folder-2-dash');
+ expect(toBeDeleted.dashboardUids[0]).toEqual('folder-2-dash');
});
it('should not filter out children if parent is checked and root', () => {
- expect(toBeDeleted.dashboards[1]).toEqual('root-dash');
+ expect(toBeDeleted.dashboardUids[1]).toEqual('root-dash');
});
});
@@ -527,19 +557,19 @@ describe('ManageDashboards', () => {
ctrl = createCtrlWithStubs([]);
ctrl.sections = [
- {
+ mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: true, uid: 'dash' }],
checked: false,
uid: 'folder',
- },
- {
+ }),
+ mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true, uid: 'dash-2' }],
checked: false,
- },
+ }),
];
});
@@ -562,5 +592,10 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) {
},
};
- return new ManageDashboardsCtrl({}, { getNav: () => {} }, searchSrvStub as SearchSrv, { isEditor: true });
+ return new ManageDashboardsCtrl(
+ {} as BackendSrv,
+ { getNav: () => {} } as NavModelSrv,
+ searchSrvStub as SearchSrv,
+ { isEditor: true } as ContextSrv
+ );
}
diff --git a/public/app/core/specs/search_srv.test.ts b/public/app/core/specs/search_srv.test.ts
index 550d11bbf9e..6566224c613 100644
--- a/public/app/core/specs/search_srv.test.ts
+++ b/public/app/core/specs/search_srv.test.ts
@@ -1,8 +1,12 @@
+// @ts-ignore
+import { IQService } from 'angular';
+
import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrvMock } from 'test/mocks/backend_srv';
import impressionSrv from 'app/core/services/impression_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { beforeEach } from 'test/lib/common';
+import { BackendSrv } from '../services/backend_srv';
jest.mock('app/core/store', () => {
return {
@@ -18,18 +22,18 @@ jest.mock('app/core/services/impression_srv', () => {
});
describe('SearchSrv', () => {
- let searchSrv, backendSrvMock;
+ let searchSrv: SearchSrv, backendSrvMock: BackendSrvMock;
beforeEach(() => {
backendSrvMock = new BackendSrvMock();
- searchSrv = new SearchSrv(backendSrvMock, Promise);
+ searchSrv = new SearchSrv(backendSrvMock as BackendSrv, (Promise as any) as IQService);
contextSrv.isSignedIn = true;
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
});
describe('With recent dashboards', () => {
- let results;
+ let results: any;
beforeEach(() => {
backendSrvMock.search = jest
@@ -56,7 +60,7 @@ describe('SearchSrv', () => {
});
describe('and 3 recent dashboards removed in backend', () => {
- let results;
+ let results: any;
beforeEach(() => {
backendSrvMock.search = jest
@@ -80,7 +84,7 @@ describe('SearchSrv', () => {
});
describe('With starred dashboards', () => {
- let results;
+ let results: any;
beforeEach(() => {
backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }]));
@@ -97,7 +101,7 @@ describe('SearchSrv', () => {
});
describe('With starred dashboards and recent', () => {
- let results;
+ let results: any;
beforeEach(() => {
backendSrvMock.search = jest
@@ -125,7 +129,7 @@ describe('SearchSrv', () => {
});
describe('with no query string and dashboards with folders returned', () => {
- let results;
+ let results: any;
beforeEach(() => {
backendSrvMock.search = jest
@@ -173,12 +177,10 @@ describe('SearchSrv', () => {
});
describe('with query string and dashboards with folders returned', () => {
- let results;
+ let results: any;
beforeEach(() => {
- backendSrvMock.search = jest.fn();
-
- backendSrvMock.search.mockReturnValue(
+ backendSrvMock.search = jest.fn().mockReturnValue(
Promise.resolve([
{
id: 2,
@@ -249,8 +251,9 @@ describe('SearchSrv', () => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
- searchSrv.getRecentDashboards = () => {
+ searchSrv['getRecentDashboards'] = () => {
getRecentDashboardsCalled = true;
+ return Promise.resolve();
};
return searchSrv.search({ skipRecent: true }).then(() => {});
@@ -269,8 +272,9 @@ describe('SearchSrv', () => {
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
- searchSrv.getStarred = () => {
+ searchSrv['getStarred'] = () => {
getStarredCalled = true;
+ return Promise.resolve();
};
return searchSrv.search({ skipStarred: true }).then(() => {});
diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts
index fa7170bed13..291689941f7 100644
--- a/public/app/core/table_model.ts
+++ b/public/app/core/table_model.ts
@@ -1,17 +1,18 @@
import _ from 'lodash';
+import { Column, TableData } from '@grafana/ui';
-interface Column {
- text: string;
+/**
+ * Extends the standard Column class with variables that get
+ * mutated in the angular table panel.
+ */
+interface MutableColumn extends Column {
title?: string;
- type?: string;
sort?: boolean;
desc?: boolean;
- filterable?: boolean;
- unit?: string;
}
-export default class TableModel {
- columns: Column[];
+export default class TableModel implements TableData {
+ columns: MutableColumn[];
rows: any[];
type: string;
columnMap: any;
diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts
index 31e5a392050..fdc63b931f7 100644
--- a/public/app/core/utils/explore.ts
+++ b/public/app/core/utils/explore.ts
@@ -9,6 +9,7 @@ import store from 'app/core/store';
import { parse as parseDate } from 'app/core/utils/datemath';
import { colors } from '@grafana/ui';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
+import { getNextRefIdChar } from './query';
// Types
import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui';
@@ -225,12 +226,8 @@ export function generateKey(index = 0): string {
return `Q-${Date.now()}-${Math.random()}-${index}`;
}
-export function generateRefId(index = 0): string {
- return `${index + 1}`;
-}
-
-export function generateEmptyQuery(index = 0): { refId: string; key: string } {
- return { refId: generateRefId(index), key: generateKey(index) };
+export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery {
+ return { refId: getNextRefIdChar(queries), key: generateKey(index) };
}
/**
@@ -238,9 +235,9 @@ export function generateEmptyQuery(index = 0): { refId: string; key: string } {
*/
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
if (queries && typeof queries === 'object' && queries.length > 0) {
- return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
+ return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(queries, i) }));
}
- return [{ ...generateEmptyQuery() }];
+ return [{ ...generateEmptyQuery(queries) }];
}
/**
diff --git a/public/app/core/utils/query.test.ts b/public/app/core/utils/query.test.ts
new file mode 100644
index 00000000000..a69162751a4
--- /dev/null
+++ b/public/app/core/utils/query.test.ts
@@ -0,0 +1,30 @@
+import { DataQuery } from '@grafana/ui';
+import { getNextRefIdChar } from './query';
+
+const dataQueries: DataQuery[] = [
+ {
+ refId: 'A',
+ },
+ {
+ refId: 'B',
+ },
+ {
+ refId: 'C',
+ },
+ {
+ refId: 'D',
+ },
+ {
+ refId: 'E',
+ },
+];
+
+describe('Get next refId char', () => {
+ it('should return next char', () => {
+ expect(getNextRefIdChar(dataQueries)).toEqual('F');
+ });
+
+ it('should get first char', () => {
+ expect(getNextRefIdChar([])).toEqual('A');
+ });
+});
diff --git a/public/app/core/utils/query.ts b/public/app/core/utils/query.ts
new file mode 100644
index 00000000000..933a73138a8
--- /dev/null
+++ b/public/app/core/utils/query.ts
@@ -0,0 +1,12 @@
+import _ from 'lodash';
+import { DataQuery } from '@grafana/ui/';
+
+export const getNextRefIdChar = (queries: DataQuery[]): string => {
+ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+ return _.find(letters, refId => {
+ return _.every(queries, other => {
+ return other.refId !== refId;
+ });
+ });
+};
diff --git a/public/app/core/utils/validate.ts b/public/app/core/utils/validate.ts
deleted file mode 100644
index c6663882808..00000000000
--- a/public/app/core/utils/validate.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ValidationRule, ValidationEvents } from 'app/types';
-import { EventsWithValidation } from 'app/core/components/Form/Input';
-
-export const validate = (value: string, validationRules: ValidationRule[]) => {
- const errors = validationRules.reduce((acc, currRule) => {
- if (!currRule.rule(value)) {
- return acc.concat(currRule.errorMessage);
- }
- return acc;
- }, []);
- return errors.length > 0 ? errors : null;
-};
-
-export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents) => {
- return validationEvents && validationEvents[event];
-};
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx
index 4be5012a985..538f21d0d34 100644
--- a/public/app/features/api-keys/ApiKeysPage.tsx
+++ b/public/app/features/api-keys/ApiKeysPage.tsx
@@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
import Page from 'app/core/components/Page/Page';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
index ac1b5f08632..1562953fd1b 100644
--- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
+++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
@@ -4,13 +4,16 @@ jest.mock('app/core/store', () => {
};
});
+// @ts-ignore
import _ from 'lodash';
import config from 'app/core/config';
import { DashboardExporter } from './DashboardExporter';
import { DashboardModel } from '../../state/DashboardModel';
+import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { PanelPlugin } from 'app/types';
describe('given dashboard with repeated panels', () => {
- let dash, exported;
+ let dash: any, exported: any;
beforeEach(done => {
dash = {
@@ -89,25 +92,25 @@ describe('given dashboard with repeated panels', () => {
config.buildInfo.version = '3.0.2';
//Stubs test function calls
- const datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };
+ const datasourceSrvStub = ({ get: jest.fn(arg => getStub(arg)) } as any) as DatasourceSrv;
config.panels['graph'] = {
id: 'graph',
name: 'Graph',
info: { version: '1.1.0' },
- };
+ } as PanelPlugin;
config.panels['table'] = {
id: 'table',
name: 'Table',
info: { version: '1.1.1' },
- };
+ } as PanelPlugin;
config.panels['heatmap'] = {
id: 'heatmap',
name: 'Heatmap',
info: { version: '1.1.2' },
- };
+ } as PanelPlugin;
dash = new DashboardModel(dash, {});
const exporter = new DashboardExporter(datasourceSrvStub);
@@ -213,7 +216,7 @@ describe('given dashboard with repeated panels', () => {
});
// Stub responses
-const stubs = [];
+const stubs: { [key: string]: {} } = {};
stubs['gfdb'] = {
name: 'gfdb',
meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
@@ -249,6 +252,6 @@ stubs['-- Grafana --'] = {
},
};
-function getStub(arg) {
+function getStub(arg: string) {
return Promise.resolve(stubs[arg || 'gfdb']);
}
diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
index 6cf14f81c86..4150cb9a848 100644
--- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
+++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
@@ -1,9 +1,42 @@
-import config from 'app/core/config';
+// @ts-ignore
import _ from 'lodash';
+
+import config from 'app/core/config';
import { DashboardModel } from '../../state/DashboardModel';
+import DatasourceSrv from 'app/features/plugins/datasource_srv';
+import { PanelModel } from 'app/features/dashboard/state';
+import { PanelPlugin } from 'app/types/plugins';
+
+interface Input {
+ name: string;
+ type: string;
+ label: string;
+ value: any;
+ description: string;
+}
+
+interface Requires {
+ [key: string]: {
+ type: string;
+ id: string;
+ name: string;
+ version: string;
+ };
+}
+
+interface DataSources {
+ [key: string]: {
+ name: string;
+ label: string;
+ description: string;
+ type: string;
+ pluginId: string;
+ pluginName: string;
+ };
+}
export class DashboardExporter {
- constructor(private datasourceSrv) {}
+ constructor(private datasourceSrv: DatasourceSrv) {}
makeExportable(dashboard: DashboardModel) {
// clean up repeated rows and panels,
@@ -18,19 +51,19 @@ export class DashboardExporter {
// undo repeat cleanup
dashboard.processRepeats();
- const inputs = [];
- const requires = {};
- const datasources = {};
- const promises = [];
- const variableLookup: any = {};
+ const inputs: Input[] = [];
+ const requires: Requires = {};
+ const datasources: DataSources = {};
+ const promises: Array> = [];
+ const variableLookup: { [key: string]: any } = {};
for (const variable of saveModel.templating.list) {
variableLookup[variable.name] = variable;
}
- const templateizeDatasourceUsage = obj => {
- let datasource = obj.datasource;
- let datasourceVariable = null;
+ const templateizeDatasourceUsage = (obj: any) => {
+ let datasource: string = obj.datasource;
+ let datasourceVariable: any = null;
// ignore data source properties that contain a variable
if (datasource && datasource.indexOf('$') === 0) {
@@ -74,7 +107,7 @@ export class DashboardExporter {
);
};
- const processPanel = panel => {
+ const processPanel = (panel: PanelModel) => {
if (panel.datasource !== undefined) {
templateizeDatasourceUsage(panel);
}
@@ -87,7 +120,7 @@ export class DashboardExporter {
}
}
- const panelDef = config.panels[panel.type];
+ const panelDef: PanelPlugin = config.panels[panel.type];
if (panelDef) {
requires['panel' + panelDef.id] = {
type: 'panel',
@@ -135,7 +168,7 @@ export class DashboardExporter {
return Promise.all(promises)
.then(() => {
- _.each(datasources, (value, key) => {
+ _.each(datasources, (value: any) => {
inputs.push(value);
});
@@ -160,7 +193,7 @@ export class DashboardExporter {
}
// make inputs and requires a top thing
- const newObj = {};
+ const newObj: { [key: string]: {} } = {};
newObj['__inputs'] = inputs;
newObj['__requires'] = _.sortBy(requires, ['id']);
diff --git a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
index f358281e018..7c610c87d9f 100644
--- a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
+++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { Tooltip } from '@grafana/ui';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { StoreState, FolderInfo } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
import {
diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx
index 3955f45d926..029fb0e8371 100644
--- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx
+++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx
@@ -14,6 +14,7 @@ import { PanelEditor } from '../panel_editor/PanelEditor';
import { PanelModel, DashboardModel } from '../state';
import { PanelPlugin } from 'app/types';
import { PanelResizer } from './PanelResizer';
+import { PanelTypeChangedHook } from '@grafana/ui';
export interface Props {
panel: PanelModel;
@@ -91,7 +92,11 @@ export class DashboardPanel extends PureComponent {
this.props.panel.changeType(pluginId);
} else {
- panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions);
+ let hook: PanelTypeChangedHook | null = null;
+ if (plugin.exports.reactPanel) {
+ hook = plugin.exports.reactPanel.panelTypeChangedHook;
+ }
+ panel.changeType(pluginId, hook);
}
}
diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx
index 82d94669cd6..6941832c273 100644
--- a/public/app/features/dashboard/dashgrid/DataPanel.tsx
+++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx
@@ -11,16 +11,15 @@ import {
DataQueryResponse,
DataQueryError,
LoadingState,
- PanelData,
TableData,
TimeRange,
- TimeSeries,
ScopedVars,
+ toTableData,
} from '@grafana/ui';
interface RenderProps {
loading: LoadingState;
- panelData: PanelData;
+ data: TableData[];
}
export interface Props {
@@ -44,7 +43,7 @@ export interface State {
isFirstLoad: boolean;
loading: LoadingState;
response: DataQueryResponse;
- panelData: PanelData;
+ data?: TableData[];
}
export class DataPanel extends Component {
@@ -64,7 +63,6 @@ export class DataPanel extends Component {
response: {
data: [],
},
- panelData: {},
isFirstLoad: true,
};
}
@@ -149,7 +147,7 @@ export class DataPanel extends Component {
this.setState({
loading: LoadingState.Done,
response: resp,
- panelData: this.getPanelData(resp),
+ data: toTableData(resp.data),
isFirstLoad: false,
});
} catch (err) {
@@ -172,23 +170,9 @@ export class DataPanel extends Component {
}
};
- getPanelData(response: DataQueryResponse) {
- if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
- return {
- tableData: response.data[0] as TableData,
- timeSeries: null,
- };
- }
-
- return {
- timeSeries: response.data as TimeSeries[],
- tableData: null,
- };
- }
-
render() {
const { queries } = this.props;
- const { loading, isFirstLoad, panelData } = this.state;
+ const { loading, isFirstLoad, data } = this.state;
// do not render component until we have first data
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
@@ -206,7 +190,7 @@ export class DataPanel extends Component {
return (
<>
{loading === LoadingState.Loading && this.renderLoadingState()}
- {this.props.children({ loading, panelData })}
+ {this.props.children({ loading, data })}
>
);
}
diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx
index deb14f130ec..9a2dec5acd8 100644
--- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx
+++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx
@@ -11,7 +11,7 @@ import { DataPanel } from './DataPanel';
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
// Utils
-import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
+import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
import { profiler } from 'app/core/profiler';
import config from 'app/core/config';
@@ -19,7 +19,7 @@ import config from 'app/core/config';
// Types
import { DashboardModel, PanelModel } from '../state';
import { PanelPlugin } from 'app/types';
-import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
+import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
import { ScopedVars } from '@grafana/ui';
import templateSrv from 'app/features/templating/template_srv';
@@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent {
}
get getDataForPanel() {
- return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
+ return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null;
}
- renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
+ renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.reactPanel.panel;
@@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent {
{
onDataResponse={this.onDataResponse}
onError={this.onDataError}
>
- {({ loading, panelData }) => {
- return this.renderPanelPlugin(loading, panelData, width, height);
+ {({ loading, data }) => {
+ return this.renderPanelPlugin(loading, data, width, height);
}}
) : (
diff --git a/public/app/features/dashboard/panel_editor/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx
index 0d031cb12ba..377582d7ce5 100644
--- a/public/app/features/dashboard/panel_editor/QueryOptions.tsx
+++ b/public/app/features/dashboard/panel_editor/QueryOptions.tsx
@@ -5,17 +5,12 @@ import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
// Components
-import { Switch } from '@grafana/ui';
-import { Input } from 'app/core/components/Form';
-import { EventsWithValidation } from 'app/core/components/Form/Input';
-import { InputStatus } from 'app/core/components/Form/Input';
+import { DataSourceSelectItem, EventsWithValidation, Input, InputStatus, Switch, ValidationEvents } from '@grafana/ui';
import { DataSourceOption } from './DataSourceOption';
import { FormLabel } from '@grafana/ui';
// Types
-import { PanelModel } from '../state/PanelModel';
-import { DataSourceSelectItem } from '@grafana/ui/src/types';
-import { ValidationEvents } from 'app/types';
+import { PanelModel } from '../state';
const timeRangeValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [
diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts
index 686a8ba6d28..82af0804029 100644
--- a/public/app/features/dashboard/state/PanelModel.test.ts
+++ b/public/app/features/dashboard/state/PanelModel.test.ts
@@ -3,9 +3,10 @@ import { PanelModel } from './PanelModel';
describe('PanelModel', () => {
describe('when creating new panel model', () => {
let model;
+ let modelJson;
beforeEach(() => {
- model = new PanelModel({
+ modelJson = {
type: 'table',
showColumns: true,
targets: [{ refId: 'A' }, { noRefId: true }],
@@ -23,7 +24,8 @@ describe('PanelModel', () => {
},
],
},
- });
+ };
+ model = new PanelModel(modelJson);
});
it('should apply defaults', () => {
@@ -38,6 +40,15 @@ describe('PanelModel', () => {
expect(model.targets[1].refId).toBe('B');
});
+ it("shouldn't break panel with non-array targets", () => {
+ modelJson.targets = {
+ 0: { refId: 'A' },
+ foo: { bar: 'baz' },
+ };
+ model = new PanelModel(modelJson);
+ expect(model.targets[0].refId).toBe('A');
+ });
+
it('getSaveModel should remove defaults', () => {
const saveModel = model.getSaveModel();
expect(saveModel.gridPos).toBe(undefined);
diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts
index 88065fdf208..68f017adb9e 100644
--- a/public/app/features/dashboard/state/PanelModel.ts
+++ b/public/app/features/dashboard/state/PanelModel.ts
@@ -1,9 +1,12 @@
// Libraries
import _ from 'lodash';
-// Types
+// Utils
import { Emitter } from 'app/core/utils/emitter';
-import { DataQuery, TimeSeries, Threshold, ScopedVars } from '@grafana/ui';
+import { getNextRefIdChar } from 'app/core/utils/query';
+
+// Types
+import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
export interface GridPos {
@@ -108,12 +111,12 @@ export class PanelModel {
cachedPluginOptions?: any;
legend?: { show: boolean };
- constructor(model) {
+ constructor(model: any) {
this.events = new Emitter();
// copy properties from persisted model
for (const property in model) {
- this[property] = model[property];
+ (this as any)[property] = model[property];
}
// defaults
@@ -125,10 +128,10 @@ export class PanelModel {
}
ensureQueryIds() {
- if (this.targets) {
+ if (this.targets && _.isArray(this.targets)) {
for (const query of this.targets) {
if (!query.refId) {
- query.refId = this.getNextQueryLetter();
+ query.refId = getNextRefIdChar(this.targets);
}
}
}
@@ -147,7 +150,7 @@ export class PanelModel {
}
}
- getOptions(panelDefaults) {
+ getOptions(panelDefaults: any) {
return _.defaultsDeep(this.options || {}, panelDefaults);
}
@@ -224,7 +227,7 @@ export class PanelModel {
}
return {
...acc,
- [property]: this[property],
+ [property]: (this as any)[property],
};
}, {});
}
@@ -233,11 +236,11 @@ export class PanelModel {
const prevOptions = this.cachedPluginOptions[pluginId] || {};
Object.keys(prevOptions).map(property => {
- this[property] = prevOptions[property];
+ (this as any)[property] = prevOptions[property];
});
}
- changeType(pluginId: string, preserveOptions?: any) {
+ changeType(pluginId: string, hook?: PanelTypeChangedHook) {
const oldOptions: any = this.getOptionsToRemember();
const oldPluginId = this.type;
@@ -249,34 +252,27 @@ export class PanelModel {
continue;
}
- delete this[key];
+ delete (this as any)[key];
}
this.cachedPluginOptions[oldPluginId] = oldOptions;
this.restorePanelOptions(pluginId);
- if (preserveOptions && oldOptions) {
+ // Callback that can validate and migrate any existing settings
+ if (hook) {
this.options = this.options || {};
- Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options));
+ const old = oldOptions ? oldOptions.options : null;
+
+ Object.assign(this.options, hook(this.options, oldPluginId, old));
}
}
addQuery(query?: Partial) {
query = query || { refId: 'A' };
- query.refId = this.getNextQueryLetter();
+ query.refId = getNextRefIdChar(this.targets);
this.targets.push(query as DataQuery);
}
- getNextQueryLetter(): string {
- const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
- return _.find(letters, refId => {
- return _.every(this.targets, other => {
- return other.refId !== refId;
- });
- });
- }
-
changeQuery(query: DataQuery, index: number) {
// ensure refId is maintained
query.refId = this.targets[index].refId;
diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts
index d14432cb2eb..57f4b81a0e0 100644
--- a/public/app/features/dashboard/utils/panel.ts
+++ b/public/app/features/dashboard/utils/panel.ts
@@ -4,8 +4,7 @@ import store from 'app/core/store';
// Models
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
-import { PanelData, TimeRange, TimeSeries } from '@grafana/ui';
-import { TableData } from '@grafana/ui/src';
+import { TimeRange } from '@grafana/ui';
// Utils
import { isString as _isString } from 'lodash';
@@ -170,19 +169,3 @@ export function getResolution(panel: PanelModel): number {
return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24));
}
-
-const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints');
-const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
-export const snapshotDataToPanelData = (panel: PanelModel): PanelData => {
- const snapshotData = panel.snapshotData;
- if (isTimeSeries(snapshotData[0])) {
- return {
- timeSeries: snapshotData,
- } as PanelData;
- } else if (isTableData(snapshotData[0])) {
- return {
- tableData: snapshotData[0],
- } as PanelData;
- }
- throw new Error('snapshotData is invalid:' + snapshotData.toString());
-};
diff --git a/public/app/features/explore/LogLabelStats.tsx b/public/app/features/explore/LogLabelStats.tsx
index d689b6e70b2..466cc050e43 100644
--- a/public/app/features/explore/LogLabelStats.tsx
+++ b/public/app/features/explore/LogLabelStats.tsx
@@ -11,7 +11,9 @@ function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) {
return (
-
{value}
+
+ {value}
+
{count}
{percent}
diff --git a/public/app/features/explore/Table.tsx b/public/app/features/explore/Table.tsx
index 4946a6a505d..bbf338df8f9 100644
--- a/public/app/features/explore/Table.tsx
+++ b/public/app/features/explore/Table.tsx
@@ -40,11 +40,15 @@ export default class Table extends PureComponent
{
const tableModel = data || EMPTY_TABLE;
const columnNames = tableModel.columns.map(({ text }) => text);
const columns = tableModel.columns.map(({ filterable, text }) => ({
- Header: text,
+ Header: () => {text} ,
accessor: text,
className: VALUE_REGEX.test(text) ? 'text-right' : '',
show: text !== 'Time',
- Cell: row => {row.value} ,
+ Cell: row => (
+
+ {row.value}
+
+ ),
}));
const noDataText = data ? 'The queries returned no data for a table.' : '';
diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts
index e0b84320fa7..fda8fe5eef4 100644
--- a/public/app/features/explore/state/actions.ts
+++ b/public/app/features/explore/state/actions.ts
@@ -60,7 +60,6 @@ import {
splitCloseAction,
splitOpenAction,
addQueryRowAction,
- AddQueryRowPayload,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
@@ -87,9 +86,12 @@ const updateExploreUIState = (exploreId, uiStateFragment: Partial {
- const query = generateEmptyQuery(index + 1);
- return addQueryRowAction({ exploreId, index, query });
+export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult {
+ return (dispatch, getState) => {
+ const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
+
+ dispatch(addQueryRowAction({ exploreId, index, query }));
+ };
}
/**
@@ -126,10 +128,10 @@ export function changeQuery(
index: number,
override: boolean
): ThunkResult {
- return dispatch => {
+ return (dispatch, getState) => {
// Null query means reset
if (query === null) {
- query = { ...generateEmptyQuery(index) };
+ query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
}
dispatch(changeQueryAction({ exploreId, query, index, override }));
@@ -287,7 +289,7 @@ export function importQueries(
const nextQueries = importedQueries.map((q, i) => ({
...q,
- ...generateEmptyQuery(i),
+ ...generateEmptyQuery(queries),
}));
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
@@ -629,9 +631,9 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes
* Use this action for clicks on query examples. Triggers a query run.
*/
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult {
- return dispatch => {
+ return (dispatch, getState) => {
// Inject react keys into query objects
- const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
+ const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
dispatch(setQueriesAction({ exploreId, queries }));
dispatch(runQueries(exploreId));
};
diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts
index a8815842c89..32bfe09a96b 100644
--- a/public/app/features/explore/state/reducers.ts
+++ b/public/app/features/explore/state/reducers.ts
@@ -127,7 +127,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta
const { query, index } = action.payload;
// Override path: queries are completely reset
- const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
+ const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) };
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
@@ -267,7 +267,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta
// Modify all queries
nextQueries = queries.map((query, i) => ({
...modifier({ ...query }, modification),
- ...generateEmptyQuery(i),
+ ...generateEmptyQuery(state.queries),
}));
// Discard all ongoing transactions
nextQueryTransactions = [];
@@ -276,7 +276,9 @@ export const itemReducer = reducerFactory({} as ExploreItemSta
nextQueries = queries.map((query, i) => {
// Synchronize all queries with local query cache to ensure consistency
// TODO still needed?
- return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
+ return i === index
+ ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(state.queries) }
+ : query;
});
nextQueryTransactions = queryTransactions
// Consume the hint corresponding to the action
diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx
index a3aae423b75..705bb8f7848 100644
--- a/public/app/features/folders/FolderPermissions.tsx
+++ b/public/app/features/folders/FolderPermissions.tsx
@@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import Page from 'app/core/components/Page/Page';
import { Tooltip } from '@grafana/ui';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { getNavModel } from 'app/core/selectors/navModel';
import { NavModel, StoreState, FolderState } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts
index 9a156652a65..ab9d9aba08a 100644
--- a/public/app/features/plugins/built_in_plugins.ts
+++ b/public/app/features/plugins/built_in_plugins.ts
@@ -25,6 +25,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
import * as tablePanel from 'app/plugins/panel/table/module';
import * as table2Panel from 'app/plugins/panel/table2/module';
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
+import * as singlestatPanel2 from 'app/plugins/panel/singlestat2/module';
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
import * as gaugePanel from 'app/plugins/panel/gauge/module';
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
@@ -57,6 +58,7 @@ const builtInPlugins = {
'app/plugins/panel/table/module': tablePanel,
'app/plugins/panel/table2/module': table2Panel,
'app/plugins/panel/singlestat/module': singlestatPanel,
+ 'app/plugins/panel/singlestat2/module': singlestatPanel2,
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
'app/plugins/panel/gauge/module': gaugePanel,
'app/plugins/panel/bargauge/module': barGaugePanel,
diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx
index 363fc2f2ccc..5f47c4b906d 100644
--- a/public/app/features/teams/TeamGroupSync.tsx
+++ b/public/app/features/teams/TeamGroupSync.tsx
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { Tooltip } from '@grafana/ui';
import { TeamGroup } from '../../types';
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx
index 369771bb340..da5afb58796 100644
--- a/public/app/features/teams/TeamList.test.tsx
+++ b/public/app/features/teams/TeamList.test.tsx
@@ -1,8 +1,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Props, TeamList } from './TeamList';
-import { NavModel, Team } from '../../types';
+import { NavModel, Team, OrgRole } from '../../types';
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
+import { User } from 'app/core/services/context_srv';
const setup = (propOverrides?: object) => {
const props: Props = {
@@ -21,6 +22,11 @@ const setup = (propOverrides?: object) => {
searchQuery: '',
teamsCount: 0,
hasFetched: false,
+ editorsCanAdmin: false,
+ signedInUser: {
+ id: 1,
+ orgRole: OrgRole.Viewer,
+ } as User,
};
Object.assign(props, propOverrides);
@@ -49,6 +55,42 @@ describe('Render', () => {
expect(wrapper).toMatchSnapshot();
});
+
+ describe('when feature toggle editorsCanAdmin is turned on', () => {
+ describe('and signedin user is not viewer', () => {
+ it('should enable the new team button', () => {
+ const { wrapper } = setup({
+ teams: getMultipleMockTeams(1),
+ teamsCount: 1,
+ hasFetched: true,
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: 1,
+ orgRole: OrgRole.Editor,
+ } as User,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+
+ describe('and signedin user is a viewer', () => {
+ it('should disable the new team button', () => {
+ const { wrapper } = setup({
+ teams: getMultipleMockTeams(1),
+ teamsCount: 1,
+ hasFetched: true,
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: 1,
+ orgRole: OrgRole.Viewer,
+ } as User,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+ });
});
describe('Life cycle', () => {
diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx
index 60921a3378b..5d3ef005c9e 100644
--- a/public/app/features/teams/TeamList.tsx
+++ b/public/app/features/teams/TeamList.tsx
@@ -4,11 +4,13 @@ import { hot } from 'react-hot-loader';
import Page from 'app/core/components/Page/Page';
import { DeleteButton } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
-import { NavModel, Team } from 'app/types';
+import { NavModel, Team, OrgRole } from 'app/types';
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
-import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
+import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors';
import { getNavModel } from 'app/core/selectors/navModel';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
+import { config } from 'app/core/config';
+import { contextSrv, User } from 'app/core/services/context_srv';
export interface Props {
navModel: NavModel;
@@ -19,6 +21,8 @@ export interface Props {
loadTeams: typeof loadTeams;
deleteTeam: typeof deleteTeam;
setSearchQuery: typeof setSearchQuery;
+ editorsCanAdmin?: boolean;
+ signedInUser?: User;
}
export class TeamList extends PureComponent {
@@ -39,7 +43,10 @@ export class TeamList extends PureComponent {
};
renderTeam(team: Team) {
+ const { editorsCanAdmin, signedInUser } = this.props;
+ const permission = team.permission;
const teamUrl = `org/teams/edit/${team.id}`;
+ const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser });
return (
@@ -58,7 +65,7 @@ export class TeamList extends PureComponent {
{team.memberCount}
- this.deleteTeam(team)} />
+ this.deleteTeam(team)} disabled={!canDelete} />
);
@@ -84,7 +91,10 @@ export class TeamList extends PureComponent {
}
renderTeamList() {
- const { teams, searchQuery } = this.props;
+ const { teams, searchQuery, editorsCanAdmin, signedInUser } = this.props;
+ const isCanAdminAndViewer = editorsCanAdmin && signedInUser.orgRole === OrgRole.Viewer;
+ const disabledClass = isCanAdminAndViewer ? ' disabled' : '';
+ const newTeamHref = isCanAdminAndViewer ? '#' : 'org/teams/new';
return (
<>
@@ -101,7 +111,7 @@ export class TeamList extends PureComponent {
-
+
New team
@@ -152,6 +162,8 @@ function mapStateToProps(state) {
searchQuery: getSearchQuery(state.teams),
teamsCount: getTeamsCount(state.teams),
hasFetched: state.teams.hasFetched,
+ editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
+ signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
};
}
diff --git a/public/app/features/teams/TeamMemberRow.test.tsx b/public/app/features/teams/TeamMemberRow.test.tsx
new file mode 100644
index 00000000000..0607825bff3
--- /dev/null
+++ b/public/app/features/teams/TeamMemberRow.test.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { TeamMember, TeamPermissionLevel } from '../../types';
+import { getMockTeamMember } from './__mocks__/teamMocks';
+import { TeamMemberRow, Props } from './TeamMemberRow';
+import { SelectOptionItem } from '@grafana/ui';
+
+const setup = (propOverrides?: object) => {
+ const props: Props = {
+ member: getMockTeamMember(),
+ syncEnabled: false,
+ editorsCanAdmin: false,
+ signedInUserIsTeamAdmin: false,
+ updateTeamMember: jest.fn(),
+ removeTeamMember: jest.fn(),
+ };
+
+ Object.assign(props, propOverrides);
+
+ const wrapper = shallow( );
+ const instance = wrapper.instance() as TeamMemberRow;
+
+ return {
+ wrapper,
+ instance,
+ };
+};
+
+describe('Render', () => {
+ it('should render team members when sync enabled', () => {
+ const member = getMockTeamMember();
+ member.labels = ['LDAP'];
+ const { wrapper } = setup({ member, syncEnabled: true });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ describe('when feature toggle editorsCanAdmin is turned on', () => {
+ it('should render permissions select if user is team admin', () => {
+ const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: true });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render span and disable buttons if user is team member', () => {
+ const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: false });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+
+ describe('when feature toggle editorsCanAdmin is turned off', () => {
+ it('should not render permissions', () => {
+ const { wrapper } = setup({ editorsCanAdmin: false, signedInUserIsTeamAdmin: true });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
+
+describe('Functions', () => {
+ describe('on remove member', () => {
+ const member = getMockTeamMember();
+ const { instance } = setup({ member });
+
+ instance.onRemoveMember(member);
+
+ expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
+ });
+
+ describe('on update permision for user in team', () => {
+ const member: TeamMember = {
+ userId: 3,
+ teamId: 2,
+ avatarUrl: '',
+ email: 'user@user.org',
+ labels: [],
+ login: 'member',
+ permission: TeamPermissionLevel.Member,
+ };
+ const { instance } = setup({ member });
+ const permission = TeamPermissionLevel.Admin;
+ const item: SelectOptionItem = { value: permission };
+ const expectedTeamMemeber = { ...member, permission };
+
+ instance.onPermissionChange(item, member);
+
+ expect(instance.props.updateTeamMember).toHaveBeenCalledWith(expectedTeamMemeber);
+ });
+});
diff --git a/public/app/features/teams/TeamMemberRow.tsx b/public/app/features/teams/TeamMemberRow.tsx
new file mode 100644
index 00000000000..0111d1efd8e
--- /dev/null
+++ b/public/app/features/teams/TeamMemberRow.tsx
@@ -0,0 +1,106 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui';
+
+import { TeamMember, teamsPermissionLevels } from 'app/types';
+import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
+import { updateTeamMember, removeTeamMember } from './state/actions';
+import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
+
+export interface Props {
+ member: TeamMember;
+ syncEnabled: boolean;
+ editorsCanAdmin: boolean;
+ signedInUserIsTeamAdmin: boolean;
+ removeTeamMember?: typeof removeTeamMember;
+ updateTeamMember?: typeof updateTeamMember;
+}
+
+export class TeamMemberRow extends PureComponent {
+ constructor(props: Props) {
+ super(props);
+ this.renderLabels = this.renderLabels.bind(this);
+ this.renderPermissions = this.renderPermissions.bind(this);
+ }
+
+ onRemoveMember(member: TeamMember) {
+ this.props.removeTeamMember(member.userId);
+ }
+
+ onPermissionChange = (item: SelectOptionItem, member: TeamMember) => {
+ const permission = item.value;
+ const updatedTeamMember = { ...member, permission };
+
+ this.props.updateTeamMember(updatedTeamMember);
+ };
+
+ renderPermissions(member: TeamMember) {
+ const { editorsCanAdmin, signedInUserIsTeamAdmin } = this.props;
+ const value = teamsPermissionLevels.find(dp => dp.value === member.permission);
+
+ return (
+
+
+
+ {signedInUserIsTeamAdmin && (
+ this.onPermissionChange(item, member)}
+ className="gf-form-select-box__control--menu-right"
+ value={value}
+ />
+ )}
+ {!signedInUserIsTeamAdmin && {value.label} }
+
+
+
+ );
+ }
+
+ renderLabels(labels: string[]) {
+ if (!labels) {
+ return ;
+ }
+
+ return (
+
+ {labels.map(label => (
+ {}} />
+ ))}
+
+ );
+ }
+
+ render() {
+ const { member, syncEnabled, signedInUserIsTeamAdmin } = this.props;
+ return (
+
+
+
+
+ {member.login}
+ {member.email}
+ {this.renderPermissions(member)}
+ {syncEnabled && this.renderLabels(member.labels)}
+
+ this.onRemoveMember(member)} disabled={!signedInUserIsTeamAdmin} />
+
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {};
+}
+
+const mapDispatchToProps = {
+ removeTeamMember,
+ updateTeamMember,
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TeamMemberRow);
diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx
index 9922619adc2..01b9f12aa3b 100644
--- a/public/app/features/teams/TeamMembers.test.tsx
+++ b/public/app/features/teams/TeamMembers.test.tsx
@@ -1,18 +1,25 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TeamMembers, Props, State } from './TeamMembers';
-import { TeamMember } from '../../types';
-import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
+import { TeamMember, OrgRole } from '../../types';
+import { getMockTeamMembers } from './__mocks__/teamMocks';
+import { User } from 'app/core/services/context_srv';
+
+const signedInUserId = 1;
const setup = (propOverrides?: object) => {
const props: Props = {
members: [] as TeamMember[],
searchMemberQuery: '',
setSearchMemberQuery: jest.fn(),
- loadTeamMembers: jest.fn(),
addTeamMember: jest.fn(),
- removeTeamMember: jest.fn(),
syncEnabled: false,
+ editorsCanAdmin: false,
+ signedInUser: {
+ id: signedInUserId,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Viewer,
+ } as User,
};
Object.assign(props, propOverrides);
@@ -28,24 +35,13 @@ const setup = (propOverrides?: object) => {
describe('Render', () => {
it('should render component', () => {
- const { wrapper } = setup();
+ const { wrapper } = setup({});
expect(wrapper).toMatchSnapshot();
});
it('should render team members', () => {
- const { wrapper } = setup({
- members: getMockTeamMembers(5),
- });
-
- expect(wrapper).toMatchSnapshot();
- });
-
- it('should render team members when sync enabled', () => {
- const { wrapper } = setup({
- members: getMockTeamMembers(5),
- syncEnabled: true,
- });
+ const { wrapper } = setup({ members: getMockTeamMembers(5, 5) });
expect(wrapper).toMatchSnapshot();
});
@@ -54,7 +50,7 @@ describe('Render', () => {
describe('Functions', () => {
describe('on search member query change', () => {
it('it should call setSearchMemberQuery', () => {
- const { instance } = setup();
+ const { instance } = setup({});
instance.onSearchQueryChange('member');
@@ -62,17 +58,8 @@ describe('Functions', () => {
});
});
- describe('on remove member', () => {
- const { instance } = setup();
- const mockTeamMember = getMockTeamMember();
-
- instance.onRemoveMember(mockTeamMember);
-
- expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
- });
-
describe('on add user to team', () => {
- const { wrapper, instance } = setup();
+ const { wrapper, instance } = setup({});
const state = wrapper.state() as State;
state.newTeamMember = {
diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx
index e5c3aaafef0..ab06f465097 100644
--- a/public/app/features/teams/TeamMembers.tsx
+++ b/public/app/features/teams/TeamMembers.tsx
@@ -1,22 +1,25 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker';
-import { DeleteButton } from '@grafana/ui';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User } from 'app/types';
-import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
-import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
+import { addTeamMember, setSearchMemberQuery } from './state/actions';
+import { getSearchMemberQuery, isSignedInUserTeamAdmin } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
+import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
+import { config } from 'app/core/config';
+import { contextSrv, User as SignedInUser } from 'app/core/services/context_srv';
+import TeamMemberRow from './TeamMemberRow';
export interface Props {
members: TeamMember[];
searchMemberQuery: string;
- loadTeamMembers: typeof loadTeamMembers;
addTeamMember: typeof addTeamMember;
- removeTeamMember: typeof removeTeamMember;
setSearchMemberQuery: typeof setSearchMemberQuery;
syncEnabled: boolean;
+ editorsCanAdmin?: boolean;
+ signedInUser?: SignedInUser;
}
export interface State {
@@ -30,18 +33,10 @@ export class TeamMembers extends PureComponent {
this.state = { isAdding: false, newTeamMember: null };
}
- componentDidMount() {
- this.props.loadTeamMembers();
- }
-
onSearchQueryChange = (value: string) => {
this.props.setSearchMemberQuery(value);
};
- onRemoveMember(member: TeamMember) {
- this.props.removeTeamMember(member.userId);
- }
-
onToggleAdding = () => {
this.setState({ isAdding: !this.state.isAdding });
};
@@ -69,25 +64,11 @@ export class TeamMembers extends PureComponent {
);
}
- renderMember(member: TeamMember, syncEnabled: boolean) {
- return (
-
-
-
-
- {member.login}
- {member.email}
- {syncEnabled && this.renderLabels(member.labels)}
-
- this.onRemoveMember(member)} />
-
-
- );
- }
-
render() {
const { isAdding } = this.state;
- const { searchMemberQuery, members, syncEnabled } = this.props;
+ const { searchMemberQuery, members, syncEnabled, editorsCanAdmin, signedInUser } = this.props;
+ const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
+
return (
@@ -103,7 +84,11 @@ export class TeamMembers extends PureComponent
{
-
+
Add member
@@ -132,11 +117,25 @@ export class TeamMembers extends PureComponent
{
Name
Email
+
+ Permission
+
{syncEnabled && }
- {members && members.map(member => this.renderMember(member, syncEnabled))}
+
+ {members &&
+ members.map(member => (
+
+ ))}
+
@@ -146,15 +145,14 @@ export class TeamMembers extends PureComponent {
function mapStateToProps(state) {
return {
- members: getTeamMembers(state.team),
searchMemberQuery: getSearchMemberQuery(state.team),
+ editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
+ signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
};
}
const mapDispatchToProps = {
- loadTeamMembers,
addTeamMember,
- removeTeamMember,
setSearchMemberQuery,
};
diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx
index 5d751f46989..c49be9f550b 100644
--- a/public/app/features/teams/TeamPages.test.tsx
+++ b/public/app/features/teams/TeamPages.test.tsx
@@ -1,8 +1,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TeamPages, Props } from './TeamPages';
-import { NavModel, Team } from '../../types';
+import { NavModel, Team, TeamMember, OrgRole } from '../../types';
import { getMockTeam } from './__mocks__/teamMocks';
+import { User } from 'app/core/services/context_srv';
jest.mock('app/core/config', () => ({
buildInfo: { isEnterprise: true },
@@ -13,8 +14,16 @@ const setup = (propOverrides?: object) => {
navModel: {} as NavModel,
teamId: 1,
loadTeam: jest.fn(),
+ loadTeamMembers: jest.fn(),
pageName: 'members',
team: {} as Team,
+ members: [] as TeamMember[],
+ editorsCanAdmin: false,
+ signedInUser: {
+ id: 1,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Viewer,
+ } as User,
};
Object.assign(props, propOverrides);
@@ -65,4 +74,46 @@ describe('Render', () => {
expect(wrapper).toMatchSnapshot();
});
+
+ describe('when feature toggle editorsCanAdmin is turned on', () => {
+ it('should render settings page if user is team admin', () => {
+ const { wrapper } = setup({
+ team: getMockTeam(),
+ pageName: 'settings',
+ preferences: {
+ homeDashboardId: 1,
+ theme: 'Default',
+ timezone: 'Default',
+ },
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: 1,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Admin,
+ } as User,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should not render settings page if user is team member', () => {
+ const { wrapper } = setup({
+ team: getMockTeam(),
+ pageName: 'settings',
+ preferences: {
+ homeDashboardId: 1,
+ theme: 'Default',
+ timezone: 'Default',
+ },
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: 1,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Viewer,
+ } as User,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
});
diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx
index 15adc8b3856..e6b70e7b33b 100644
--- a/public/app/features/teams/TeamPages.tsx
+++ b/public/app/features/teams/TeamPages.tsx
@@ -7,19 +7,24 @@ import Page from 'app/core/components/Page/Page';
import TeamMembers from './TeamMembers';
import TeamSettings from './TeamSettings';
import TeamGroupSync from './TeamGroupSync';
-import { NavModel, Team } from 'app/types';
-import { loadTeam } from './state/actions';
-import { getTeam } from './state/selectors';
+import { NavModel, Team, TeamMember } from 'app/types';
+import { loadTeam, loadTeamMembers } from './state/actions';
+import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
import { getTeamLoadingNav } from './state/navModel';
import { getNavModel } from 'app/core/selectors/navModel';
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
+import { contextSrv, User } from 'app/core/services/context_srv';
export interface Props {
team: Team;
loadTeam: typeof loadTeam;
+ loadTeamMembers: typeof loadTeamMembers;
teamId: number;
pageName: string;
navModel: NavModel;
+ members?: TeamMember[];
+ editorsCanAdmin?: boolean;
+ signedInUser?: User;
}
interface State {
@@ -51,6 +56,7 @@ export class TeamPages extends PureComponent {
const { loadTeam, teamId } = this.props;
this.setState({ isLoading: true });
const team = await loadTeam(teamId);
+ await this.props.loadTeamMembers();
this.setState({ isLoading: false });
return team;
}
@@ -61,30 +67,56 @@ export class TeamPages extends PureComponent {
return _.includes(pages, currentPage) ? currentPage : pages[0];
}
- renderPage() {
+ textsAreEqual = (text1: string, text2: string) => {
+ if (!text1 && !text2) {
+ return true;
+ }
+
+ if (!text1 || !text2) {
+ return false;
+ }
+
+ return text1.toLocaleLowerCase() === text2.toLocaleLowerCase();
+ };
+
+ hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => {
+ if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) {
+ navModel.main.children
+ .filter(navItem => !this.textsAreEqual(navItem.text, PageTypes.Members))
+ .map(navItem => {
+ navItem.hideFromTabs = true;
+ });
+ }
+
+ return navModel;
+ };
+
+ renderPage(isSignedInUserTeamAdmin: boolean) {
const { isSyncEnabled } = this.state;
+ const { members } = this.props;
const currentPage = this.getCurrentPage();
switch (currentPage) {
case PageTypes.Members:
- return ;
+ return ;
case PageTypes.Settings:
- return ;
+ return isSignedInUserTeamAdmin && ;
case PageTypes.GroupSync:
- return isSyncEnabled && ;
+ return isSignedInUserTeamAdmin && isSyncEnabled && ;
}
return null;
}
render() {
- const { team, navModel } = this.props;
+ const { team, navModel, members, editorsCanAdmin, signedInUser } = this.props;
+ const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
return (
-
+
- {team && Object.keys(team).length !== 0 && this.renderPage()}
+ {team && Object.keys(team).length !== 0 && this.renderPage(isTeamAdmin)}
);
@@ -95,17 +127,24 @@ function mapStateToProps(state) {
const teamId = getRouteParamsId(state.location);
const pageName = getRouteParamsPage(state.location) || 'members';
const teamLoadingNav = getTeamLoadingNav(pageName);
+ const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav);
+ const team = getTeam(state.team, teamId);
+ const members = getTeamMembers(state.team);
return {
- navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav),
+ navModel,
teamId: teamId,
pageName: pageName,
- team: getTeam(state.team, teamId),
+ team,
+ members,
+ editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
+ signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
};
}
const mapDispatchToProps = {
loadTeam,
+ loadTeamMembers,
};
export default hot(module)(
diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts
index 339f227c081..abaa5ef555f 100644
--- a/public/app/features/teams/__mocks__/teamMocks.ts
+++ b/public/app/features/teams/__mocks__/teamMocks.ts
@@ -1,4 +1,4 @@
-import { Team, TeamGroup, TeamMember } from 'app/types';
+import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types';
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
const teams: Team[] = [];
@@ -9,6 +9,7 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
avatarUrl: 'some/url/',
email: `test-${i}@test.com`,
memberCount: i,
+ permission: TeamPermissionLevel.Member,
});
}
@@ -22,10 +23,11 @@ export const getMockTeam = (): Team => {
avatarUrl: 'some/url/',
email: 'test@test.com',
memberCount: 1,
+ permission: TeamPermissionLevel.Member,
};
};
-export const getMockTeamMembers = (amount: number): TeamMember[] => {
+export const getMockTeamMembers = (amount: number, teamAdminId: number): TeamMember[] => {
const teamMembers: TeamMember[] = [];
for (let i = 1; i <= amount; i++) {
@@ -36,6 +38,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => {
email: 'test@test.com',
login: `testUser-${i}`,
labels: ['label 1', 'label 2'],
+ permission: i === teamAdminId ? TeamPermissionLevel.Admin : TeamPermissionLevel.Member,
});
}
@@ -50,6 +53,7 @@ export const getMockTeamMember = (): TeamMember => {
email: 'test@test.com',
login: 'testUser',
labels: [],
+ permission: TeamPermissionLevel.Member,
};
};
diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
index 76d13e6a3d6..1017f0d9460 100644
--- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
+++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
@@ -133,6 +133,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
@@ -183,6 +184,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
@@ -233,6 +235,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
@@ -283,6 +286,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
@@ -333,6 +337,259 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
+
+
+
+
+
+
+
+`;
+
+exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is a viewer should disable the new team button 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is not viewer should enable the new team button 1`] = `
+
+
+
+
-
-`;
-
-exports[`Render should render team members when sync enabled 1`] = `
-
-
-
-
-
-
-
- Add member
-
-
-
-
-
-
-
-
- Add team member
-
-
-
-
-
-
-
diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
index 70f37cea4c5..4322d6991b4 100644
--- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
+++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
@@ -30,6 +30,7 @@ exports[`Render should render member page if team not empty 1`] = `
isLoading={true}
>
@@ -47,3 +48,25 @@ exports[`Render should render settings and preferences page 1`] = `
`;
+
+exports[`Render when feature toggle editorsCanAdmin is turned on should not render settings page if user is team member 1`] = `
+
+
+
+`;
+
+exports[`Render when feature toggle editorsCanAdmin is turned on should render settings page if user is team admin 1`] = `
+
+
+
+
+
+`;
diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts
index d948dc1c5a3..e2582839233 100644
--- a/public/app/features/teams/state/actions.ts
+++ b/public/app/features/teams/state/actions.ts
@@ -160,3 +160,12 @@ export function deleteTeam(id: number): ThunkResult
{
dispatch(loadTeams());
};
}
+
+export function updateTeamMember(member: TeamMember): ThunkResult {
+ return async dispatch => {
+ await getBackendSrv().put(`/api/teams/${member.teamId}/members/${member.userId}`, {
+ permission: member.permission,
+ });
+ dispatch(loadTeamMembers());
+ };
+}
diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts
index 2fd5a68e680..aeb6b85f91e 100644
--- a/public/app/features/teams/state/navModel.ts
+++ b/public/app/features/teams/state/navModel.ts
@@ -1,4 +1,4 @@
-import { Team, NavModelItem, NavModel } from 'app/types';
+import { Team, NavModelItem, NavModel, TeamPermissionLevel } from 'app/types';
import config from 'app/core/config';
export function buildNavModel(team: Team): NavModelItem {
@@ -47,6 +47,7 @@ export function getTeamLoadingNav(pageName: string): NavModel {
name: 'Loading',
email: 'loading',
memberCount: 0,
+ permission: TeamPermissionLevel.Member,
});
let node: NavModelItem;
diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts
index e88fbdfd4b1..5d9981e0403 100644
--- a/public/app/features/teams/state/selectors.test.ts
+++ b/public/app/features/teams/state/selectors.test.ts
@@ -1,6 +1,7 @@
-import { getTeam, getTeamMembers, getTeams } from './selectors';
+import { getTeam, getTeamMembers, getTeams, isSignedInUserTeamAdmin, Config } from './selectors';
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
-import { Team, TeamGroup, TeamsState, TeamState } from '../../../types';
+import { Team, TeamGroup, TeamsState, TeamState, OrgRole } from '../../../types';
+import { User } from 'app/core/services/context_srv';
describe('Teams selectors', () => {
describe('Get teams', () => {
@@ -40,7 +41,7 @@ describe('Team selectors', () => {
});
describe('Get members', () => {
- const mockTeamMembers = getMockTeamMembers(5);
+ const mockTeamMembers = getMockTeamMembers(5, 5);
it('should return team members', () => {
const mockState: TeamState = {
@@ -55,3 +56,94 @@ describe('Team selectors', () => {
});
});
});
+
+const signedInUserId = 1;
+
+const setup = (configOverrides?: Partial) => {
+ const defaultConfig: Config = {
+ editorsCanAdmin: false,
+ members: getMockTeamMembers(5, 5),
+ signedInUser: {
+ id: signedInUserId,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Viewer,
+ } as User,
+ };
+
+ return { ...defaultConfig, ...configOverrides };
+};
+
+describe('isSignedInUserTeamAdmin', () => {
+ describe('when feature toggle editorsCanAdmin is turned off', () => {
+ it('should return true', () => {
+ const config = setup();
+
+ const result = isSignedInUserTeamAdmin(config);
+
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('when feature toggle editorsCanAdmin is turned on', () => {
+ it('should return true if signed in user is grafanaAdmin', () => {
+ const config = setup({
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: signedInUserId,
+ isGrafanaAdmin: true,
+ orgRole: OrgRole.Viewer,
+ } as User,
+ });
+
+ const result = isSignedInUserTeamAdmin(config);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true if signed in user is org admin', () => {
+ const config = setup({
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: signedInUserId,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Admin,
+ } as User,
+ });
+
+ const result = isSignedInUserTeamAdmin(config);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true if signed in user is team admin', () => {
+ const config = setup({
+ members: getMockTeamMembers(5, signedInUserId),
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: signedInUserId,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Viewer,
+ } as User,
+ });
+
+ const result = isSignedInUserTeamAdmin(config);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => {
+ const config = setup({
+ editorsCanAdmin: true,
+ signedInUser: {
+ id: signedInUserId,
+ isGrafanaAdmin: false,
+ orgRole: OrgRole.Viewer,
+ } as User,
+ });
+
+ const result = isSignedInUserTeamAdmin(config);
+
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts
index 9201993bf0d..e770abfc093 100644
--- a/public/app/features/teams/state/selectors.ts
+++ b/public/app/features/teams/state/selectors.ts
@@ -1,4 +1,5 @@
-import { Team, TeamsState, TeamState } from 'app/types';
+import { Team, TeamsState, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types';
+import { User } from 'app/core/services/context_srv';
export const getSearchQuery = (state: TeamsState) => state.searchQuery;
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
@@ -28,3 +29,32 @@ export const getTeamMembers = (state: TeamState) => {
return regex.test(member.login) || regex.test(member.email);
});
};
+
+export interface Config {
+ members: TeamMember[];
+ editorsCanAdmin: boolean;
+ signedInUser: User;
+}
+
+export const isSignedInUserTeamAdmin = (config: Config): boolean => {
+ const { members, signedInUser, editorsCanAdmin } = config;
+ const userInMembers = members.find(m => m.userId === signedInUser.id);
+ const permission = userInMembers ? userInMembers.permission : TeamPermissionLevel.Member;
+
+ return isPermissionTeamAdmin({ permission, signedInUser, editorsCanAdmin });
+};
+
+export interface PermissionConfig {
+ permission: TeamPermissionLevel;
+ editorsCanAdmin: boolean;
+ signedInUser: User;
+}
+
+export const isPermissionTeamAdmin = (config: PermissionConfig): boolean => {
+ const { permission, signedInUser, editorsCanAdmin } = config;
+ const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin;
+ const userIsTeamAdmin = permission === TeamPermissionLevel.Admin;
+ const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin;
+
+ return isSignedInUserTeamAdmin || !editorsCanAdmin;
+};
diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
index 9f60c3440eb..2358b1ddab4 100644
--- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
+++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
@@ -2,55 +2,47 @@
import React, { PureComponent } from 'react';
// Services & Utils
-import { processSingleStatPanelData } from '@grafana/ui';
+import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui';
import { config } from 'app/core/config';
-// Components
-import { BarGauge, VizRepeater } from '@grafana/ui';
-
// Types
import { BarGaugeOptions } from './types';
-import { PanelProps, SingleStatValueInfo } from '@grafana/ui/src/types';
+import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
+import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
-interface Props extends PanelProps {}
-
-export class BarGaugePanel extends PureComponent {
- renderBarGauge(value: SingleStatValueInfo, width, height) {
- const { replaceVariables, options } = this.props;
- const { valueOptions } = options;
-
- const prefix = replaceVariables(valueOptions.prefix);
- const suffix = replaceVariables(valueOptions.suffix);
+export class BarGaugePanel extends PureComponent> {
+ renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+ const { options } = this.props;
return (
+ );
+ };
+
+ getProcessedValues = (): DisplayValue[] => {
+ return getSingleStatValues(this.props);
+ };
+
+ render() {
+ const { height, width, options, data, renderCounter } = this.props;
+ return (
+
);
}
-
- render() {
- const { panelData, options, width, height } = this.props;
-
- const values = processSingleStatPanelData({
- panelData: panelData,
- stat: options.valueOptions.stat,
- });
-
- return (
-
- {({ vizHeight, vizWidth, value }) => this.renderBarGauge(value, vizWidth, vizHeight)}
-
- );
- }
}
diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
index 4232155228b..3404c8d8805 100644
--- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
+++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
@@ -2,13 +2,13 @@
import React, { PureComponent } from 'react';
// Components
-import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
// Types
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
-import { BarGaugeOptions, orientationOptions } from './types';
-import { SingleStatValueOptions } from '../gauge/types';
+import { BarGaugeOptions, orientationOptions, displayModes } from './types';
+import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
+import { SingleStatValueOptions } from '../singlestat2/types';
export class BarGaugePanelEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
@@ -32,6 +32,7 @@ export class BarGaugePanelEditor extends PureComponent this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
onOrientationChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
+ onDisplayModeChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
render() {
const { options } = this.props;
@@ -53,6 +54,16 @@ export class BarGaugePanelEditor extends PureComponent item.value === options.orientation)}
/>
+
+ Display Mode
+ item.value === options.displayMode)}
+ />
+
diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx
index d5dcbabc34f..3c46adeb4f9 100644
--- a/public/app/plugins/panel/bargauge/module.tsx
+++ b/public/app/plugins/panel/bargauge/module.tsx
@@ -3,20 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
import { BarGaugePanel } from './BarGaugePanel';
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
import { BarGaugeOptions, defaults } from './types';
+import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin(BarGaugePanel);
reactPanel.setEditor(BarGaugePanelEditor);
reactPanel.setDefaults(defaults);
-reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
- const options: Partial = {};
-
- if (prevOptions.valueOptions) {
- options.valueOptions = prevOptions.valueOptions;
- options.thresholds = prevOptions.thresholds;
- options.maxValue = prevOptions.maxValue;
- options.minValue = prevOptions.minValue;
- }
-
- return options;
-});
+reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts
index 962d18d0e3d..4a22a64bdd4 100644
--- a/public/app/plugins/panel/bargauge/types.ts
+++ b/public/app/plugins/panel/bargauge/types.ts
@@ -1,15 +1,18 @@
-import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui';
-import { SingleStatValueOptions } from '../gauge/types';
+import { VizOrientation, SelectOptionItem } from '@grafana/ui';
+import { SingleStatBaseOptions } from '../singlestat2/types';
-export interface BarGaugeOptions {
+export interface BarGaugeOptions extends SingleStatBaseOptions {
minValue: number;
maxValue: number;
- orientation: VizOrientation;
- valueOptions: SingleStatValueOptions;
- valueMappings: ValueMapping[];
- thresholds: Threshold[];
+ displayMode: 'basic' | 'lcd' | 'gradient';
}
+export const displayModes: SelectOptionItem[] = [
+ { value: 'gradient', label: 'Gradient' },
+ { value: 'lcd', label: 'Retro LCD' },
+ { value: 'basic', label: 'Basic' },
+];
+
export const orientationOptions: SelectOptionItem[] = [
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
{ value: VizOrientation.Vertical, label: 'Vertical' },
@@ -18,6 +21,7 @@ export const orientationOptions: SelectOptionItem[] = [
export const defaults: BarGaugeOptions = {
minValue: 0,
maxValue: 100,
+ displayMode: 'lcd',
orientation: VizOrientation.Horizontal,
valueOptions: {
unit: 'none',
diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx
index f309b38e67d..a0fdf9ed405 100644
--- a/public/app/plugins/panel/gauge/GaugePanel.tsx
+++ b/public/app/plugins/panel/gauge/GaugePanel.tsx
@@ -2,37 +2,27 @@
import React, { PureComponent } from 'react';
// Services & Utils
-import { processSingleStatPanelData } from '@grafana/ui';
import { config } from 'app/core/config';
// Components
-import { Gauge, VizRepeater } from '@grafana/ui';
+import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
-import { PanelProps, VizOrientation, SingleStatValueInfo } from '@grafana/ui/src/types';
+import { DisplayValue, PanelProps } from '@grafana/ui';
+import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
+import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
-interface Props extends PanelProps {}
-
-export class GaugePanel extends PureComponent {
- renderGauge(value: SingleStatValueInfo, width, height) {
- const { replaceVariables, options } = this.props;
- const { valueOptions } = options;
-
- const prefix = replaceVariables(valueOptions.prefix);
- const suffix = replaceVariables(valueOptions.suffix);
+export class GaugePanel extends PureComponent> {
+ renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+ const { options } = this.props;
return (
{
theme={config.theme}
/>
);
- }
+ };
+
+ getProcessedValues = (): DisplayValue[] => {
+ return getSingleStatValues(this.props);
+ };
render() {
- const { panelData, options, height, width } = this.props;
-
- const values = processSingleStatPanelData({
- panelData: panelData,
- stat: options.valueOptions.stat,
- });
-
+ const { height, width, options, data, renderCounter } = this.props;
return (
-
- {({ vizHeight, vizWidth, value }) => this.renderGauge(value, vizWidth, vizHeight)}
-
+
);
}
}
diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
index f33ded6a28d..e3b21302331 100644
--- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
+++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
@@ -9,9 +9,10 @@ import {
ValueMapping,
} from '@grafana/ui';
-import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { GaugeOptionsBox } from './GaugeOptionsBox';
-import { GaugeOptions, SingleStatValueOptions } from './types';
+import { GaugeOptions } from './types';
+import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
+import { SingleStatValueOptions } from '../singlestat2/types';
export class GaugePanelEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx
index 95a6e29ae4d..340af06a080 100644
--- a/public/app/plugins/panel/gauge/module.tsx
+++ b/public/app/plugins/panel/gauge/module.tsx
@@ -3,20 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types';
+import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin(GaugePanel);
reactPanel.setEditor(GaugePanelEditor);
reactPanel.setDefaults(defaults);
-reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
- const options: Partial = {};
-
- if (prevOptions.valueOptions) {
- options.valueOptions = prevOptions.valueOptions;
- options.thresholds = prevOptions.thresholds;
- options.maxValue = prevOptions.maxValue;
- options.minValue = prevOptions.minValue;
- }
-
- return options;
-});
+reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts
index 6922538c210..bab29d5d2ad 100644
--- a/public/app/plugins/panel/gauge/types.ts
+++ b/public/app/plugins/panel/gauge/types.ts
@@ -1,21 +1,11 @@
-import { Threshold, ValueMapping } from '@grafana/ui';
+import { SingleStatBaseOptions } from '../singlestat2/types';
+import { VizOrientation } from '@grafana/ui';
-export interface GaugeOptions {
- valueMappings: ValueMapping[];
+export interface GaugeOptions extends SingleStatBaseOptions {
maxValue: number;
minValue: number;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
- thresholds: Threshold[];
- valueOptions: SingleStatValueOptions;
-}
-
-export interface SingleStatValueOptions {
- unit: string;
- suffix: string;
- stat: string;
- prefix: string;
- decimals?: number | null;
}
export const defaults: GaugeOptions = {
@@ -32,4 +22,5 @@ export const defaults: GaugeOptions = {
},
valueMappings: [],
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+ orientation: VizOrientation.Auto,
};
diff --git a/public/app/plugins/panel/graph/threshold_manager.ts b/public/app/plugins/panel/graph/threshold_manager.ts
index f0bc7d62499..dcebc45c1e7 100644
--- a/public/app/plugins/panel/graph/threshold_manager.ts
+++ b/public/app/plugins/panel/graph/threshold_manager.ts
@@ -216,12 +216,6 @@ export class ThresholdManager {
break;
}
case 'custom': {
- if (!threshold.fillColor) {
- threshold.fillColor = 'rgba(255, 255, 255, 1)';
- }
- if (!threshold.lineColor) {
- threshold.lineColor = 'rgba(255, 255, 255, 0)';
- }
fillColor = threshold.fillColor;
lineColor = threshold.lineColor;
break;
diff --git a/public/app/plugins/panel/graph/thresholds_form.html b/public/app/plugins/panel/graph/thresholds_form.html
index 99e2f530c8d..075b4824d28 100644
--- a/public/app/plugins/panel/graph/thresholds_form.html
+++ b/public/app/plugins/panel/graph/thresholds_form.html
@@ -23,7 +23,7 @@
Color
+ ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.onThresholdTypeChange($index)" ng-disabled="ctrl.disabled">
@@ -73,4 +73,4 @@
-
\ No newline at end of file
+
diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts
index 4f480873d5b..08acd887559 100644
--- a/public/app/plugins/panel/graph/thresholds_form.ts
+++ b/public/app/plugins/panel/graph/thresholds_form.ts
@@ -1,5 +1,6 @@
import coreModule from 'app/core/core_module';
-
+import config from 'app/core/config';
+import tinycolor from 'tinycolor2';
export class ThresholdFormCtrl {
panelCtrl: any;
panel: any;
@@ -56,6 +57,19 @@ export class ThresholdFormCtrl {
this.render();
};
}
+
+ onThresholdTypeChange(index) {
+ // Because of the ng-model binding, threshold's color mode is already set here
+ if (this.panel.thresholds[index].colorMode === 'custom') {
+ this.panel.thresholds[index].fillColor = tinycolor(config.theme.colors.blueBase)
+ .setAlpha(0.2)
+ .toRgbString();
+ this.panel.thresholds[index].lineColor = tinycolor(config.theme.colors.blueShade)
+ .setAlpha(0.6)
+ .toRgbString();
+ }
+ this.panelCtrl.render();
+ }
}
coreModule.directive('graphThresholdForm', () => {
diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx
index f1fc2b43d51..f04e73e56fb 100644
--- a/public/app/plugins/panel/graph2/GraphPanel.tsx
+++ b/public/app/plugins/panel/graph2/GraphPanel.tsx
@@ -16,13 +16,13 @@ interface Props extends PanelProps {}
export class GraphPanel extends PureComponent {
render() {
- const { panelData, timeRange, width, height } = this.props;
+ const { data, timeRange, width, height } = this.props;
const { showLines, showBars, showPoints } = this.props.options;
let vmSeries: TimeSeriesVMs;
- if (panelData.timeSeries) {
+ if (data) {
vmSeries = processTimeSeries({
- timeSeries: panelData.timeSeries,
+ data,
nullValueMode: NullValueMode.Ignore,
});
}
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts
index bcf09297cf7..5b75de44949 100644
--- a/public/app/plugins/panel/singlestat/module.ts
+++ b/public/app/plugins/panel/singlestat/module.ts
@@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
-import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
+import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
scopedVars: _.extend({}, this.panel.scopedVars),
};
- if (dataList.length > 0 && dataList[0].type === 'table') {
+ if (dataList.length > 0 && isTableData(dataList[0])) {
this.dataType = 'table';
const tableData = dataList.map(this.tableHandler.bind(this));
this.setTableValues(tableData, data);
diff --git a/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx b/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx
new file mode 100644
index 00000000000..b8abf3aabb0
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx
@@ -0,0 +1,48 @@
+import React, { PureComponent } from 'react';
+import { VizOrientation } from '@grafana/ui';
+import { VizRepeater } from '@grafana/ui';
+
+export interface Props {
+ width: number;
+ height: number;
+ orientation: VizOrientation;
+ source: any; // If this changes, the values will be processed
+ renderCounter: number; // change to force processing
+
+ getProcessedValues: () => T[];
+ renderValue: (value: T, width: number, height: number) => JSX.Element;
+}
+
+interface State {
+ values: T[];
+}
+
+/**
+ * This is essentially a cache of processed values. This checks for changes
+ * to the source and then saves the processed values in the State
+ */
+export class ProcessedValuesRepeater extends PureComponent, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ values: props.getProcessedValues(),
+ };
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ const { renderCounter, source } = this.props;
+ if (renderCounter !== prevProps.renderCounter || source !== prevProps.source) {
+ this.setState({ values: this.props.getProcessedValues() });
+ }
+ }
+
+ render() {
+ const { orientation, height, width, renderValue } = this.props;
+ const { values } = this.state;
+ return (
+
+ {({ vizHeight, vizWidth, value }) => renderValue(value, vizWidth, vizHeight)}
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/README.md b/public/app/plugins/panel/singlestat2/README.md
new file mode 100644
index 00000000000..42d72825c27
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/README.md
@@ -0,0 +1,9 @@
+# Singlestat Panel - Native Plugin
+
+The Singlestat Panel is **included** with Grafana.
+
+The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series.
+
+Read more about it here:
+
+[http://docs.grafana.org/reference/singlestat/](http://docs.grafana.org/reference/singlestat/)
\ No newline at end of file
diff --git a/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
new file mode 100644
index 00000000000..61b8588adce
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
@@ -0,0 +1,48 @@
+// Libraries
+import React, { PureComponent } from 'react';
+import {
+ PanelEditorProps,
+ ThresholdsEditor,
+ Threshold,
+ PanelOptionsGrid,
+ ValueMappingsEditor,
+ ValueMapping,
+} from '@grafana/ui';
+
+import { SingleStatOptions, SingleStatValueOptions } from './types';
+import { SingleStatValueEditor } from './SingleStatValueEditor';
+
+export class SingleStatEditor extends PureComponent> {
+ onThresholdsChanged = (thresholds: Threshold[]) =>
+ this.props.onOptionsChange({
+ ...this.props.options,
+ thresholds,
+ });
+
+ onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
+ this.props.onOptionsChange({
+ ...this.props.options,
+ valueMappings,
+ });
+
+ onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+ this.props.onOptionsChange({
+ ...this.props.options,
+ valueOptions,
+ });
+
+ render() {
+ const { options } = this.props;
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
new file mode 100644
index 00000000000..1193a031c94
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
@@ -0,0 +1,71 @@
+// Libraries
+import React, { PureComponent, CSSProperties } from 'react';
+
+// Types
+import { SingleStatOptions, SingleStatBaseOptions } from './types';
+
+import { DisplayValue, PanelProps, processTimeSeries, NullValueMode } from '@grafana/ui';
+import { config } from 'app/core/config';
+import { getDisplayProcessor } from '@grafana/ui';
+import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
+
+export const getSingleStatValues = (props: PanelProps): DisplayValue[] => {
+ const { data, replaceVariables, options } = props;
+ const { valueOptions, valueMappings } = options;
+ const { unit, decimals, stat } = valueOptions;
+
+ const processor = getDisplayProcessor({
+ unit,
+ decimals,
+ mappings: valueMappings,
+ thresholds: options.thresholds,
+ prefix: replaceVariables(valueOptions.prefix),
+ suffix: replaceVariables(valueOptions.suffix),
+ theme: config.theme,
+ });
+
+ return processTimeSeries({
+ data,
+ nullValueMode: NullValueMode.Null,
+ }).map((series, index) => {
+ const value = stat !== 'name' ? series.stats[stat] : series.label;
+ return processor(value);
+ });
+};
+
+export class SingleStatPanel extends PureComponent> {
+ renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+ const style: CSSProperties = {};
+ style.margin = '0 auto';
+ style.fontSize = '250%';
+ style.textAlign = 'center';
+ if (value.color) {
+ style.color = value.color;
+ }
+
+ return (
+
+ );
+ };
+
+ getProcessedValues = (): DisplayValue[] => {
+ return getSingleStatValues(this.props);
+ };
+
+ render() {
+ const { height, width, options, data, renderCounter } = this.props;
+ return (
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
similarity index 100%
rename from public/app/plugins/panel/gauge/SingleStatValueEditor.tsx
rename to public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
diff --git a/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg b/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg
new file mode 100644
index 00000000000..746687d360f
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx
new file mode 100644
index 00000000000..283b32802e1
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/module.tsx
@@ -0,0 +1,29 @@
+import { ReactPanelPlugin } from '@grafana/ui';
+import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
+import { SingleStatPanel } from './SingleStatPanel';
+import cloneDeep from 'lodash/cloneDeep';
+import { SingleStatEditor } from './SingleStatEditor';
+
+export const reactPanel = new ReactPanelPlugin(SingleStatPanel);
+
+const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
+
+export const singleStatBaseOptionsCheck = (
+ options: Partial,
+ prevPluginId?: string,
+ prevOptions?: any
+) => {
+ if (prevOptions) {
+ optionsToKeep.forEach(v => {
+ if (prevOptions.hasOwnProperty(v)) {
+ options[v] = cloneDeep(prevOptions.display);
+ }
+ });
+ }
+
+ return options;
+};
+
+reactPanel.setEditor(SingleStatEditor);
+reactPanel.setDefaults(defaults);
+reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
diff --git a/public/app/plugins/panel/singlestat2/plugin.json b/public/app/plugins/panel/singlestat2/plugin.json
new file mode 100644
index 00000000000..6828399ec2b
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/plugin.json
@@ -0,0 +1,20 @@
+{
+ "type": "panel",
+ "name": "Singlestat (react)",
+ "id": "singlestat2",
+ "state": "alpha",
+
+ "dataFormats": ["time_series", "table"],
+
+ "info": {
+ "description": "Singlestat Panel for Grafana",
+ "author": {
+ "name": "Grafana Project",
+ "url": "https://grafana.com"
+ },
+ "logos": {
+ "small": "img/icn-singlestat-panel.svg",
+ "large": "img/icn-singlestat-panel.svg"
+ }
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/types.ts b/public/app/plugins/panel/singlestat2/types.ts
new file mode 100644
index 00000000000..1f31783e814
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/types.ts
@@ -0,0 +1,33 @@
+import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui';
+
+export interface SingleStatBaseOptions {
+ valueMappings: ValueMapping[];
+ thresholds: Threshold[];
+ valueOptions: SingleStatValueOptions;
+ orientation: VizOrientation;
+}
+
+export interface SingleStatValueOptions {
+ unit: string;
+ suffix: string;
+ stat: string;
+ prefix: string;
+ decimals?: number | null;
+}
+
+export interface SingleStatOptions extends SingleStatBaseOptions {
+ // TODO, fill in with options from angular
+}
+
+export const defaults: SingleStatOptions = {
+ valueOptions: {
+ prefix: '',
+ suffix: '',
+ decimals: null,
+ stat: 'avg',
+ unit: 'none',
+ },
+ valueMappings: [],
+ thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+ orientation: VizOrientation.Auto,
+};
diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts
index 268f5aa7ac4..b7b3c0312a3 100644
--- a/public/app/plugins/panel/table/module.ts
+++ b/public/app/plugins/panel/table/module.ts
@@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers';
import { tablePanelEditor } from './editor';
import { columnOptionsTab } from './column_options';
import { TableRenderer } from './renderer';
+import { isTableData } from '@grafana/ui';
class TablePanelCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
// automatically correct transform mode based on data
if (this.dataRaw && this.dataRaw.length) {
- if (this.dataRaw[0].type === 'table') {
+ if (isTableData(this.dataRaw[0])) {
this.panel.transform = 'table';
} else {
if (this.dataRaw[0].type === 'docs') {
diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx
index a7cd84f8ecb..70b652a8d2c 100644
--- a/public/app/plugins/panel/table2/TablePanel.tsx
+++ b/public/app/plugins/panel/table2/TablePanel.tsx
@@ -14,15 +14,15 @@ export class TablePanel extends Component {
}
render() {
- const { panelData, options } = this.props;
+ const { data, options } = this.props;
- if (!panelData || !panelData.tableData) {
+ if (data.length < 1) {
return No Table Data...
;
}
return (
- {theme => }
+ {theme => }
);
}
diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx
index bac292e8c29..b2e3057de34 100644
--- a/public/app/plugins/panel/text2/module.tsx
+++ b/public/app/plugins/panel/text2/module.tsx
@@ -8,3 +8,10 @@ export const reactPanel = new ReactPanelPlugin(TextPanel);
reactPanel.setEditor(TextPanelEditor);
reactPanel.setDefaults(defaults);
+reactPanel.setPanelTypeChangedHook((options: TextOptions, prevPluginId: string, prevOptions: any) => {
+ if (prevPluginId === 'text') {
+ return prevOptions as TextOptions;
+ }
+
+ return options;
+});
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts
index 442fb5acb0c..19bb96be603 100644
--- a/public/app/routes/routes.ts
+++ b/public/app/routes/routes.ts
@@ -195,7 +195,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
.when('/org/teams', {
template: ' ',
resolve: {
- roles: () => ['Editor', 'Admin'],
+ roles: () => (config.editorsCanAdmin ? [] : ['Editor', 'Admin']),
component: () => TeamList,
},
})
@@ -207,7 +207,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
.when('/org/teams/edit/:id/:page?', {
template: ' ',
resolve: {
- roles: () => ['Admin'],
+ roles: () => (config.editorsCanAdmin ? [] : ['Admin']),
component: () => TeamPages,
},
})
diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts
index 21f7bdac2d4..55e9bff620b 100644
--- a/public/app/types/acl.ts
+++ b/public/app/types/acl.ts
@@ -98,3 +98,23 @@ export const dashboardPermissionLevels: DashboardPermissionInfo[] = [
description: 'Can add/remove permissions and can add, edit and delete dashboards.',
},
];
+
+export enum TeamPermissionLevel {
+ Member = 0,
+ Admin = 4,
+}
+
+export interface TeamPermissionInfo {
+ value: TeamPermissionLevel;
+ label: string;
+ description: string;
+}
+
+export const teamsPermissionLevels: TeamPermissionInfo[] = [
+ { value: TeamPermissionLevel.Member, label: 'Member', description: 'Is team member' },
+ {
+ value: TeamPermissionLevel.Admin,
+ label: 'Admin',
+ description: 'Can add/remove permissions, members and delete team.',
+ },
+];
diff --git a/public/app/types/index.ts b/public/app/types/index.ts
index eefba746c61..3bf76aeb3c3 100644
--- a/public/app/types/index.ts
+++ b/public/app/types/index.ts
@@ -12,6 +12,5 @@ export * from './plugins';
export * from './organization';
export * from './appNotifications';
export * from './search';
-export * from './form';
export * from './explore';
export * from './store';
diff --git a/public/app/types/jquery.d.ts b/public/app/types/jquery/jquery.d.ts
similarity index 100%
rename from public/app/types/jquery.d.ts
rename to public/app/types/jquery/jquery.d.ts
diff --git a/public/app/types/search.ts b/public/app/types/search.ts
index e5e17288de1..e15797f41a3 100644
--- a/public/app/types/search.ts
+++ b/public/app/types/search.ts
@@ -1,9 +1,20 @@
+export enum DashboardSearchHitType {
+ DashHitDB = 'dash-db',
+ DashHitHome = 'dash-home',
+ DashHitFolder = 'dash-folder',
+}
export interface DashboardSearchHit {
id: number;
- tags: string[];
- title: string;
- type: string;
uid: string;
+ title: string;
uri: string;
url: string;
+ slug: string;
+ type: DashboardSearchHitType;
+ tags: string[];
+ isStarred: boolean;
+ folderId: number;
+ folderUid: string;
+ folderTitle: string;
+ folderUrl: string;
}
diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts
index a524f0dcf87..707ff97b738 100644
--- a/public/app/types/teams.ts
+++ b/public/app/types/teams.ts
@@ -1,9 +1,12 @@
+import { TeamPermissionLevel } from './acl';
+
export interface Team {
id: number;
name: string;
avatarUrl: string;
email: string;
memberCount: number;
+ permission: TeamPermissionLevel;
}
export interface TeamMember {
@@ -13,6 +16,7 @@ export interface TeamMember {
email: string;
login: string;
labels: string[];
+ permission: number;
}
export interface TeamGroup {
diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss
index 367d25ada6b..22c82461e85 100644
--- a/public/sass/components/_panel_logs.scss
+++ b/public/sass/components/_panel_logs.scss
@@ -299,6 +299,8 @@ $column-horizontal-spacing: 10px;
&__value {
flex: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
&__count,
diff --git a/public/sass/pages/_admin.scss b/public/sass/pages/_admin.scss
index 32f47212131..63cf1269f05 100644
--- a/public/sass/pages/_admin.scss
+++ b/public/sass/pages/_admin.scss
@@ -19,3 +19,9 @@ td.admin-settings-key {
margin-bottom: 5px;
}
}
+
+.admin-list-table {
+ .team-permissions {
+ padding-right: 120px;
+ }
+}
diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts
index 58403ac7ed7..f9124773c97 100644
--- a/public/test/specs/helpers.ts
+++ b/public/test/specs/helpers.ts
@@ -3,6 +3,7 @@ import config from 'app/core/config';
import * as dateMath from 'app/core/utils/datemath';
import { angularMocks, sinon } from '../lib/common';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
+import { PanelPlugin } from 'app/types';
export function ControllerTestContext(this: any) {
const self = this;
@@ -62,7 +63,7 @@ export function ControllerTestContext(this: any) {
$rootScope.colors.push('#' + i);
}
- config.panels['test'] = { info: {} };
+ config.panels['test'] = { info: {} } as PanelPlugin;
self.ctrl = $controller(
Ctrl,
{ $scope: self.scope },
diff --git a/scripts/build/ci-build/Dockerfile b/scripts/build/ci-build/Dockerfile
new file mode 100644
index 00000000000..7c6ed58c0e4
--- /dev/null
+++ b/scripts/build/ci-build/Dockerfile
@@ -0,0 +1,114 @@
+FROM ubuntu:14.04 as toolchain
+
+ENV OSX_SDK_URL=https://s3.dockerproject.org/darwin/v2/ \
+ OSX_SDK=MacOSX10.10.sdk \
+ OSX_MIN=10.10 \
+ CTNG=1.23.0
+
+# FIRST PART
+# build osx64 toolchain (stripped of man documentation)
+# the toolchain produced is not self contained, it needs clang at runtime
+#
+# SECOND PART
+# build gcc (no g++) centos6-x64 toolchain
+# doc: https://crosstool-ng.github.io/docs/
+# apt-get should be all dep to build toolchain
+# sed and 1st echo are for convenience to get the toolchain in /tmp/x86_64-centos6-linux-gnu
+# other echo are to enable build by root (crosstool-NG refuse to do that by default)
+# the last 2 rm are just to save some time and space writing docker layers
+#
+# THIRD PART
+# build fpm and creates a set of deb from gem
+# ruby2.0 depends on ruby1.9.3 which is install as default ruby
+# rm/ln are here to change that
+# created deb depends on rubygem-json but json gem is not build
+# so do by hand
+
+
+# might wanna make sure osx cross and the other tarball as well as the packages ends up somewhere other than tmp
+# might also wanna put them as their own layer to not have to unpack them every time?
+
+RUN apt-get update && \
+ apt-get install -y \
+ clang-3.8 patch libxml2-dev \
+ ca-certificates \
+ curl \
+ git \
+ make \
+ xz-utils && \
+ git clone https://github.com/tpoechtrager/osxcross.git /tmp/osxcross && \
+ curl -L ${OSX_SDK_URL}/${OSX_SDK}.tar.xz -o /tmp/osxcross/tarballs/${OSX_SDK}.tar.xz && \
+ ln -s /usr/bin/clang-3.8 /usr/bin/clang && \
+ ln -s /usr/bin/clang++-3.8 /usr/bin/clang++ && \
+ ln -s /usr/bin/llvm-dsymutil-3.8 /usr/bin/dsymutil && \
+ UNATTENDED=yes OSX_VERSION_MIN=${OSX_MIN} /tmp/osxcross/build.sh && \
+ rm -rf /tmp/osxcross/target/SDK/${OSX_SDK}/usr/share && \
+ cd /tmp && \
+ tar cfJ osxcross.tar.xz osxcross/target && \
+ rm -rf /tmp/osxcross && \
+ apt-get install -y \
+ bison curl flex gawk gcc g++ gperf help2man libncurses5-dev make patch python-dev texinfo xz-utils && \
+ curl -L http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-${CTNG}.tar.xz \
+ | tar -xJ -C /tmp/ && \
+ cd /tmp/crosstool-ng-${CTNG} && \
+ ./configure --enable-local && \
+ make && \
+ ./ct-ng x86_64-centos6-linux-gnu && \
+ sed -i '/CT_PREFIX_DIR=/d' .config && \
+ echo 'CT_PREFIX_DIR="/tmp/${CT_HOST:+HOST-${CT_HOST}/}${CT_TARGET}"' >> .config && \
+ echo 'CT_EXPERIMENTAL=y' >> .config && \
+ echo 'CT_ALLOW_BUILD_AS_ROOT=y' >> .config && \
+ echo 'CT_ALLOW_BUILD_AS_ROOT_SURE=y' >> .config && \
+ ./ct-ng build && \
+ cd /tmp && \
+ rm /tmp/x86_64-centos6-linux-gnu/build.log.bz2 && \
+ tar cfJ x86_64-centos6-linux-gnu.tar.xz x86_64-centos6-linux-gnu/ && \
+ rm -rf /tmp/x86_64-centos6-linux-gnu/ && \
+ rm -rf /tmp/crosstool-ng-${CTNG}
+
+# base image to crossbuild grafana
+FROM ubuntu:14.04
+
+ENV GOVERSION=1.11.5 \
+ PATH=/usr/local/go/bin:$PATH \
+ GOPATH=/go \
+ NODEVERSION=10.14.2
+
+COPY --from=toolchain /tmp/x86_64-centos6-linux-gnu.tar.xz /tmp/
+COPY --from=toolchain /tmp/osxcross.tar.xz /tmp/
+
+RUN apt-get update && \
+ apt-get install -y \
+ clang-3.8 gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-mingw-w64-x86-64 \
+ apt-transport-https \
+ ca-certificates \
+ curl \
+ libfontconfig1 \
+ gcc \
+ g++ \
+ git \
+ make \
+ rpm \
+ xz-utils \
+ expect \
+ gnupg2 \
+ unzip && \
+ ln -s /usr/bin/clang-3.8 /usr/bin/clang && \
+ ln -s /usr/bin/clang++-3.8 /usr/bin/clang++ && \
+ ln -s /usr/bin/llvm-dsymutil-3.8 /usr/bin/dsymutil && \
+ curl -L https://nodejs.org/dist/v${NODEVERSION}/node-v${NODEVERSION}-linux-x64.tar.xz \
+ | tar -xJ --strip-components=1 -C /usr/local && \
+ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
+ echo "deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main" \
+ | tee /etc/apt/sources.list.d/yarn.list && \
+ apt-get update && apt-get install --no-install-recommends yarn && \
+ curl -L https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz \
+ | tar -xz -C /usr/local
+
+RUN apt-get install -y \
+ gcc libc-dev make && \
+ gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \
+ curl -sSL https://get.rvm.io | bash -s stable && \
+ /bin/bash -l -c "rvm requirements && rvm install 2.2 && gem install -N fpm"
+
+ADD ./bootstrap.sh /tmp/bootstrap.sh
\ No newline at end of file
diff --git a/scripts/build/ci-build/Makefile b/scripts/build/ci-build/Makefile
new file mode 100644
index 00000000000..64fa376d7cf
--- /dev/null
+++ b/scripts/build/ci-build/Makefile
@@ -0,0 +1,54 @@
+VERSION="dev"
+TAG="grafana/build-container"
+USER_ID=$(shell id -u)
+GROUP_ID=$(shell id -g)
+
+all: build deploy
+
+build:
+ docker build -t "${TAG}:${VERSION}" .
+
+deploy:
+ docker push "${TAG}:${VERSION}"
+
+run:
+ docker run -ti \
+ -e "CIRCLE_BRANCH=local" \
+ -e "CIRCLE_BUILD_NUM=472" \
+ ${TAG}:${VERSION} \
+ bash
+
+run-with-local-source-live:
+ docker run -d \
+ -e "CIRCLE_BRANCH=local" \
+ -e "CIRCLE_BUILD_NUM=472" \
+ -w "/go/src/github.com/grafana/grafana" \
+ --name grafana-build \
+ -v "${GOPATH}/src/github.com/grafana/grafana:/go/src/github.com/grafana/grafana" \
+ ${TAG}:${VERSION} \
+ bash -c "/tmp/bootstrap.sh; mkdir /.cache; chown "${USER_ID}:${GROUP_ID}" /.cache; tail -f /dev/null"
+ docker exec -ti --user "${USER_ID}:${GROUP_ID}" grafana-build bash
+
+run-with-local-source-copy:
+ docker run -d \
+ -e "CIRCLE_BRANCH=local" \
+ -e "CIRCLE_BUILD_NUM=472" \
+ -w "/go/src/github.com/grafana/grafana" \
+ --name grafana-build \
+ ${TAG}:${VERSION} \
+ bash -c "/tmp/bootstrap.sh; tail -f /dev/null"
+ docker cp "${GOPATH}/src/github.com/grafana/grafana" grafana-build:/go/src/github.com/grafana/
+ docker exec -ti grafana-build bash
+
+update-source:
+ docker cp "${GOPATH}/src/github.com/grafana/grafana" grafana-build:/go/src/github.com/grafana/
+
+attach:
+ docker exec -ti grafana-build bash
+
+attach-live:
+ docker exec -ti --user "${USER_ID}:${GROUP_ID}" grafana-build bash
+
+stop:
+ docker kill grafana-build
+ docker rm grafana-build
diff --git a/scripts/build/ci-build/README.md b/scripts/build/ci-build/README.md
new file mode 100644
index 00000000000..e66ec1b3cf7
--- /dev/null
+++ b/scripts/build/ci-build/README.md
@@ -0,0 +1,20 @@
+# grafana-build-container
+Grafana build container
+
+## Description
+
+This is a container for cross-platform builds of Grafana. You can run it locally using the Makefile.
+
+## Makefile targets
+
+* `make run-with-local-source-copy`
+ - Starts the container locally and copies your local sources into the container
+* `make run-with-local-source-live`
+ - Starts the container (as your user) locally and maps your Grafana project dir into the container
+* `make update-source`
+ - Updates the sources in the container from your local sources
+* `make stop`
+ - Kills the container
+* `make attach`
+ - Opens bash within the running container
+
diff --git a/scripts/build/ci-build/bootstrap.sh b/scripts/build/ci-build/bootstrap.sh
new file mode 100755
index 00000000000..2eda345b5ab
--- /dev/null
+++ b/scripts/build/ci-build/bootstrap.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+cd /tmp
+tar xfJ x86_64-centos6-linux-gnu.tar.xz
+tar xfJ osxcross.tar.xz
diff --git a/scripts/build/ci-build/build-deploy.sh b/scripts/build/ci-build/build-deploy.sh
new file mode 100755
index 00000000000..c2a33e4a9e4
--- /dev/null
+++ b/scripts/build/ci-build/build-deploy.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+_version="1.2.3"
+_tag="grafana/build-container:${_version}"
+
+docker build -t $_tag .
+docker push $_tag
diff --git a/scripts/build/ci-deploy/Dockerfile b/scripts/build/ci-deploy/Dockerfile
index dd4987b96c3..e608d9156e7 100644
--- a/scripts/build/ci-deploy/Dockerfile
+++ b/scripts/build/ci-deploy/Dockerfile
@@ -10,7 +10,7 @@ FROM circleci/python:2.7-stretch
USER root
-RUN pip install awscli && \
+RUN pip install -U awscli crcmod && \
curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-222.0.0-linux-x86_64.tar.gz | \
tar xvzf - -C /opt && \
apt update && \
diff --git a/scripts/build/ci-deploy/build-deploy.sh b/scripts/build/ci-deploy/build-deploy.sh
index 8dedeead009..ed9c9e5459e 100755
--- a/scripts/build/ci-deploy/build-deploy.sh
+++ b/scripts/build/ci-deploy/build-deploy.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-_version="1.2.0"
+_version="1.2.1"
_tag="grafana/grafana-ci-deploy:${_version}"
docker build -t $_tag .
diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh
new file mode 100755
index 00000000000..bacd9928362
--- /dev/null
+++ b/scripts/circle-test-cache-servers.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+function exit_if_fail {
+ command=$@
+ echo "Executing '$command'"
+ eval $command
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ echo "'$command' returned $rc."
+ exit $rc
+ fi
+}
+
+echo "running redis and memcache tests"
+
+time exit_if_fail go test -tags=redis ./pkg/infra/remotecache/...
+time exit_if_fail go test -tags=memcached ./pkg/infra/remotecache/...
diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh
index 9d945a03b7f..423dee84954 100755
--- a/scripts/circle-test-frontend.sh
+++ b/scripts/circle-test-frontend.sh
@@ -14,7 +14,7 @@ function exit_if_fail {
start=$(date +%s)
exit_if_fail npm run prettier:check
-# exit_if_fail npm run test
+exit_if_fail npm run test
end=$(date +%s)
seconds=$((end - start))
diff --git a/scripts/cli/utils/useSpinner.ts b/scripts/cli/utils/useSpinner.ts
index 81ed9bb6fcf..298a6516689 100644
--- a/scripts/cli/utils/useSpinner.ts
+++ b/scripts/cli/utils/useSpinner.ts
@@ -10,8 +10,7 @@ export const useSpinner = (spinnerLabel: string, fn: FnToSpin, killProcess
await fn(options);
spinner.succeed();
} catch (e) {
- spinner.fail();
- console.log(e);
+ spinner.fail(e);
if (killProcess) {
process.exit(1);
}
diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js
index 8a71ea26627..a656e0c60af 100644
--- a/scripts/grunt/default_task.js
+++ b/scripts/grunt/default_task.js
@@ -34,14 +34,17 @@ module.exports = function (grunt) {
]);
grunt.registerTask('no-only-tests', function () {
- var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js');
+ var files = grunt.file.expand(
+ 'public/**/*@(_specs|\.test)\.@(ts|js|tsx|jsx)',
+ 'packages/grafana-ui/**/*@(_specs|\.test)\.@(ts|js|tsx|jsx)'
+ );
files.forEach(function (spec) {
var rows = grunt.file.read(spec).split('\n');
rows.forEach(function (row) {
if (row.indexOf('.only(') > 0) {
grunt.log.errorlns(row);
- grunt.fail.warn('found only statement in test: ' + spec)
+ grunt.fail.warn('found only statement in test: ' + spec);
}
});
});
diff --git a/style_guides/backend.md b/style_guides/backend.md
new file mode 100644
index 00000000000..1c6c86efc0b
--- /dev/null
+++ b/style_guides/backend.md
@@ -0,0 +1,30 @@
+# Backend style guide
+
+Grafanas backend has been developed for a long time with a mix of code styles.
+
+This style guide is a guide for how we want to write Go code in the future. Generally, we want to follow the style guides used in Go [Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style)
+
+
+## Global state
+Global state makes testing and debugging software harder and its something we want to avoid when possible.
+Unfortunately, there is quite a lot of global state in Grafana. The way we want to migrate away from this
+is to use the `inject` package to wire up all dependencies either in `pkg/cmd/grafana-server/main.go` or
+self registering using `registry.RegisterService` ex https://github.com/grafana/grafana/blob/master/pkg/services/cleanup/cleanup.go#L25
+
+### the `bus`
+`bus.Dispatch` is used in many places and something we want to avoid in the future since it refers to a global instance.
+The preferred solution, in this case, is to inject the `bus` into services or take the bus instance as a parameter into functions.
+
+### settings package
+In the `setting` packages there are many global variables which Grafana sets at startup. This is also something we want to move
+away from and move as much configuration as possible to the `setting.Cfg` struct and pass it around, just like the bus.
+
+## Linting and formatting
+We enforce strict `gofmt` formating and use some linters on our codebase. You can find the current list of linters at https://github.com/grafana/grafana/blob/master/scripts/gometalinter.sh#L23
+
+We don't enforce `golint` but we encourage it and we will test so the number of linting errors does not increase over time.
+
+## Testing
+We use GoConvey for BDD/scenario based testing. Which we think is useful for testing certain chain or interactions. Ex https://github.com/grafana/grafana/blob/master/pkg/services/auth/auth_token_test.go
+
+For smaller tests its preferred to use standard library testing.
diff --git a/style_guides/frontend.md b/style_guides/frontend.md
index caef4f711ef..18069183e66 100644
--- a/style_guides/frontend.md
+++ b/style_guides/frontend.md
@@ -1,36 +1,36 @@
# Frontend Style Guide
-Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react).
+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)
+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.
+- 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 small component do not need their own folder
- * Place test next to their component file (same 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?
+- 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 small component do not need their own folder
+ - Place test next to their component file (same 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.
+- Name callback props & handlers with a "on" prefix.
```tsx
// good
@@ -56,5 +56,32 @@ render() {
}
```
+- React Component definitions
+```jsx
+// good
+export class YourClass extends PureComponent<{},{}> { ... }
+// bad
+export class YourClass extends PureComponent { ... }
+```
+
+- React Component constructor
+
+```typescript
+// good
+constructor(props:Props) {...}
+
+// bad
+constructor(props) {...}
+```
+
+- React Component defaultProps
+
+```typescript
+// good
+static defaultProps: Partial = { ... }
+
+// bad
+static defaultProps = { ... }
+```
diff --git a/style_guides/pull-request-review-checklist.md b/style_guides/pull-request-review-checklist.md
new file mode 100644
index 00000000000..2fd017386ea
--- /dev/null
+++ b/style_guides/pull-request-review-checklist.md
@@ -0,0 +1,36 @@
+# Pull Request Review Checklist
+
+## High level checks
+
+- [ ] The pull request adds value and the impact of the change is in line with [Frontend Style Guide](https://github.com/grafana/grafana/blob/master/style_guides/frontend.md).
+- [ ] The pull request works the way it says it should do.
+- [ ] The pull request does not increase the Angular code base.
+ > We are in the process of migrating to React so any increment of Angular code is generally discouraged from. (there are a few exceptions)
+- [ ] The pull request closes one issue if possible and does not fix unrelated issues within the same pull request.
+- [ ] The pull request contains necessary tests.
+
+## Low level checks
+
+- [ ] The pull request contains a title that explains the PR.
+- [ ] The pull request contains necessary link(s) to issue(s).
+- [ ] The pull request contains commits with commit messages that are small and understandable.
+- [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead.
+
+### Bug specific checks
+
+- [ ] The pull request contains only one commit if possible.
+- [ ] The pull request contains `closes: #Issue` or `fixes: #Issue` in pull request description.
+
+## Frontend specific checks
+
+- [ ] The pull request does not increase the number of `implicit any` errors.
+- [ ] The pull request does not contain uses of `any` or `{}` without comments describing why.
+- [ ] The pull request does not contain large React component that could easily be split into several smaller components.
+- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead.
+
+### Redux specific checks (skip if pull request does not contain Redux changes)
+
+- [ ] The pull request does not contain code that mutate state in reducers or thunks.
+- [ ] The pull request uses helpers `actionCreatorFactory` and `reducerFactory` instead of traditional `switch statement` reducers in Redux. ([Redux framework](https://github.com/grafana/grafana/blob/master/style_guides/redux.md))
+- [ ] The pull request uses `reducerTester` to test reducers.([Redux framework](https://github.com/grafana/grafana/blob/master/style_guides/redux.md))
+- [ ] The pull request does not contain code that access reducers state slice directly, instead the code uses state selectors to access state.
diff --git a/style_guides/redux.md b/style_guides/redux.md
new file mode 100644
index 00000000000..ff64fe400f3
--- /dev/null
+++ b/style_guides/redux.md
@@ -0,0 +1,158 @@
+# Redux framework
+
+To reduce the amount of boilerplate code used to create a strongly typed redux solution with actions, action creators, reducers and tests we've introduced a small framework around Redux.
+
+`+` Much less boilerplate code
+`-` Non Redux standard api
+
+## New core functionality
+
+### actionCreatorFactory
+
+Used to create an action creator with the following signature
+
+```typescript
+{ type: string , (payload: T): {type: string; payload: T;} }
+```
+
+where the `type` string will be ensured to be unique and `T` is the type supplied to the factory.
+
+#### Example
+
+```typescript
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+
+// later when dispatched
+someAction('this rocks!');
+```
+
+```typescript
+// best practices, always use an interface as type
+interface SomeAction {
+ data: string;
+}
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+
+// later when dispatched
+someAction({ data: 'best practices' });
+```
+
+```typescript
+// declaring an action creator with a type string that has already been defined will throw
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+export const theAction = actionCreatorFactory('SOME_ACTION').create(); // will throw
+```
+
+### noPayloadActionCreatorFactory
+
+Used when you don't need to supply a payload for your action. Will create an action creator with the following signature
+
+```typescript
+{ type: string , (): {type: string; payload: undefined;} }
+```
+
+where the `type` string will be ensured to be unique.
+
+#### Example
+
+```typescript
+export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create();
+
+// later when dispatched
+noPayloadAction();
+```
+
+```typescript
+// declaring an action creator with a type string that has already been defined will throw
+export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create();
+export const noAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create(); // will throw
+```
+
+### reducerFactory
+
+Fluent API used to create a reducer. (same as implementing the standard switch statement in Redux)
+
+#### Example
+
+```typescript
+interface ExampleReducerState {
+ data: string[];
+}
+
+const intialState: ExampleReducerState = { data: [] };
+
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+export const otherAction = actionCreatorFactory('Other_ACTION').create();
+
+export const exampleReducer = reducerFactory(intialState)
+ // addMapper is the function that ties an action creator to a state change
+ .addMapper({
+ // action creator to filter out which mapper to use
+ filter: someAction,
+ // mapper function where the state change occurs
+ mapper: (state, action) => ({ ...state, data: state.data.concat(action.payload) }),
+ })
+ // a developer can just chain addMapper functions until reducer is done
+ .addMapper({
+ filter: otherAction,
+ mapper: (state, action) => ({ ...state, data: action.payload }),
+ })
+ .create(); // this will return the reducer
+```
+
+#### Typing limitations
+
+There is a challenge left with the mapper function that I can not solve with TypeScript. The signature of a mapper is
+
+```typescript
+(state: State, action: ActionOf) => State;
+```
+
+If you would to return an object that is not of the state type like the following mapper
+
+```typescript
+mapper: (state, action) => ({ nonExistingProperty: ''}),
+```
+
+Then you would receive the following compile error
+
+```shell
+[ts] Property 'data' is missing in type '{ nonExistingProperty: string; }' but required in type 'ExampleReducerState'. [2741]
+```
+
+But if you return an object that is spreading state and add a non existing property type like the following mapper
+
+```typescript
+mapper: (state, action) => ({ ...state, nonExistingProperty: ''}),
+```
+
+Then you would not receive any compile error.
+
+If you want to make sure that never happens you can just supply the State type to the mapper callback like the following mapper:
+
+```typescript
+mapper: (state, action): ExampleReducerState => ({ ...state, nonExistingProperty: 'kalle' }),
+```
+
+Then you would receive the following compile error
+
+```shell
+[ts]
+Type '{ nonExistingProperty: string; data: string[]; }' is not assignable to type 'ExampleReducerState'.
+ Object literal may only specify known properties, and 'nonExistingProperty' does not exist in type 'ExampleReducerState'. [2322]
+```
+
+## New test functionality
+
+### reducerTester
+
+Fluent API that simplifies the testing of reducers
+
+#### Example
+
+```typescript
+reducerTester()
+ .givenReducer(someReducer, initialState)
+ .whenActionIsDispatched(someAction('reducer tests'))
+ .thenStateShouldEqual({ ...initialState, data: 'reducer tests' });
+```
diff --git a/tsconfig.json b/tsconfig.json
index f223c027af6..68a68abc58c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -27,7 +27,7 @@
"noUnusedLocals": true,
"baseUrl": "public",
"pretty": true,
- "typeRoots": ["node_modules/@types", "types"],
+ "typeRoots": ["node_modules/@types", "public/app/types"],
"paths": {
"app": ["app"],
"sass": ["sass"]
diff --git a/yarn.lock b/yarn.lock
index bdce2e10a6d..188b5d3a41d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -50,29 +50,23 @@
source-map "^0.5.0"
"@babel/core@^7.1.6":
- version "7.2.2"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687"
- integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw==
dependencies:
"@babel/code-frame" "^7.0.0"
- "@babel/generator" "^7.2.2"
+ "@babel/generator" "^7.3.4"
"@babel/helpers" "^7.2.0"
- "@babel/parser" "^7.2.2"
+ "@babel/parser" "^7.3.4"
"@babel/template" "^7.2.2"
- "@babel/traverse" "^7.2.2"
- "@babel/types" "^7.2.2"
+ "@babel/traverse" "^7.3.4"
+ "@babel/types" "^7.3.4"
convert-source-map "^1.1.0"
debug "^4.1.0"
json5 "^2.1.0"
- lodash "^4.17.10"
+ lodash "^4.17.11"
resolve "^1.3.2"
semver "^5.4.1"
source-map "^0.5.0"
-"@babel/generator@^7.0.0", "@babel/generator@^7.2.2":
- version "7.3.2"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.2.tgz#fff31a7b2f2f3dad23ef8e01be45b0d5c2fc0132"
- integrity sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==
+"@babel/generator@^7.0.0", "@babel/generator@^7.3.4":
dependencies:
"@babel/types" "^7.3.2"
jsesc "^2.5.1"
@@ -123,16 +117,17 @@
"@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0"
-"@babel/helper-create-class-features-plugin@^7.3.0":
- version "7.3.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.2.tgz#ba1685603eb1c9f2f51c9106d5180135c163fe73"
- integrity sha512-tdW8+V8ceh2US4GsYdNVNoohq5uVwOf9k6krjwW4E1lINcHgttnWcNqgdoessn12dAy8QkbezlbQh2nXISNY+A==
+"@babel/helper-create-class-features-plugin@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c"
+ integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g==
dependencies:
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-member-expression-to-functions" "^7.0.0"
"@babel/helper-optimise-call-expression" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
- "@babel/helper-replace-supers" "^7.2.3"
+ "@babel/helper-replace-supers" "^7.3.4"
+ "@babel/helper-split-export-declaration" "^7.0.0"
"@babel/helper-define-map@^7.1.0":
version "7.1.0"
@@ -230,10 +225,7 @@
"@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0"
-"@babel/helper-replace-supers@^7.0.0", "@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5"
- integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA==
+"@babel/helper-replace-supers@^7.0.0", "@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4":
dependencies:
"@babel/helper-member-expression-to-functions" "^7.0.0"
"@babel/helper-optimise-call-expression" "^7.0.0"
@@ -298,10 +290,6 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c"
integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==
-"@babel/parser@^7.1.0", "@babel/parser@^7.1.3", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3":
- version "7.3.2"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.2.tgz#95cdeddfc3992a6ca2a1315191c1679ca32c55cd"
- integrity sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==
"@babel/plugin-proposal-async-generator-functions@^7.1.0", "@babel/plugin-proposal-async-generator-functions@^7.2.0":
version "7.2.0"
@@ -325,11 +313,11 @@
"@babel/plugin-syntax-class-properties" "^7.0.0"
"@babel/plugin-proposal-class-properties@^7.2.0":
- 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==
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.4.tgz#410f5173b3dc45939f9ab30ca26684d72901405e"
+ integrity sha512-lUf8D3HLs4yYlAo8zjuneLvfxN7qfKv1Yzbj5vjqaqMJxgJA3Ipwp4VUJ+OrOdz53Wbww6ahwB8UhB2HQyLotA==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.3.0"
+ "@babel/helper-create-class-features-plugin" "^7.3.4"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-decorators@7.1.2":
@@ -358,10 +346,7 @@
"@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.3.1":
- version "7.3.2"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1"
- integrity sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA==
+"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.3.4":
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
@@ -462,9 +447,9 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-typescript@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.2.0.tgz#55d240536bd314dcbbec70fd949c5cabaed1de29"
- integrity sha512-WhKr6yu6yGpGcNMVgIBuI9MkredpVc7Y3YR4UzEZmDztHoL6wV56YBHLhWnjO1EvId1B32HrD3DRFc+zSoKI1g==
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991"
+ integrity sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
@@ -475,10 +460,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-transform-async-to-generator@^7.1.0", "@babel/plugin-transform-async-to-generator@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff"
- integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ==
+"@babel/plugin-transform-async-to-generator@^7.1.0", "@babel/plugin-transform-async-to-generator@^7.3.4":
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
@@ -500,10 +482,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4"
- integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q==
+"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.3.4":
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
lodash "^4.17.10"
@@ -530,10 +509,7 @@
"@babel/helper-split-export-declaration" "^7.0.0"
globals "^11.1.0"
-"@babel/plugin-transform-classes@^7.1.0", "@babel/plugin-transform-classes@^7.2.0":
- version "7.2.2"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz#6c90542f210ee975aa2aa8c8b5af7fa73a126953"
- integrity sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ==
+"@babel/plugin-transform-classes@^7.1.0", "@babel/plugin-transform-classes@^7.3.4":
dependencies:
"@babel/helper-annotate-as-pure" "^7.0.0"
"@babel/helper-define-map" "^7.1.0"
@@ -612,9 +588,9 @@
"@babel/plugin-syntax-flow" "^7.0.0"
"@babel/plugin-transform-flow-strip-types@^7.0.0":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.2.3.tgz#e3ac2a594948454e7431c7db33e1d02d51b5cd69"
- integrity sha512-xnt7UIk9GYZRitqCnsVMjQK1O2eKZwFB3CvvHjf5SGx6K6vr/MScCKQDnf1DxRaj501e3pXjti+inbSXX2ZUoQ==
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.3.4.tgz#00156236defb7dedddc2d3c9477dcc01a4494327"
+ integrity sha512-PmQC9R7DwpBFA+7ATKMyzViz3zCaMNouzZMPZN2K5PnbBbtL3AXFYTkDk+Hey5crQq2A90UG5Uthz0mel+XZrA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-flow" "^7.2.0"
@@ -658,10 +634,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-simple-access" "^7.1.0"
-"@babel/plugin-transform-modules-systemjs@^7.0.0", "@babel/plugin-transform-modules-systemjs@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068"
- integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ==
+"@babel/plugin-transform-modules-systemjs@^7.0.0", "@babel/plugin-transform-modules-systemjs@^7.3.4":
dependencies:
"@babel/helper-hoist-variables" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
@@ -705,9 +678,9 @@
"@babel/helper-replace-supers" "^7.1.0"
"@babel/plugin-transform-parameters@^7.1.0", "@babel/plugin-transform-parameters@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2"
- integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA==
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30"
+ integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw==
dependencies:
"@babel/helper-call-delegate" "^7.1.0"
"@babel/helper-get-function-arity" "^7.0.0"
@@ -768,10 +741,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-jsx" "^7.2.0"
-"@babel/plugin-transform-regenerator@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1"
- integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==
+"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.3.4":
dependencies:
regenerator-transform "^0.13.3"
@@ -902,9 +872,6 @@
semver "^5.3.0"
"@babel/preset-env@^7.1.6", "@babel/preset-env@^7.2.0":
- 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"
@@ -1042,9 +1009,9 @@
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2":
- version "7.3.1"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a"
- integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
+ integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
dependencies:
regenerator-runtime "^0.12.0"
@@ -1072,25 +1039,18 @@
globals "^11.1.0"
lodash "^4.17.11"
-"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3":
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8"
- integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==
dependencies:
"@babel/code-frame" "^7.0.0"
- "@babel/generator" "^7.2.2"
+ "@babel/generator" "^7.3.4"
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.0.0"
- "@babel/parser" "^7.2.3"
- "@babel/types" "^7.2.2"
+ "@babel/parser" "^7.3.4"
+ "@babel/types" "^7.3.4"
debug "^4.1.0"
globals "^11.1.0"
- lodash "^4.17.10"
+ lodash "^4.17.11"
-"@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", "@babel/types@^7.3.2":
- version "7.3.2"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.2.tgz#424f5be4be633fff33fb83ab8d67e4a8290f5a2f"
- integrity sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==
+"@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", "@babel/types@^7.3.4":
dependencies:
esutils "^2.0.2"
lodash "^4.17.10"
@@ -1229,20 +1189,6 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.1.3.tgz#b700d97385fa91affed60c71dfd51c67e9dad762"
integrity sha512-QsYGKdhhuDFNq7bjm2r44y0mp5xW3uO3csuTPDWZc0OIiMQv+AIY5Cqwd4mJiC5N8estVl7qlvOx1hbtOuUWbw==
-"@iamstarkov/listr-update-renderer@0.4.1":
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e"
- integrity sha512-IJyxQWsYDEkf8C8QthBn5N8tIUR9V9je6j3sMIpAkonaadjbvxmRC6RAhpa3RKxndhNnU2M6iNbtJwd7usQYIA==
- dependencies:
- chalk "^1.1.3"
- cli-truncate "^0.2.1"
- elegant-spinner "^1.0.1"
- figures "^1.7.0"
- indent-string "^3.0.0"
- log-symbols "^1.0.2"
- log-update "^2.3.0"
- strip-ansi "^3.0.1"
-
"@icons/material@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
@@ -1418,16 +1364,16 @@
any-observable "^0.3.0"
"@storybook/addon-actions@^4.1.7":
- 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==
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-4.1.14.tgz#4bd962da767aa6a99867114894229c739f87d780"
+ integrity sha512-zq8MSSLXv+D+e/m3LdRrAMVrJP5xcETO4OtutJCdJeAkIjMvstnWncUEUHMltv/+bTBQgb77uoTFKVN4zrJL2g==
dependencies:
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
"@emotion/styled" "^0.10.6"
- "@storybook/addons" "4.1.11"
- "@storybook/components" "4.1.11"
- "@storybook/core-events" "4.1.11"
+ "@storybook/addons" "4.1.14"
+ "@storybook/components" "4.1.14"
+ "@storybook/core-events" "4.1.14"
core-js "^2.5.7"
deep-equal "^1.0.1"
global "^4.3.2"
@@ -1438,13 +1384,13 @@
uuid "^3.3.2"
"@storybook/addon-info@^4.1.6":
- 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==
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-info/-/addon-info-4.1.14.tgz#d0e155413881403e9dd0bcba04a05199141239a2"
+ integrity sha512-PcJfcZ0WnC+mr+AIn1LfacjruG6cLnCC98BdxpdTOMK+nncR/JffJ7ySqykFEnwRfLuSHGglIg+LwjUViQ4mwA==
dependencies:
- "@storybook/addons" "4.1.11"
- "@storybook/client-logger" "4.1.11"
- "@storybook/components" "4.1.11"
+ "@storybook/addons" "4.1.14"
+ "@storybook/client-logger" "4.1.14"
+ "@storybook/components" "4.1.14"
core-js "^2.5.7"
global "^4.3.2"
marksy "^6.1.0"
@@ -1455,14 +1401,14 @@
util-deprecate "^1.0.2"
"@storybook/addon-knobs@^4.1.7":
- version "4.1.11"
- resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-4.1.11.tgz#fd6c90d62a5bf5f94899746a95b02f1ef127cd81"
- integrity sha512-UQzYZoo0WKHKHSayaEBLvyZNqlqCOKahXzT2r+hS3t6wRnHJSfPtEHD0xTYMJSkA5t+bhlIOFJy0tribd0sdPQ==
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-4.1.14.tgz#48bc37f58707cb00960a501f24f702c993f17ad8"
+ integrity sha512-ScMi3iZxdOYMzVv7jMUqfgyPBWYwkSW4OLrVFIr5/EPP9mAOwnlYz+tI/8zM6w7/cvdEPY2pCilKt2zhzEMQ9w==
dependencies:
"@emotion/styled" "^0.10.6"
- "@storybook/addons" "4.1.11"
- "@storybook/components" "4.1.11"
- "@storybook/core-events" "4.1.11"
+ "@storybook/addons" "4.1.14"
+ "@storybook/components" "4.1.14"
+ "@storybook/core-events" "4.1.14"
copy-to-clipboard "^3.0.8"
core-js "^2.5.7"
escape-html "^1.0.3"
@@ -1474,39 +1420,39 @@
react-lifecycles-compat "^3.0.4"
util-deprecate "^1.0.2"
-"@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==
+"@storybook/addons@4.1.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.1.14.tgz#e976ac21b82043efb8fb2289063b370d94c29eae"
+ integrity sha512-nAieZLLeXzuUY8TIHshzVX8VUtUowsyohIKilZSzLIBFMpCEYlyPBilrE9ULqUEtVQWi5peAiKRZ1xJQxx/TIA==
dependencies:
- "@storybook/channels" "4.1.11"
- "@storybook/components" "4.1.11"
+ "@storybook/channels" "4.1.14"
+ "@storybook/components" "4.1.14"
global "^4.3.2"
util-deprecate "^1.0.2"
-"@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==
+"@storybook/channel-postmessage@4.1.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-4.1.14.tgz#003dfbf8dd62f4f3f9d9b7766e72f57377a8589e"
+ integrity sha512-Kz1oOoJXoqZYWh7V4yykEAKsZwegY/VQzEIFQjlyCAvNyfCvN35AgAdrgLCU2Uwss5lds18SkdwxCOxmH2lnEA==
dependencies:
- "@storybook/channels" "4.1.11"
+ "@storybook/channels" "4.1.14"
global "^4.3.2"
json-stringify-safe "^5.0.1"
-"@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.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-4.1.14.tgz#8af55d028d7f86f5a9abb9329df7e9d1121f404b"
+ integrity sha512-2BpKF7MXWfJeY9lRHrUseJ6JoZXshmo9B4np7TAex3NXOeRLsptQMCfQXgMgUMDQhABc0o9XBAruQOb9zMyd7w==
-"@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.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-4.1.14.tgz#73a408280af594f66259f750bd058534f7a3a95f"
+ integrity sha512-8Vb4DaGvUsc/voPxOW2LAJZBK5ac8btvesGZZJO43HVWpmROLPrCcoKIUFLWgPmmXyHTkLuheTn9tcbok1BowQ==
-"@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==
+"@storybook/components@4.1.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/components/-/components-4.1.14.tgz#54e33ab9bf09ce75f1b97e9212672921444a9fdc"
+ integrity sha512-UdSwxZutRUW8umaaPnris5M9pGz2220KXs/5g2URaw3rEL8eUaKLKJXSpFSOeXPhlCnV7B9R/qanG9GXGQKXmg==
dependencies:
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
@@ -1519,27 +1465,27 @@
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.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-4.1.14.tgz#919a577bab7d4c45ea6cb83f6cf8b35782a8f671"
+ integrity sha512-UBVTWZonTD5hHR7huWs15mYMKJONjQC6GBQy5AvKDpAIXtAlHe75mv5aZM5gRTP/ThtNE9V1HO4IF3C54dvUjg==
-"@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==
+"@storybook/core@4.1.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/core/-/core-4.1.14.tgz#488e6f8b4cbb9c1346e8c3755437e0269f790b74"
+ integrity sha512-864gvxZNaL3vjskyPkXqx88y+C51KenVY4g5AdX1MrerHHSzPzfSS9PPO+XT8rrJO6FQ5vRHyG9sxa0MkG6O3A==
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.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"
+ "@storybook/addons" "4.1.14"
+ "@storybook/channel-postmessage" "4.1.14"
+ "@storybook/client-logger" "4.1.14"
+ "@storybook/core-events" "4.1.14"
+ "@storybook/node-logger" "4.1.14"
+ "@storybook/ui" "4.1.14"
airbnb-js-shims "^1 || ^2"
autoprefixer "^9.3.1"
babel-plugin-macros "^2.4.2"
@@ -1603,10 +1549,10 @@
"@storybook/react-simple-di" "^1.2.1"
babel-runtime "6.x.x"
-"@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==
+"@storybook/node-logger@4.1.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-4.1.14.tgz#d97ba3f8bd727dc0a0266aaabc07e3111a58f0ca"
+ integrity sha512-eUWa+PnZ2t/j74PghtcjMINkPRLRWi8ekSbrbYsiwfErG6B6r/m89Ca6gvg/51o1e12rPz5B4cbCv6gtZEa7UA==
dependencies:
chalk "^2.4.1"
core-js "^2.5.7"
@@ -1651,16 +1597,16 @@
babel-runtime "^6.5.0"
"@storybook/react@^4.1.4":
- version "4.1.11"
- resolved "https://registry.yarnpkg.com/@storybook/react/-/react-4.1.11.tgz#fb3cda82fd3334a6653325ec281a2da284ac6895"
- integrity sha512-NPNcfOlWmFBevza/+GXIK23h46HqdWKFmId19E0PtoWGoR5xOR56rnwagr2+yHngUb4AATUCzhzTwxfWGxSvBg==
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/react/-/react-4.1.14.tgz#fb1e68ffabaed258fa90cf328500803715b45bf6"
+ integrity sha512-sza+wvWymLv2e6aGEcVO4ka5J3gde298jRw3Qjf7DBHfKx0dBx1QCwNTCUn3Zw6NZvY3Li2gFHN1MFAwshsZtA==
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.11"
- "@storybook/node-logger" "4.1.11"
+ "@storybook/core" "4.1.14"
+ "@storybook/node-logger" "4.1.14"
"@svgr/webpack" "^4.0.3"
babel-plugin-named-asset-import "^0.2.3"
babel-plugin-react-docgen "^2.0.0"
@@ -1676,16 +1622,16 @@
semver "^5.6.0"
webpack "^4.23.1"
-"@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==
+"@storybook/ui@4.1.14":
+ version "4.1.14"
+ resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-4.1.14.tgz#5dd50490fcede29d552ffe4e0870648a35f0c997"
+ integrity sha512-GV7MKjcFkfBmr7odrb/oJJ2VlDE1kKXjOo0Fk+3jYhdOd8JHv60Xj5z7Q91xqxPLwThNUZkCMUeWdbvyYRykFw==
dependencies:
"@emotion/core" "^0.13.1"
"@emotion/provider" "^0.11.2"
"@emotion/styled" "^0.10.6"
- "@storybook/components" "4.1.11"
- "@storybook/core-events" "4.1.11"
+ "@storybook/components" "4.1.14"
+ "@storybook/core-events" "4.1.14"
"@storybook/mantra-core" "^1.7.2"
"@storybook/podda" "^1.2.3"
"@storybook/react-komposer" "^2.0.5"
@@ -1881,15 +1827,22 @@
chalk "*"
"@types/cheerio@*":
- version "0.22.10"
- resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6"
- integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg==
+ version "0.22.11"
+ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.11.tgz#61c0facf9636d14ba5f77fc65ed8913aa845d717"
+ integrity sha512-x0X3kPbholdJZng9wDMhb2swvUi3UYRNAuWAmIPIWlfgAJZp//cql/qblE7181Mg7SjWVwq6ldCPCLn5AY/e7w==
+ dependencies:
+ "@types/node" "*"
"@types/classnames@^2.2.6":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.7.tgz#fb68cc9be8487e6ea5b13700e759bfbab7e0fefd"
integrity sha512-rzOhiQ55WzAiFgXRtitP/ZUT8iVNyllEpylJ5zHzR4vArUvMB39GTk+Zon/uAM0JxEFAWnwsxC2gH8s+tZ3Myg==
+"@types/clipboard@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/clipboard/-/clipboard-2.0.1.tgz#75a74086c293d75b12bc93ff13bc7797fef05a40"
+ integrity sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA==
+
"@types/commander@^2.12.2":
version "2.12.2"
resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae"
@@ -1898,119 +1851,119 @@
commander "*"
"@types/d3-array@*":
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.5.tgz#7f87eccfe53396d48700294a518c379be14c8254"
- integrity sha512-kELkPCl/pCcelr5cXDoQyy3WOkLn8dVdYA+qmtQcuxX9gLoD4s12/CJf6Yxx4UvvuMKJHA8kUbdcH/3DY8SzNg==
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.6.tgz#bbc5d7e6e837353f5849c0ffad9bf6fbf9c775bb"
+ integrity sha512-/EcY/15X5tnwkMT2txpjiLUNJj5xHA2vGHOXI8NTYGhETK914RRLQLjNm6EpAI1D2IY5vh3CzuLODnqBAwKjPA==
"@types/d3-axis@*":
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.11.tgz#efd975f9fec14c2afd03828f3acec0ef97d37c3b"
- integrity sha512-cuigApCyCwYJxaQPghj+BqaxzbdRdT/lpZBMtF7EuEIJ61NMQ8yvGnqFvHCIgJEmUu2Wb2wiZqy9kiHi3Ddftg==
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.12.tgz#8c124edfcc02f3b3a9cdaa2a28b8a20341401799"
+ integrity sha512-BZISgSD5M8TgURyNtcPAmUB9sk490CO1Thb6/gIn0WZTt3Y50IssX+2Z0vTccoqZksUDTep0b+o4ofXslvNbqg==
dependencies:
"@types/d3-selection" "*"
"@types/d3-brush@*":
- version "1.0.9"
- resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.0.9.tgz#c71070845946eeee4cf330e04123a3997e6476bf"
- integrity sha512-mAx8IVc0luUHfk51pl0UN1vzybnAzLMUsvIwLt3fbsqqPkSXr+Pu1AxOPPeyNc27LhHJnfH/LCV7Jlv+Yzqu1A==
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.0.10.tgz#aa9b5545d816c29d19cff20118f236713af8e9fb"
+ integrity sha512-J8jREATIrfJaAfhJivqaEKPnJsRlwwrOPje+ABqZFgamADjll+q9zaDXnYyjiGPPsiJEU+Qq9jQi5rECxIOfhg==
dependencies:
"@types/d3-selection" "*"
"@types/d3-chord@*":
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.8.tgz#08c0fbb10281be0a5b3fdf48c9c081af02f79fb6"
- integrity sha512-F0ftYOo7FenAIxsRjXLt8vbij0NLDuVcL+xaGY7R9jUmF2Mrpj1T5XukBI9Cad+Ei7YSxEWREIO+CYcaKCl2qQ==
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.9.tgz#ccc5de03ff079025491b7aa6b750670a140b45ae"
+ integrity sha512-UA6lI9CVW5cT5Ku/RV4hxoFn4mKySHm7HEgodtfRthAj1lt9rKZEPon58vyYfk+HIAm33DtJJgZwMXy2QgyPXw==
"@types/d3-collection@*":
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.7.tgz#829e1db477d6bbbcdc038cbc489f22798752d707"
- integrity sha512-vR3BT0GwHc5y93Jv6bxn3zoxP/vGu+GdXu/r1ApjbP9dLk9I2g6NiV7iP/QMQSuFZd0It0n/qWrfXHxCWwHIkg==
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.8.tgz#aa9552c570a96e33c132e0fd20e331f64baa9dd5"
+ integrity sha512-y5lGlazdc0HNO0F3UUX2DPE7OmYvd9Kcym4hXwrJcNUkDaypR5pX+apuMikl9LfTxKItJsY9KYvzBulpCKyvuQ==
"@types/d3-color@*":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.1.tgz#26141c3c554e320edd40726b793570a3ae57397e"
- integrity sha512-xwb1tqvYNWllbHuhMFhiXk63Imf+QNq/dJdmbXmr2wQVnwGenCuj3/0IWJ9hdIFQIqzvhT7T37cvx93jtAsDbQ==
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf"
+ integrity sha512-6pBxzJ8ZP3dYEQ4YjQ+NVbQaOflfgXq/JbDiS99oLobM2o72uAST4q6yPxHv6FOTCRC/n35ktuo8pvw/S4M7sw==
"@types/d3-dispatch@*":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.6.tgz#19b173f669cd2ab7dd3d862e8037aae1a98c7508"
- integrity sha512-xyWJQMr832vqhu6fD/YqX+MSFBWnkxasNhcStvlhqygXxj0cKqPft0wuGoH5TIq5ADXgP83qeNVa4R7bEYN3uA==
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.7.tgz#6721aefbb9862ce78c20a87a1490c21f57c3ed7f"
+ integrity sha512-M+z84G7UKwK6hEPnGCSccOg8zJ3Nk2hgDQ9sCstHXgsFU0sMxlIZVKqKB5oxUDbALqQG6ucg0G9e8cmOSlishg==
"@types/d3-drag@*":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.2.tgz#1cdd7716212a8cdef0a24c782c9d86c6aeb4a451"
- integrity sha512-+UKFeaMVTfSQvMO0PTzOyLXSr7OZbF2Rx1iNVwo2XsyiOsd4MSuLyJKUwRmGn67044QpbNzr+VD6/8iBBLExWw==
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.3.tgz#d8ddccca28e939e9c689bea6f40a937e48c39051"
+ integrity sha512-rWB5SPvkYVxW3sqUxHOJUZwifD0KqvKwvt1bhNqcLpW6Azsd0BJgRNcyVW8GAferaAk5r8dzeZnf9zKlg9+xMQ==
dependencies:
"@types/d3-selection" "*"
"@types/d3-dsv@*":
- version "1.0.35"
- resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.35.tgz#eeb884bfbaa6775daa41ebd2dc8c07a24505f311"
- integrity sha512-QeH7cN9phcm68TDwpSGmzE71/JtGoKZ2rZJABNUMQ7nYIhHkm2UldqI1Cp2pjEo8ycSeutudjzq+Lfim/ZCadQ==
+ version "1.0.36"
+ resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.36.tgz#e91129d7c02b1b814838d001e921e8b9a67153d0"
+ integrity sha512-jbIWQ27QJcBNMZbQv0NSQMHnBDCmxghAxePxgyiPH1XPCRkOsTBei7jcdi3fDrUCGpCV3lKrSZFSlOkhUQVClA==
"@types/d3-ease@*":
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.7.tgz#93a301868be9e15061f3d44343b1ab3f8acb6f09"
- integrity sha1-k6MBhovp4VBh89RDQ7GrP4rLbwk=
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.8.tgz#b440761fb85985d76259ec9c5bf01c4c56778ac2"
+ integrity sha512-VRf8czVWHSJPoUWxMunzpePK02//wHDAswknU8QWzcyrQn6pqe46bHRYi2smSpw5VjsT2CG8k/QeWIdWPS3Bmg==
"@types/d3-force@*":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.0.tgz#430d572eb3922bc463726dd06580829ef36b6434"
- integrity sha512-rfNJogFDPEO16RBqA4anZtiscYeMxreNg8zUKGnBi/1DnrZ42rG5RvOS/qXqBqLZJ3HY0ouAx8AZqgT3/hHbFA==
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.1.tgz#c28803ea36fe29788db69efa0ad6c2dc09544e83"
+ integrity sha512-jqK+I36uz4kTBjyk39meed5y31Ab+tXYN/x1dn3nZEus9yOHCLc+VrcIYLc/aSQ0Y7tMPRlIhLetulME76EiiA==
"@types/d3-format@*":
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.0.tgz#c5e115fac8e6861ce656fe9861892b22f6b0cfcb"
- integrity sha512-ZiY4j3iJvAdOwzwW24WjlZbUNvqOsnPAMfPBmdXqxj3uKJbrzBlRrdGl5uC89pZpFs9Dc92E81KcwG2uEgkIZA==
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.1.tgz#35bf88264bd6bcda39251165bb827f67879c4384"
+ integrity sha512-KAWvReOKMDreaAwOjdfQMm0HjcUMlQG47GwqdVKgmm20vTd2pucj0a70c3gUSHrnsmo6H2AMrkBsZU2UhJLq8A==
"@types/d3-geo@*":
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.11.0.tgz#f7921c5c50d1a43df928846d8a7e47140455687f"
- integrity sha512-/IbMHRG9cur+6hkWvBrRg3DnnUWtaSW8Bl6nu1OO1J8K25BxRYyLslyjIBbwlK0kV0haztlAR2LCIRuDc/U2LA==
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.11.1.tgz#e96ec91f16221d87507fec66b2cc889f52d2493e"
+ integrity sha512-Ox8WWOG3igDRoep/dNsGbOiSJYdUG3ew/6z0ETvHyAtXZVBjOE0S96zSSmzgl0gqQ3RdZjn2eeJOj9oRcMZPkQ==
dependencies:
"@types/geojson" "*"
"@types/d3-hierarchy@*":
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#b9a941bbfbd8f83a163a1667aae8a52f41a9edab"
- integrity sha512-DKhqURrURt2c7MsF9sHiF2wrWf2+yZR4Q9oIG026t/ZY4VWoM0Yd7UonaR+rygyReWcFSEjKC/+5A27TgD8R8g==
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#4c017521900813ea524c9ecb8d7985ec26a9ad9a"
+ integrity sha512-vvSaIDf/Ov0o3KwMT+1M8+WbnnlRiGjlGD5uvk83a1mPCTd/E5x12bUJ/oP55+wUY/4Kb5kc67rVpVGJ2KUHxg==
"@types/d3-interpolate@*":
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.0.tgz#65b9627900bfdd82474875d9b23d574a4388af7c"
- integrity sha512-Ng4ds7kPSvP/c3W3J5PPUQlgewif1tGBqCeh5lgY+UG82Y7H9zQ8c2gILsEFDLg7wRGOwnuKZ940Q/LSN14w9w==
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.1.tgz#1c280511f622de9b0b47d463fa55f9a4fd6f5fc8"
+ integrity sha512-z8Zmi08XVwe8e62vP6wcA+CNuRhpuUU5XPEfqpG0hRypDE5BWNthQHB1UNWWDB7ojCbGaN4qBdsWp5kWxhT1IQ==
dependencies:
"@types/d3-color" "*"
"@types/d3-path@*":
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.7.tgz#a0736fceed688a695f48265a82ff7a3369414b81"
- integrity sha512-U8dFRG+8WhkLJr2sxZ9Cw/5WeRgBnNqMxGdA1+Z0+ZG6tK0s75OQ4OXnxeyfKuh6E4wQPY8OAKr1+iNDx01BEQ==
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79"
+ integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA==
"@types/d3-polygon@*":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.6.tgz#db25c630a2afb9191fe51ba61dd37baee9dd44c7"
- integrity sha512-E6Kyodn9JThgLq20nxSbEce9ow5/ePgm9PX2EO6W1INIL4DayM7cFaiG10DStuamjYAd0X4rntW2q+GRjiIktw==
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.7.tgz#7b3947aa2d48287ff535230d3d396668ab17bfdf"
+ integrity sha512-Xuw0eSjQQKs8jTiNbntWH0S+Xp+JyhqxmQ0YAQ3rDu6c3kKMFfgsaGN7Jv5u3zG6yVX/AsLP/Xs/QRjmi9g43Q==
"@types/d3-quadtree@*":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.6.tgz#45da9e603688ba90eedd3d40f6e504764e06e493"
- integrity sha512-sphVuDdiSIaxLt9kQgebJW98pTktQ/xuN7Ysd8X68Rnjeg/q8+c36/ShlqU52qoKg9nob/JEHH1uQMdxURZidQ==
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.7.tgz#8e29464ff5b326f6612c1428d9362b4b35de2b70"
+ integrity sha512-0ajFawWicfjsaCLh6NzxOyVDYhQAmMFbsiI3MPGLInorauHFEh9/Cl6UHNf+kt/J1jfoxKY/ZJaKAoDpbvde5Q==
"@types/d3-queue@*":
- version "3.0.7"
- resolved "https://registry.yarnpkg.com/@types/d3-queue/-/d3-queue-3.0.7.tgz#94dc7af693281ab78ccdf381a8c1f71ef16659c1"
- integrity sha512-nBbDO69wu1TUWqtGYAePw40jSPcQSt5VwAf7403vYopVCs3Rtbt5f47j2wbuMY4Z2x543VbTIlDo5gwdpV5O+Q==
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-queue/-/d3-queue-3.0.8.tgz#fad6212f14f34a549fc67144e354f032fb25a447"
+ integrity sha512-1FWOiI/MYwS5Z1Sa9EvS1Xet3isiVIIX5ozD6iGnwHonGcqL+RcC1eThXN5VfDmAiYt9Me9EWNEv/9J9k9RIKQ==
"@types/d3-random@*":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.1.tgz#38647ce2ff4ce7d0d56974334c1c4092513c8b9f"
- integrity sha512-jUPeBq1XKK9/5XasTvy5QAUwFeMsjma2yt/nP02yC2Tijovx7i/W5776U/HZugxc5SSmtpx4Z3g9KFVon0QrjQ==
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.2.tgz#6f77e8b7bb64ac393f92d33fe8f71038bc4f3cde"
+ integrity sha512-Jui+Zn28pQw/3EayPKaN4c/PqTvqNbIPjHkgIIFnxne1FdwNjfHtAIsZIBMKlquQNrrMjFzCrlF2gPs3xckqaA==
"@types/d3-request@*":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@types/d3-request/-/d3-request-1.0.4.tgz#b7bde4c63794ee6a47dc3d71d64ca91b9ac87349"
- integrity sha512-6ZVaWdXNjXEp3A+PB/vMTIZDfmEiSay3oDyy7HpsTmnSAWSsqfXYTE9RxMmZs8MY0QMFbtous0LiUSrv5uOGXA==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@types/d3-request/-/d3-request-1.0.5.tgz#a2717ab95cd1e504662f52802aff1476af38cce4"
+ integrity sha512-X+/c/qXp92o056C5Qbcp7jL27YRHpmIqOchHb/WB7NwFFqkBtAircqO7oKWv2GTtX4LyEqiDF9gqXsV+ldOlIg==
dependencies:
"@types/d3-dsv" "*"
@@ -2022,48 +1975,48 @@
"@types/d3-time" "*"
"@types/d3-selection@*":
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.4.tgz#2f9b71e25fb73bc95c3842dd52b7e6d523292896"
- integrity sha512-WQ6Ivy7VuUlZ/Grqc8493ZxC+y/fpvZLy5+8ELvmCr2hll8eJPUqC05l6fgRRA7kjqlpbH7lbmvY6pRKf6yzxw==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.1.tgz#fa1f8710a6b5d7cfe5c6caa61d161be7cae4a022"
+ integrity sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA==
"@types/d3-shape@*":
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.0.tgz#ea195c543a1f6d3e250ceb07a7d208d80ee37b01"
- integrity sha512-ERWJ8bNZjkzfWfPAlkN3XCqYOOsWTnqTX0jX2Bx+WLd2AfEl97WXr2igwssFc91MadrZLw7HNS/JTUZPQL5sZQ==
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.1.tgz#1b4f92b7efd7306fe2474dc6ee94c0f0ed2e6ab6"
+ integrity sha512-usqdvUvPJ7AJNwpd2drOzRKs1ELie53p2m2GnPKr076/ADM579jVTJ5dPsoZ5E/CMNWk8lvPWYQSvilpp6jjwg==
dependencies:
"@types/d3-path" "*"
"@types/d3-time-format@*":
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.0.tgz#011e0fb7937be34a9a8f580ae1e2f2f1336a8a22"
- integrity sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.1.tgz#dd2c79ec4575f1355484ab6b10407824668eba42"
+ integrity sha512-tJSyXta8ZyJ52wDDHA96JEsvkbL6jl7wowGmuf45+fAkj5Y+SQOnz0N7/H68OWmPshPsAaWMQh+GAws44IzH3g==
"@types/d3-time@*":
- version "1.0.9"
- resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.9.tgz#c2cf05a3cd51f810b8d8a9bbca0c74030d4e535e"
- integrity sha512-m+D4NbQdDlTVaO7QgXAnatR3IDxQYDMBtRhgSCi5rs9R1LPq1y7/2aqa1FJ2IWjFm1mOV63swDxonnCDlHgHMA==
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.10.tgz#d338c7feac93a98a32aac875d1100f92c7b61f4f"
+ integrity sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw==
"@types/d3-timer@*":
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.8.tgz#a3441d9605367059e14ad8c3494132143cbc8d58"
- integrity sha512-AKUgQ/nljUFcUO2P3gK24weVI5XwUTdJvjoh8gJ0yxT4aJ+d7t2Or3TB+k9dEYl14BAjoj32D0ky+YzQSVszfg==
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.9.tgz#aed1bde0cf18920d33f5d44839d73de393633fd3"
+ integrity sha512-WvfJ3LFxBbWjqRGz9n7GJt08RrTHPJDVsIwwoCMROlqF+iDacYiAFjf9oqnq0mXpb2juA2N/qjKP+MKdal3YNQ==
"@types/d3-transition@*":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.3.tgz#efcf4941dae22135d595514ba488f4f370d396b0"
- integrity sha512-1EukXNuVu/z2G1GZpZagzFJnie9C5zze17ox/vhTgGXNy46rYAm4UkhLLlUeeZ1ndq88k95SOeC8898RpKMLOQ==
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.4.tgz#3c7a35ae9acfc59dfef1eb7308ebabf0fc0680de"
+ integrity sha512-/vsmKVUIXEyCcIXYAlw7bnYkIs9/J/nZbptRJFKUN3FdXq/dF6j9z9xXzerkyU6TDHLrMrwx9eGwdKyTIy/j9w==
dependencies:
"@types/d3-selection" "*"
"@types/d3-voronoi@*":
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.8.tgz#a039cb8368bce4efc1a70aebe744d210851cf1a7"
- integrity sha512-zqNhW7QsYQGlfOdrwPNPG3Wk64zUa4epKRurkJ/dVc6oeXrB+iTDt8sRZ0KZKOOXvvfa1dcdB0e45TZeLBiodQ==
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz#7bbc210818a3a5c5e0bafb051420df206617c9e5"
+ integrity sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==
"@types/d3-zoom@*":
- version "1.7.3"
- resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.3.tgz#ed9421551328157f70edabc401d8c91c38d360d9"
- integrity sha512-Tz7+z4+Id0MxERw/ozinC5QHJmGLARs9Mpi/7VVfiR+9AHcFGe9q+fjQa30/oPNY8WPuCh5p5uuXmBYAJ3y91Q==
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.4.tgz#9226ffd2bd3846ec0e4a4e2bff211612d3aafad5"
+ integrity sha512-5jnFo/itYhJeB2khO/lKe730kW/h2EbKMOvY0uNp3+7NdPm4w63DwPEMxifQZ7n902xGYK5DdU67FmToSoy4VA==
dependencies:
"@types/d3-interpolate" "*"
"@types/d3-selection" "*"
@@ -2105,9 +2058,9 @@
"@types/d3-zoom" "*"
"@types/enzyme@^3.1.13":
- version "3.1.17"
- resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.17.tgz#41f553bfdbaa00880488feabb3ade47d5489db42"
- integrity sha512-pZ+Blk1hODkprPZ9cxXd8njxdBnbLGWOKAmKk0QhpJvWzI4q4F20FHHUnkZXPXJt5WnK6SbbY5lfTKoz1M/CTw==
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.9.0.tgz#a81c91e2dfd2d70e67f013f2c0e5efed6df05489"
+ integrity sha512-o0C7ooyBtj9NKyMzn2BWN53W4J21KPhO+/v+qqQX28Pcz0Z1B3DjL9bq2ZR4TN70PVw8O7gkhuFtC7VN3tausg==
dependencies:
"@types/cheerio" "*"
"@types/react" "*"
@@ -2118,9 +2071,9 @@
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/geojson@*":
- version "7946.0.5"
- resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.5.tgz#9aea839ea5af4b1bc079f1d9fa977d48665e02b0"
- integrity sha512-rLlMXpd3rdlrp0+xsrda/hFfOpIxgqFcRpk005UKbHtcdFK+QXAjhBAPnvO58qF4O1LdDXrcaiJxMgstCIlcaw==
+ version "7946.0.6"
+ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.6.tgz#416f388a06b227784a2d91a88a53f14de05cd54b"
+ integrity sha512-f6qai3iR62QuMPPdgyH+LyiXTL2n9Rf62UniJjV7KHrbiwzLTZUKsdq0mFSTxAHbO7JvwxwC4tH0m1UnweuLrA==
"@types/inquirer@^0.0.43":
version "0.0.43"
@@ -2158,24 +2111,24 @@
integrity sha512-SVtqEcudm7yjkTwoRA1gC6CNMhGDdMx4Pg8BPdiqI7bXXdCn1BPmtxgeWYQOgDxrq53/5YTlhq5ULxBEAlWIBg==
"@types/lodash@^4.14.119":
- version "4.14.119"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
- integrity sha512-Z3TNyBL8Vd/M9D9Ms2S3LmFq2sSMzahodD6rCS9V2N44HUMINb75jNkSuwAx7eo2ufqTdfOdtGQpNbieUjPQmw==
+ version "4.14.123"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.123.tgz#39be5d211478c8dd3bdae98ee75bb7efe4abfe4d"
+ integrity sha512-pQvPkc4Nltyx7G1Ww45OjVqUsJP4UsZm+GWJpigXgkikZqJgRm4c48g027o6tdgubWHwFRF15iFd+Y4Pmqv6+Q==
-"@types/node@*":
- version "11.9.0"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.0.tgz#35fea17653490dab82e1d5e69731abfdbf13160d"
- integrity sha512-ry4DOrC+xenhQbzk1iIPzCZGhhPGEFv7ia7Iu6XXSLVluiJIe9FfG7Iu3mObH9mpxEXCWLCMU4JWbCCR9Oy1Zg==
+"@types/node@*", "@types/node@^11.9.5":
+ version "11.11.3"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58"
+ integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==
"@types/node@^10.12.18":
- version "10.12.25"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.25.tgz#0d01a7dd6127de60d861ece4a650963042abb538"
- integrity sha512-IcvnGLGSQFDvC07Bz2I8SX+QKErDZbUdiQq7S2u3XyzTyJfUmT0sWJMbeQkMzpTAkO7/N7sZpW/arUM2jfKsbQ==
+ version "10.14.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.1.tgz#8701cd760acc20beba5ffe0b7a1b879f39cb8c41"
+ integrity sha512-Rymt08vh1GaW4vYB6QP61/5m/CFLGnFZP++bJpWbiNxceNa6RBipDmb413jvtSf/R1gg5a/jQVl2jY4XVRscEA==
"@types/node@^8.0.31":
- version "8.10.40"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.40.tgz#4314888d5cd537945d73e9ce165c04cc550144a4"
- integrity sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ==
+ version "8.10.44"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.44.tgz#b00cf3595c6a3d75740af9768739a8125053a5a9"
+ integrity sha512-HY3SK7egERHGUfY8p6ztXIEQWcIPHouYhCGcLAPQin7gE2G/fALFz+epnMwcxKUS6aKqTVoAFdi+t1llQd3xcw==
"@types/papaparse@^4.5.9":
version "4.5.9"
@@ -2185,19 +2138,19 @@
"@types/node" "*"
"@types/prop-types@*":
- version "15.5.8"
- resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce"
- integrity sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==
+ version "15.7.0"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.0.tgz#4c48fed958d6dcf9487195a0ef6456d5f6e0163a"
+ integrity sha512-eItQyV43bj4rR3JPV0Skpl1SncRCdziTEK9/v8VwXmV6d/qOUO8/EuWeHBbCZcsfSHfzI5UyMJLCSXtxxznyZg==
"@types/q@^1.5.1":
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18"
- integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
+ integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@types/react-color@^2.14.0":
- version "2.14.0"
- resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-2.14.0.tgz#e01f6069902819fe9f6544375829a485f894206f"
- integrity sha512-5UfGjUsu7bXop7K064nNrIGgS7wPjGEY7Or9tAE6BLslDtfhRnAlqWo9N2BIntEZ/3KpXlRnt0MBuc+hZJTevw==
+ version "2.17.0"
+ resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-2.17.0.tgz#7f3c958bb43ebeedc7e04309576a235d5233ce9d"
+ integrity sha512-NQCLW437DXzaV/XvtoH3cBW75f0KQ9ZtFvvXnn7QEudLTR5zGxLdsEhPffrateSizsG2CTml4X+2/2TyEisotQ==
dependencies:
"@types/react" "*"
@@ -2208,10 +2161,10 @@
dependencies:
"@types/react" "*"
-"@types/react-dom@*", "@types/react-dom@^16.0.9":
- version "16.8.0"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.0.tgz#c565f43f9d2ec911f9e0b8f3b74e25e67879aa3f"
- integrity sha512-Jp4ufcEEjVJEB0OHq2MCZcE1u3KYUKO6WnSuiU/tZeYeiZxUoQavfa/TZeiIT+1XoN6l0lQVNM30VINZFDeolQ==
+"@types/react-dom@*", "@types/react-dom@^16.8.2":
+ version "16.8.2"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.2.tgz#9bd7d33f908b243ff0692846ef36c81d4941ad12"
+ integrity sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw==
dependencies:
"@types/react" "*"
@@ -2223,25 +2176,25 @@
"@types/react" "*"
"@types/react-select@^2.0.4":
- version "2.0.11"
- resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.11.tgz#9b2b1fdb12b67a5a617c5f572e15617636cc65af"
- integrity sha512-kITn4R50eUJCi2YT3JFZS4z5M2SJJqqYiVUX1HyLSFWbHbF6J25ZPKCCXANQrsnQzSrac2XiNpR5oYBif6l93g==
+ version "2.0.15"
+ resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.15.tgz#51d607667f59a12e980abcc5bbf9636307293e44"
+ integrity sha512-lbtGCfZ82lKAU0KPoO6M81ZqoT3cOOLWTNkwgmPlBekNBt95ccWItAIKiGZnoO7+gzk413biIxetRSM2CoLL8w==
dependencies:
"@types/react" "*"
"@types/react-dom" "*"
"@types/react-transition-group" "*"
"@types/react-test-renderer@^16.0.3":
- version "16.8.0"
- resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.0.tgz#dbed6549f97a7f114b6920bf553a5db7e61bf83c"
- integrity sha512-m563EQSTVB2g6h+FDUH2cgfiRdjL1KHVyi643EQQSFIblMPrWwJh/adqTcMS/FhJHvhEboR4pmhrhEXyHDDsmQ==
+ version "16.8.1"
+ resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.1.tgz#96f3ce45a3a41c94eca532a99103dd3042c9d055"
+ integrity sha512-8gU69ELfJGxzVWVYj4MTtuHxz9nO+d175XeQ1XrXXxesUBsB4KK6OCfzVhEX6leZWWBDVtMJXp/rUjhClzL7gw==
dependencies:
"@types/react" "*"
"@types/react-transition-group@*", "@types/react-transition-group@^2.0.15":
- version "2.0.15"
- resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.15.tgz#e5ee3fe558832e141cc6041bdd54caea7b787af8"
- integrity sha512-S0QnNzbHoWXDbKBl/xk5dxA4FT+BNlBcI3hku991cl8Cz3ytOkUMcCRtzdX11eb86E131bSsQqy5WrPCdJYblw==
+ version "2.0.16"
+ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.16.tgz#2dcb9e396ab385ee19c4af1c9caa469a14cd042f"
+ integrity sha512-FUJEx2BGJPU1qVQoWd9v7wpOwnCPTWhcE4iTaU5prry9SvwiI11lCXOci8Nz9cM/Fuf650l7Skg6nlVeCYjPFA==
dependencies:
"@types/react" "*"
@@ -2253,10 +2206,10 @@
"@types/prop-types" "*"
"@types/react" "*"
-"@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==
+"@types/react@*", "@types/react@16.8.8", "@types/react@^16.8.8":
+ version "16.8.8"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.8.tgz#4b60a469fd2469f7aa6eaa0f8cfbc51f6d76e662"
+ integrity sha512-xwEvyet96u7WnB96kqY0yY7qxx/pEpU51QeACkKFtrgjjXITQn0oO1iwPEraXVgh10ZFPix7gs1R4OJXF7P5sg==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
@@ -2375,30 +2328,30 @@
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/storybook__addon-actions@^3.4.1":
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.1.tgz#8f90d76b023b58ee794170f2fe774a3fddda2c1d"
- integrity sha512-An8pNb1/7QhkdOT8Ht5WjJsSxAh2mWti/J4eILwUHpXVZ1j3xlVaOzwTbg8twN4DjgOAggjEDOj6Bx8YOWh9Pg==
+ version "3.4.2"
+ resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.2.tgz#1d08689cc3259269ddb3479a2307c9d16944309e"
+ integrity sha512-CWxGz2pXav9PHcwrtXmkuH+xJL7sAu2AmIGEbkdT3Xs5jzBPZUEDEN//ZF7o6IOPP/tdXU37K1hrVMt9TDO0Bw==
"@types/storybook__addon-info@^3.4.2":
- version "3.4.3"
- resolved "https://registry.yarnpkg.com/@types/storybook__addon-info/-/storybook__addon-info-3.4.3.tgz#c952150737830d665b8c95a9b652b18226dd8afa"
- integrity sha512-j9lhGbdSV6ydZGJ24CShlMEnvYTiGYBM+sfT88XnLm3j/mr/VkBzU9fhGlDPdWk7/rDgqkrztDk/0E3AsxBNRg==
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/@types/storybook__addon-info/-/storybook__addon-info-3.4.4.tgz#6ded3159f6e279746790566544333f83d06a2703"
+ integrity sha512-x3AIgYfaojkjxq9WxtQE/ZdQzptisiSzL4J5cwKvO8vIMyHPdzD4lMAOEs+ucP43TxmZldrdutqzzq20jbbZKQ==
dependencies:
"@types/react" "*"
"@types/storybook__react" "*"
"@types/storybook__addon-knobs@^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@types/storybook__addon-knobs/-/storybook__addon-knobs-4.0.0.tgz#b218f0d84888833cc8b8d7a7b524175e8bb3030f"
- integrity sha512-x3GNz8f0fQv7USvDuVXdZ4p/7nofFHyH6iB/qwR84Yp97xZxOzlQ0SY+6K14tVbdi9P7Qm5DZ2kZr0a+Io8qEQ==
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/@types/storybook__addon-knobs/-/storybook__addon-knobs-4.0.4.tgz#66bd835a086a8d881013a09386da65270713c3a0"
+ integrity sha512-dN7sRS7pjLHVRY+Cnk7G94kfc7LZQAzEGnqOY0XG9ZdKse25tAKlAyidI7rjxwJ54v1CWswP8p+X4lBznV3cRw==
dependencies:
"@types/react" "*"
"@types/storybook__react" "*"
"@types/storybook__react@*", "@types/storybook__react@^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@types/storybook__react/-/storybook__react-4.0.0.tgz#52cc452fbab599568d595075a90142ef4a1233f6"
- integrity sha512-Iq3RX953fqZRwWN3jywm8pUx1/Atev+x/9tF7/2CNA+Ii55sGSJJRWMRthUKQXTa3zOexcvfksfVYdUaIZY91w==
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@types/storybook__react/-/storybook__react-4.0.1.tgz#b6320c9d027b8ee7ef1445fef8b4cba196d48ace"
+ integrity sha512-knkZErqv8Iy2QbebqBa5tsy2itIMKdO6bcQ7C19nmgTc+j1pnQhXCGcVyARzAQ1/NAuSYudSWQAKG+plgK7hyQ==
dependencies:
"@types/react" "*"
"@types/webpack-env" "*"
@@ -2428,9 +2381,9 @@
integrity sha512-25L/RL5tqZkquKXVHM1fM2bd23qjfbcPpAZ2N/H05Y45g3UEi+Hw8CbDV28shKY8gH1SHiLpZSxPI1lacqdpGg==
"@types/unist@*", "@types/unist@^2.0.0":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.2.tgz#5dc0a7f76809b7518c0df58689cd16a19bd751c6"
- integrity sha512-iHI60IbyfQilNubmxsq4zqSjdynlmc2Q/QvH9kjzg9+CCYVVzq1O6tc7VBzSygIwnmOt07w80IG6HDQvjv3Liw==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
+ integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
"@types/vfile-message@*":
version "1.0.1"
@@ -2450,23 +2403,20 @@
"@types/vfile-message" "*"
"@types/webpack-env@*":
- version "1.13.7"
- resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.7.tgz#137a4e57aa31ab57b1baf66f5dc3b6bf085e9944"
- integrity sha512-rzi6fw7hhxPcCoNVsgysHFlKnhYYvVj7AJwdAO0HQNP5vg9sY0DoRRC1pfuCQm94cOa1sab82HGUtdFlWHIhBg==
+ version "1.13.9"
+ resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.9.tgz#a67287861c928ebf4159a908d1fb1a2a34d4097a"
+ integrity sha512-p8zp5xqkly3g4cCmo2mKOHI9+Z/kObmDj0BmjbDDJQlgDTiEGTbm17MEwTAusV6XceCy+bNw9q/ZHXHyKo3zkg==
"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
version "12.0.9"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
-"@webassemblyjs/ast@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
- integrity sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==
dependencies:
- "@webassemblyjs/helper-module-context" "1.7.11"
- "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
- "@webassemblyjs/wast-parser" "1.7.11"
+ "@webassemblyjs/helper-module-context" "1.7.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.6"
+ "@webassemblyjs/wast-parser" "1.7.6"
+ mamacro "^0.0.3"
"@webassemblyjs/ast@1.8.5":
version "1.8.5"
@@ -2477,42 +2427,26 @@
"@webassemblyjs/helper-wasm-bytecode" "1.8.5"
"@webassemblyjs/wast-parser" "1.8.5"
-"@webassemblyjs/floating-point-hex-parser@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz#a69f0af6502eb9a3c045555b1a6129d3d3f2e313"
- integrity sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==
"@webassemblyjs/floating-point-hex-parser@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721"
integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==
-"@webassemblyjs/helper-api-error@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz#c7b6bb8105f84039511a2b39ce494f193818a32a"
- integrity sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==
"@webassemblyjs/helper-api-error@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7"
integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==
-"@webassemblyjs/helper-buffer@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz#3122d48dcc6c9456ed982debe16c8f37101df39b"
- integrity sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==
"@webassemblyjs/helper-buffer@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204"
integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==
-"@webassemblyjs/helper-code-frame@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz#cf8f106e746662a0da29bdef635fcd3d1248364b"
- integrity sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==
dependencies:
- "@webassemblyjs/wast-printer" "1.7.11"
+ "@webassemblyjs/wast-printer" "1.7.6"
"@webassemblyjs/helper-code-frame@1.8.5":
version "1.8.5"
@@ -2521,20 +2455,14 @@
dependencies:
"@webassemblyjs/wast-printer" "1.8.5"
-"@webassemblyjs/helper-fsm@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz#df38882a624080d03f7503f93e3f17ac5ac01181"
- integrity sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==
"@webassemblyjs/helper-fsm@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452"
integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==
-"@webassemblyjs/helper-module-context@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz#d874d722e51e62ac202476935d649c802fa0e209"
- integrity sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==
+ dependencies:
+ mamacro "^0.0.3"
"@webassemblyjs/helper-module-context@1.8.5":
version "1.8.5"
@@ -2544,25 +2472,17 @@
"@webassemblyjs/ast" "1.8.5"
mamacro "^0.0.3"
-"@webassemblyjs/helper-wasm-bytecode@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz#dd9a1e817f1c2eb105b4cf1013093cb9f3c9cb06"
- integrity sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==
"@webassemblyjs/helper-wasm-bytecode@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61"
integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==
-"@webassemblyjs/helper-wasm-section@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz#9c9ac41ecf9fbcfffc96f6d2675e2de33811e68a"
- integrity sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/helper-buffer" "1.7.11"
- "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
- "@webassemblyjs/wasm-gen" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/helper-buffer" "1.7.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.6"
+ "@webassemblyjs/wasm-gen" "1.7.6"
"@webassemblyjs/helper-wasm-section@1.8.5":
version "1.8.5"
@@ -2574,10 +2494,6 @@
"@webassemblyjs/helper-wasm-bytecode" "1.8.5"
"@webassemblyjs/wasm-gen" "1.8.5"
-"@webassemblyjs/ieee754@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz#c95839eb63757a31880aaec7b6512d4191ac640b"
- integrity sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==
dependencies:
"@xtuc/ieee754" "^1.2.0"
@@ -2588,10 +2504,6 @@
dependencies:
"@xtuc/ieee754" "^1.2.0"
-"@webassemblyjs/leb128@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63"
- integrity sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==
dependencies:
"@xtuc/long" "4.2.1"
@@ -2602,29 +2514,21 @@
dependencies:
"@xtuc/long" "4.2.2"
-"@webassemblyjs/utf8@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.11.tgz#06d7218ea9fdc94a6793aa92208160db3d26ee82"
- integrity sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==
"@webassemblyjs/utf8@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc"
integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==
-"@webassemblyjs/wasm-edit@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz#8c74ca474d4f951d01dbae9bd70814ee22a82005"
- integrity sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/helper-buffer" "1.7.11"
- "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
- "@webassemblyjs/helper-wasm-section" "1.7.11"
- "@webassemblyjs/wasm-gen" "1.7.11"
- "@webassemblyjs/wasm-opt" "1.7.11"
- "@webassemblyjs/wasm-parser" "1.7.11"
- "@webassemblyjs/wast-printer" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/helper-buffer" "1.7.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.6"
+ "@webassemblyjs/helper-wasm-section" "1.7.6"
+ "@webassemblyjs/wasm-gen" "1.7.6"
+ "@webassemblyjs/wasm-opt" "1.7.6"
+ "@webassemblyjs/wasm-parser" "1.7.6"
+ "@webassemblyjs/wast-printer" "1.7.6"
"@webassemblyjs/wasm-edit@1.8.5":
version "1.8.5"
@@ -2640,16 +2544,12 @@
"@webassemblyjs/wasm-parser" "1.8.5"
"@webassemblyjs/wast-printer" "1.8.5"
-"@webassemblyjs/wasm-gen@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz#9bbba942f22375686a6fb759afcd7ac9c45da1a8"
- integrity sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
- "@webassemblyjs/ieee754" "1.7.11"
- "@webassemblyjs/leb128" "1.7.11"
- "@webassemblyjs/utf8" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.6"
+ "@webassemblyjs/ieee754" "1.7.6"
+ "@webassemblyjs/leb128" "1.7.6"
+ "@webassemblyjs/utf8" "1.7.6"
"@webassemblyjs/wasm-gen@1.8.5":
version "1.8.5"
@@ -2662,15 +2562,11 @@
"@webassemblyjs/leb128" "1.8.5"
"@webassemblyjs/utf8" "1.8.5"
-"@webassemblyjs/wasm-opt@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7"
- integrity sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/helper-buffer" "1.7.11"
- "@webassemblyjs/wasm-gen" "1.7.11"
- "@webassemblyjs/wasm-parser" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/helper-buffer" "1.7.6"
+ "@webassemblyjs/wasm-gen" "1.7.6"
+ "@webassemblyjs/wasm-parser" "1.7.6"
"@webassemblyjs/wasm-opt@1.8.5":
version "1.8.5"
@@ -2682,17 +2578,13 @@
"@webassemblyjs/wasm-gen" "1.8.5"
"@webassemblyjs/wasm-parser" "1.8.5"
-"@webassemblyjs/wasm-parser@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz#6e3d20fa6a3519f6b084ef9391ad58211efb0a1a"
- integrity sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/helper-api-error" "1.7.11"
- "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
- "@webassemblyjs/ieee754" "1.7.11"
- "@webassemblyjs/leb128" "1.7.11"
- "@webassemblyjs/utf8" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/helper-api-error" "1.7.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.6"
+ "@webassemblyjs/ieee754" "1.7.6"
+ "@webassemblyjs/leb128" "1.7.6"
+ "@webassemblyjs/utf8" "1.7.6"
"@webassemblyjs/wasm-parser@1.8.5":
version "1.8.5"
@@ -2706,17 +2598,14 @@
"@webassemblyjs/leb128" "1.8.5"
"@webassemblyjs/utf8" "1.8.5"
-"@webassemblyjs/wast-parser@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz#25bd117562ca8c002720ff8116ef9072d9ca869c"
- integrity sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/floating-point-hex-parser" "1.7.11"
- "@webassemblyjs/helper-api-error" "1.7.11"
- "@webassemblyjs/helper-code-frame" "1.7.11"
- "@webassemblyjs/helper-fsm" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/floating-point-hex-parser" "1.7.6"
+ "@webassemblyjs/helper-api-error" "1.7.6"
+ "@webassemblyjs/helper-code-frame" "1.7.6"
+ "@webassemblyjs/helper-fsm" "1.7.6"
"@xtuc/long" "4.2.1"
+ mamacro "^0.0.3"
"@webassemblyjs/wast-parser@1.8.5":
version "1.8.5"
@@ -2730,13 +2619,9 @@
"@webassemblyjs/helper-fsm" "1.8.5"
"@xtuc/long" "4.2.2"
-"@webassemblyjs/wast-printer@1.7.11":
- version "1.7.11"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz#c4245b6de242cb50a2cc950174fdbf65c78d7813"
- integrity sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==
dependencies:
- "@webassemblyjs/ast" "1.7.11"
- "@webassemblyjs/wast-parser" "1.7.11"
+ "@webassemblyjs/ast" "1.7.6"
+ "@webassemblyjs/wast-parser" "1.7.6"
"@xtuc/long" "4.2.1"
"@webassemblyjs/wast-printer@1.8.5":
@@ -2841,10 +2726,7 @@ acorn@^5.0.0, acorn@^5.5.0, acorn@^5.5.3:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-acorn@^6.0.1, acorn@^6.0.5:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818"
- integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==
+acorn@^6.0.1, acorn@^6.0.5, acorn@^6.1.1:
acorn@^6.0.7:
version "6.1.1"
@@ -2873,7 +2755,7 @@ address@1.0.3, address@^1.0.1:
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==
-agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
+agent-base@4, agent-base@^4.1.0, agent-base@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
@@ -2942,9 +2824,6 @@ ajv@^6.1.0:
uri-js "^4.2.2"
ajv@^6.1.1, ajv@^6.5.5:
- version "6.9.1"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1"
- integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
@@ -3022,9 +2901,9 @@ ansi-align@^3.0.0:
string-width "^3.0.0"
ansi-colors@^3.0.0:
- version "3.2.3"
- resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813"
- integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
+ integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
ansi-escapes@^1.1.0:
version "1.4.0"
@@ -3051,10 +2930,10 @@ ansi-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
-ansi-regex@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
- integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==
+ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+ integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-styles@^2.2.1:
version "2.2.1"
@@ -3357,9 +3236,9 @@ astral-regex@^1.0.0:
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
async-each@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
- integrity sha1-GdOGodntxufByF04iu28xW0zYC0=
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.2.tgz#8b8a7ca2a658f927e9f307d6d1a42f4199f0f735"
+ integrity sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg==
async-foreach@^0.1.3:
version "0.1.3"
@@ -3376,14 +3255,7 @@ async@^1.5.2, async@~1.5.2:
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
-async@^2.0.0, async@^2.1.4, async@^2.5.0:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
- integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
- dependencies:
- lodash "^4.17.10"
-
-async@^2.6.1:
+async@^2.0.0, async@^2.1.4, async@^2.6.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381"
integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==
@@ -3411,12 +3283,12 @@ autolinker@~0.15.0:
integrity sha1-NCQX2PLzRhsUzwkIjV7fh5HcmDI=
autoprefixer@^9.3.1:
- version "9.4.7"
- resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.7.tgz#f997994f9a810eae47b38fa6d8a119772051c4ff"
- integrity sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA==
+ version "9.5.0"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.5.0.tgz#7e51d0355c11596e6cf9a0afc9a44e86d1596c70"
+ integrity sha512-hMKcyHsZn5+qL6AUeP3c8OyuteZ4VaUlg+fWbyl8z7PqsKHF/Bf8/px3K6AT8aMzDkBo8Bc11245MM+itDBOxQ==
dependencies:
- browserslist "^4.4.1"
- caniuse-lite "^1.0.30000932"
+ browserslist "^4.4.2"
+ caniuse-lite "^1.0.30000947"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
postcss "^7.0.14"
@@ -4155,14 +4027,14 @@ browserslist@^4.0.0, browserslist@^4.4.2:
electron-to-chromium "^1.3.113"
node-releases "^1.1.8"
-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==
+browserslist@^4.1.0, browserslist@^4.3.4, browserslist@^4.4.2:
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.1.tgz#2226cada1947b33f4cfcf7b608dcb519b6128106"
+ integrity sha512-/pPw5IAUyqaQXGuD5vS8tcbudyPZ241jk1W5pQBsGDfcjNQt7p8qxZhgMNuygDShte1PibLFexecWUPgmVLfrg==
dependencies:
- caniuse-lite "^1.0.30000929"
- electron-to-chromium "^1.3.103"
- node-releases "^1.1.3"
+ caniuse-lite "^1.0.30000949"
+ electron-to-chromium "^1.3.116"
+ node-releases "^1.1.11"
bs-logger@0.x:
version "0.2.6"
@@ -4391,9 +4263,6 @@ camelcase@^4.0.0, camelcase@^4.1.0:
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
camelcase@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
- integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
camelcase@^5.2.0:
version "5.2.0"
@@ -4415,15 +4284,11 @@ caniuse-db@1.0.30000772:
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
integrity sha1-UarokXaChureSj2DGep21qAbUSs=
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30000940:
- version "1.0.30000945"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000945.tgz#d51e3750416dd05126d5ac94a9c57d1c26c6fd21"
- integrity sha512-PSGwYChNIXJ4FZr9Z9mrVzBCB1TF3yyiRmIDRIdKDHZ6u+1jYH6xeR28XaquxnMwcZVX3f48S9zi7eswO/G1nQ==
+caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000947, caniuse-lite@^1.0.30000949:
+ version "1.0.30000950"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000950.tgz#8c559d66e332b34e919d1086cc6d29c1948856ae"
+ integrity sha512-Cs+4U9T0okW2ftBsCIHuEYXXkki7mjXmjCh4c6PzYShk04qDEr76/iC7KwhLoWoY65wcra1XOsRD+S7BptEb5A==
-caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000932:
- version "1.0.30000936"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz#5d33b118763988bf721b9b8ad436d0400e4a116b"
- integrity sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw==
capture-exit@^1.2.0:
version "1.2.0"
@@ -4545,9 +4410,6 @@ child-process-promise@^2.2.1:
promise-polyfill "^6.0.1"
chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.0:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.1.tgz#adc39ad55a2adf26548bd2afa048f611091f9184"
- integrity sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==
dependencies:
anymatch "^2.0.0"
async-each "^1.0.1"
@@ -4716,16 +4578,7 @@ cli-width@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
-clipboard@^1.7.1:
- version "1.7.1"
- resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
- integrity sha1-Ng1taUbpmnof7zleQrqStem1oWs=
- dependencies:
- good-listener "^1.2.2"
- select "^1.1.2"
- tiny-emitter "^2.0.0"
-
-clipboard@^2.0.0:
+clipboard@^2.0.0, clipboard@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d"
integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==
@@ -4805,7 +4658,7 @@ co@^4.6.0:
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
-coa@^2.0.2, coa@~2.0.1:
+coa@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
@@ -4814,6 +4667,13 @@ coa@^2.0.2, coa@~2.0.1:
chalk "^2.4.1"
q "^1.1.2"
+coa@~1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
+ integrity sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=
+ dependencies:
+ q "^1.1.2"
+
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@@ -4897,7 +4757,7 @@ comma-separated-tokens@^1.0.0:
dependencies:
trim "0.0.1"
-commander@*, commander@2, commander@^2.12.1, commander@^2.14.1, commander@^2.18.0, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0:
+commander@*, commander@2, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0, commander@~2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
@@ -4907,7 +4767,7 @@ commander@2.11.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==
-commander@2.17.x, commander@~2.17.1:
+commander@2.17.x:
version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
@@ -4968,23 +4828,23 @@ compress-commons@^1.2.0:
normalize-path "^2.0.0"
readable-stream "^2.0.0"
-compressible@~2.0.14:
- version "2.0.15"
- resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212"
- integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==
+compressible@~2.0.16:
+ version "2.0.16"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.16.tgz#a49bf9858f3821b64ce1be0296afc7380466a77f"
+ integrity sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA==
dependencies:
- mime-db ">= 1.36.0 < 2"
+ mime-db ">= 1.38.0 < 2"
compression@^1.5.2:
- version "1.7.3"
- resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db"
- integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+ integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
dependencies:
accepts "~1.3.5"
bytes "3.0.0"
- compressible "~2.0.14"
+ compressible "~2.0.16"
debug "2.6.9"
- on-headers "~1.0.1"
+ on-headers "~1.0.2"
safe-buffer "5.1.2"
vary "~1.1.2"
@@ -5130,9 +4990,9 @@ core-js@^1.0.0:
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.0.0, core-js@^2.4.0, core-js@^2.5.7:
- version "2.6.4"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.4.tgz#b8897c062c4d769dd30a0ac5c73976c47f92ea0d"
- integrity sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A==
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
+ integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@@ -5150,6 +5010,7 @@ cosmiconfig@^4.0.0:
require-from-string "^2.0.1"
cosmiconfig@^5.0.0:
+cosmiconfig@^5.0.2, cosmiconfig@^5.0.5, cosmiconfig@^5.0.7:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.1.0.tgz#6c5c35e97f37f985061cdf653f114784231185cf"
integrity sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==
@@ -5160,14 +5021,11 @@ cosmiconfig@^5.0.0:
lodash.get "^4.4.2"
parse-json "^4.0.0"
-cosmiconfig@^5.0.2, cosmiconfig@^5.0.5, cosmiconfig@^5.0.7:
- version "5.0.7"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04"
- integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==
dependencies:
import-fresh "^2.0.0"
is-directory "^0.3.1"
js-yaml "^3.9.0"
+ lodash.get "^4.4.2"
parse-json "^4.0.0"
crc32-stream@^2.0.0:
@@ -5428,9 +5286,9 @@ css-url-regex@^1.1.0:
integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=
css-what@2.1, css-what@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d"
- integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
cssesc@^0.1.0:
version "0.1.0"
@@ -5533,16 +5391,16 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==
cssstyle@^1.0.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb"
- integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.1.tgz#3aceb2759eaf514ac1a21628d723d6043a819495"
+ integrity sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==
dependencies:
cssom "0.3.x"
csstype@^2.2.0, csstype@^2.5.2:
- version "2.6.2"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01"
- integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow==
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.3.tgz#b701e5968245bf9b08d54ac83d00b624e622a9fa"
+ integrity sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg==
currently-unhandled@^0.4.1:
version "0.4.1"
@@ -5951,7 +5809,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.
dependencies:
ms "2.0.0"
-debug@3.1.0, debug@=3.1.0:
+debug@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
@@ -6282,11 +6140,11 @@ doctrine@^2.0.0:
esutils "^2.0.2"
dom-align@^1.7.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.0.tgz#c0e89b5b674c6e836cd248c52c2992135f093654"
- integrity sha512-B85D4ef2Gj5lw0rK0KM2+D5/pH7yqNxg2mB+E8uzFaolpm7RQmsxEfjyEuNiF8UBBkffumYDeKRzTzc3LePP+w==
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.2.tgz#fdcd36bce25ba8d34fe3582efd57ac767df490bd"
+ integrity sha512-17vInOylbB7H4qua7QRsmQT05FFTZemO8BhnOPgF9BPqjAPDyQr/9V8fmJbn05vQ31m2gu3EJSSYN2u94szUZg==
-dom-converter@~0.2:
+dom-converter@^0.2:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
@@ -6310,12 +6168,12 @@ dom-css@^2.0.0:
"@babel/runtime" "^7.1.2"
dom-serializer@0, dom-serializer@~0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
- integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
dependencies:
- domelementtype "~1.1.1"
- entities "~1.1.1"
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
dom-walk@^0.1.0:
version "0.1.1"
@@ -6327,16 +6185,11 @@ domain-browser@^1.1.1:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
-domelementtype@1, domelementtype@^1.3.0:
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
-domelementtype@~1.1.1:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
- integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=
-
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@@ -6344,13 +6197,6 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"
-domhandler@2.1:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594"
- integrity sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=
- dependencies:
- domelementtype "1"
-
domhandler@^2.3.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
@@ -6358,13 +6204,6 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
-domutils@1.1:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
- integrity sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=
- dependencies:
- domelementtype "1"
-
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@@ -6472,10 +6311,10 @@ 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.3.103, electron-to-chromium@^1.3.62:
- version "1.3.113"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9"
- integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.116, electron-to-chromium@^1.3.62:
+ version "1.3.116"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.116.tgz#1dbfee6a592a0c14ade77dbdfe54fef86387d702"
+ integrity sha512-NKwKAXzur5vFCZYBHpdWjTMO8QptNLNP80nItkSIgUOapPAo9Uia+RvkCaZJtO7fhQaVElSvBPWEc2ku6cKsPA==
electron-to-chromium@^1.3.113:
version "1.3.115"
@@ -6575,27 +6414,27 @@ entities@^1.1.1, entities@~1.1.1:
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
enzyme-adapter-react-16@^1.5.0:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.1.tgz#6d49a3a31c3a0fccf527610f31b837e0f307128a"
- integrity sha512-Egzogv1y77DUxdnq/CyHxLHaNxmSSKDDSDNNB/EiAXCZVFXdFibaNy2uUuRQ1n24T2m6KH/1Rw16XDRq+1yVEg==
+ version "1.11.2"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.11.2.tgz#8efeafb27e96873a5492fdef3f423693182eb9d4"
+ integrity sha512-2ruTTCPRb0lPuw/vKTXGVZVBZqh83MNDnakMhzxhpJcIbneEwNy2Cv0KvL97pl57/GOazJHflWNLjwWhex5AAA==
dependencies:
- enzyme-adapter-utils "^1.10.0"
- function.prototype.name "^1.1.0"
+ enzyme-adapter-utils "^1.10.1"
object.assign "^4.1.0"
object.values "^1.1.0"
- prop-types "^15.6.2"
- react-is "^16.7.0"
+ prop-types "^15.7.2"
+ react-is "^16.8.4"
react-test-renderer "^16.0.0-0"
+ semver "^5.6.0"
-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==
+enzyme-adapter-utils@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.1.tgz#58264efa19a7befdbf964fb7981a108a5452ac96"
+ integrity sha512-oasinhhLoBuZsIkTe8mx0HiudtfErUtG0Ooe1FOplu/t4c9rOmyG5gtrBASK6u4whHIRWvv0cbZMElzNTR21SA==
dependencies:
function.prototype.name "^1.1.0"
object.assign "^4.1.0"
object.fromentries "^2.0.0"
- prop-types "^15.6.2"
+ prop-types "^15.7.2"
semver "^5.6.0"
enzyme-to-json@^3.3.4:
@@ -6606,17 +6445,19 @@ enzyme-to-json@^3.3.4:
lodash "^4.17.4"
enzyme@^3.6.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.8.0.tgz#646d2d5d0798cb98fdec39afcee8a53237b47ad5"
- integrity sha512-bfsWo5nHyZm1O1vnIsbwdfhU989jk+squU9NKvB+Puwo5j6/Wg9pN5CO0YJelm98Dao3NPjkDZk+vvgwpMwYxw==
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.9.0.tgz#2b491f06ca966eb56b6510068c7894a7e0be3909"
+ integrity sha512-JqxI2BRFHbmiP7/UFqvsjxTirWoM1HfeaJrmVSZ9a1EADKkZgdPcAuISPMpoUiHlac9J4dYt81MC5BBIrbJGMg==
dependencies:
array.prototype.flat "^1.2.1"
cheerio "^1.0.0-rc.2"
function.prototype.name "^1.1.0"
has "^1.0.3"
+ html-element-map "^1.0.0"
is-boolean-object "^1.0.0"
is-callable "^1.1.4"
is-number-object "^1.0.3"
+ is-regex "^1.0.4"
is-string "^1.0.4"
is-subset "^0.1.1"
lodash.escape "^4.0.1"
@@ -6671,13 +6512,13 @@ 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.47"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.47.tgz#d24232e1380daad5449a817be19bde9729024a11"
- integrity sha512-/1TItLfj+TTfWoeRcDn/0FbGV6SNo4R+On2GGVucPU/j3BWnXE2Co8h8CTo4Tu34gFJtnmwS9xiScKs4EjZhdw==
+ version "0.10.49"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e"
+ integrity sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==
dependencies:
es6-iterator "~2.0.3"
es6-symbol "~3.1.1"
- next-tick "1"
+ next-tick "^1.0.0"
es5-shim@^4.5.10:
version "4.5.12"
@@ -6711,9 +6552,9 @@ es6-promise@^3.0.2:
integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=
es6-promise@^4.0.3:
- version "4.2.5"
- resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
- integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
+ integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
es6-promisify@^5.0.0:
version "5.0.0"
@@ -6734,9 +6575,9 @@ es6-set@~0.1.5:
event-emitter "~0.3.5"
es6-shim@^0.35.3:
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.4.tgz#8d5a4109756383d3f0323421089c423acf8378f1"
- integrity sha512-oJidbXjN/VWXZJs41E9JEqWzcFbjt43JupimIoVX82Thzt5qy1CiYezdhRmWkj3KOuwJ106IG/ZZrcFC6fgIUQ==
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab"
+ integrity sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg==
es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
version "3.1.1"
@@ -6775,9 +6616,9 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
escodegen@^1.9.1:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
- integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510"
+ integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==
dependencies:
esprima "^3.1.3"
estraverse "^4.2.0"
@@ -6797,9 +6638,9 @@ escope@^3.6.0:
estraverse "^4.1.1"
eslint-scope@^4.0.0:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e"
- integrity sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
+ integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
@@ -6895,6 +6736,11 @@ estree-walker@^0.5.2:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39"
integrity sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==
+estree-walker@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae"
+ integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw==
+
esutils@^2.0.0, esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@@ -7374,12 +7220,12 @@ find-cache-dir@^1.0.0:
pkg-dir "^2.0.0"
find-cache-dir@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
- integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
+ integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
dependencies:
commondir "^1.0.1"
- make-dir "^1.0.0"
+ make-dir "^2.0.0"
pkg-dir "^3.0.0"
find-npm-prefix@^1.0.2:
@@ -7446,6 +7292,16 @@ flat-cache@^1.2.1:
rimraf "~2.6.2"
write "^0.2.1"
+flatten@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+ integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
+
+flow-parser@^0.*:
+ version "0.95.1"
+ resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.95.1.tgz#e1045d958b7e05f65e4fd3304f266f1fd0619698"
+ integrity sha512-HKkivrDpJduju6PfvnTdycPp5U8jemBDBwT/PlqiiW4TNOqmlSIbgniMcssl0h7vpoCUrQ/LX3CCIc/QSIDJWA==
+
flush-write-stream@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -7460,9 +7316,6 @@ fn-name@~2.0.1:
integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=
follow-redirects@^1.0.0:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.1.tgz#514973c44b5757368bad8bddfe52f81f015c94cb"
- integrity sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==
dependencies:
debug "=3.1.0"
@@ -7678,9 +7531,9 @@ function.prototype.name@^1.1.0:
is-callable "^1.1.3"
fuse.js@^3.0.1, fuse.js@^3.3.0:
- version "3.4.2"
- resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.2.tgz#d7a638c436ecd7b9c4c0051478c09594eb956212"
- integrity sha512-WVbrm+cAxPtyMqdtL7cYhR7aZJPhtOfjNClPya8GKMVukKDYs7pEnPINeRVX1C9WmWgU8MdYGYbUPAP2AJXdoQ==
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.4.tgz#f98f55fcb3b595cf6a3e629c5ffaf10982103e95"
+ integrity sha512-pyLQo/1oR5Ywf+a/tY8z4JygnIglmRxVUOiyFAbd11o9keUDpUJSMGRWJngcnkURj30kDHPmhoKY8ChJiz3EpQ==
g-status@^2.0.2:
version "2.0.2"
@@ -8194,11 +8047,11 @@ handle-thing@^2.0.0:
integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==
handlebars@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a"
- integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.1.tgz#6e4e41c18ebe7719ae4d38e5aca3d32fa3dd23d3"
+ integrity sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==
dependencies:
- async "^2.5.0"
+ neo-async "^2.6.0"
optimist "^0.6.1"
source-map "^0.6.1"
optionalDependencies:
@@ -8379,12 +8232,7 @@ hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0:
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
integrity sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=
-hoist-non-react-statics@^2.5.0:
- version "2.5.5"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
- integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
-
-hoist-non-react-statics@^3.1.0:
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
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==
@@ -8392,9 +8240,9 @@ hoist-non-react-statics@^3.1.0:
react-is "^16.7.0"
homedir-polyfill@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
- integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw=
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
+ integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
dependencies:
parse-passwd "^1.0.0"
@@ -8438,6 +8286,13 @@ html-comment-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+html-element-map@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.0.0.tgz#19a41940225153ecdfead74f8509154ff1cdc18b"
+ integrity sha512-/SP6aOiM5Ai9zALvCxDubIeez0LvG3qP7R9GcRDnJEP/HBmv0A8A9K0o8+HFudcFt46+i921ANjzKsjPjb7Enw==
+ dependencies:
+ array-filter "^1.0.0"
+
html-encoding-sniffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
@@ -8519,27 +8374,17 @@ html-webpack-plugin@^4.0.0-beta.2:
tapable "^1.1.0"
util.promisify "1.0.0"
-htmlparser2@^3.9.1:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
- integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==
+htmlparser2@^3.3.0, htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
dependencies:
- domelementtype "^1.3.0"
+ domelementtype "^1.3.1"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
- readable-stream "^3.0.6"
-
-htmlparser2@~3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
- integrity sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=
- dependencies:
- domelementtype "1"
- domhandler "2.1"
- domutils "1.1"
- readable-stream "1.0"
+ readable-stream "^3.1.1"
http-cache-semantics@^3.8.1:
version "3.8.1"
@@ -9852,9 +9697,6 @@ jest-watcher@^24.5.0:
string-length "^2.0.0"
jest-worker@^24.0.0:
- version "24.0.0"
- resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.0.0.tgz#3d3483b077bf04f412f47654a27bba7e947f8b6d"
- integrity sha512-s64/OThpfQvoCeHG963MiEZOAAxu8kHsaL/rCMF7lpdzo7vgF0CtPml9hfguOMgykgH/eOm4jFP4ibfHLruytg==
dependencies:
merge-stream "^1.0.1"
supports-color "^6.1.0"
@@ -9902,9 +9744,6 @@ js-tokens@^3.0.2:
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
js-yaml@^3.12.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0:
- version "3.12.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600"
- integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
@@ -10347,11 +10186,10 @@ libnpx@^10.2.0:
yargs "^11.0.0"
lint-staged@^8.1.3:
- version "8.1.3"
- resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.3.tgz#bb069db5466c0fe16710216e633a84f2b362fa60"
- integrity sha512-6TGkikL1B+6mIOuSNq2TV6oP21IhPMnV8q0cf9oYZ296ArTVNcbFh1l1pfVOHHbBIYLlziWNsQ2q45/ffmJ4AA==
+ version "8.1.5"
+ resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.5.tgz#372476fe1a58b8834eb562ed4c99126bd60bdd79"
+ integrity sha512-e5ZavfnSLcBJE1BTzRTqw6ly8OkqVyO3GL2M6teSmTBYQ/2BuueD5GIt2RPsP31u/vjKdexUyDCxSyK75q4BDA==
dependencies:
- "@iamstarkov/listr-update-renderer" "0.4.1"
chalk "^2.3.1"
commander "^2.14.1"
cosmiconfig "^5.0.2"
@@ -10364,7 +10202,8 @@ lint-staged@^8.1.3:
is-glob "^4.0.0"
is-windows "^1.0.2"
listr "^0.14.2"
- lodash "^4.17.5"
+ listr-update-renderer "^0.5.0"
+ lodash "^4.17.11"
log-symbols "^2.2.0"
micromatch "^3.1.8"
npm-which "^3.0.1"
@@ -10502,9 +10341,6 @@ locate-path@^3.0.0:
path-exists "^3.0.0"
lock-verify@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.2.tgz#148e4f85974915c9e3c34d694b7de9ecb18ee7a8"
- integrity sha512-QNVwK0EGZBS4R3YQ7F1Ox8p41Po9VGl2QG/2GsuvTbkJZYSsPeWHKMbbH6iZMCHWSMww5nrJroZYnGzI4cePuw==
dependencies:
npm-package-arg "^5.1.2 || 6"
semver "^5.4.1"
@@ -10621,11 +10457,6 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-lodash.merge@^4.6.1:
- version "4.6.1"
- resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
- integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==
-
lodash.mergewith@^4.6.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
@@ -10788,6 +10619,14 @@ make-dir@^1.0.0, make-dir@^1.3.0:
dependencies:
pify "^3.0.0"
+make-dir@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+ integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
make-error@1.x, make-error@^1.1.1, make-error@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
@@ -10909,12 +10748,12 @@ mem@^1.1.0:
mimic-fn "^1.0.0"
mem@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"
- integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.2.0.tgz#5ee057680ed9cb8dad8a78d820f9a8897a102025"
+ integrity sha512-5fJxa68urlY0Ir8ijatKa3eRz5lwXnRCTvo9+TbTGAuTFJOwpGcY0X05moBd0nW45965Njt4CDI2GFQoG8DvqA==
dependencies:
map-age-cleaner "^0.1.1"
- mimic-fn "^1.0.0"
+ mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
memoize-one@^4.0.0:
@@ -11033,22 +10872,17 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.36.0 < 2":
+"mime-db@>= 1.38.0 < 2", mime-db@~1.38.0:
version "1.38.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==
-mime-db@~1.37.0:
- version "1.37.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
- integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
-
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19:
- version "2.1.21"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
- integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
+ version "2.1.22"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd"
+ integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==
dependencies:
- mime-db "~1.37.0"
+ mime-db "~1.38.0"
mime@1.4.1:
version "1.4.1"
@@ -11065,6 +10899,11 @@ mimic-fn@^1.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+mimic-fn@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.0.0.tgz#0913ff0b121db44ef5848242c38bbb35d44cabde"
+ integrity sha512-jbex9Yd/3lmICXwYT6gA/j2mNQGU48wCh/VzRd+/Y/PjYQtlg1gLMdZqvu9s/xH7qKvngxRObl56XZR609IMbA==
+
mimic-response@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
@@ -11230,9 +11069,9 @@ mousetrap-global-bind@^1.1.0:
integrity sha1-zX3pIivQZG+i4BDVTISnTCaojt0=
mousetrap@^1.6.0:
- version "1.6.2"
- resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587"
- integrity sha512-jDjhi7wlHwdO6q6DS7YRmSHcuI+RVxadBkLt3KHrhd3C2b+w5pKefg3oj5beTcHZyVFA9Aksf+yEE1y5jxUjVA==
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a"
+ integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==
move-concurrently@^1.0.1:
version "1.0.1"
@@ -11295,9 +11134,9 @@ mute-stream@~0.0.4:
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
nan@^2.10.0, nan@^2.6.2, nan@^2.9.2:
- version "2.12.1"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
- integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd"
+ integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==
nanomatch@^1.2.9:
version "1.2.13"
@@ -11353,7 +11192,7 @@ negotiator@0.6.1:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
-neo-async@^2.5.0:
+neo-async@^2.5.0, neo-async@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==
@@ -11363,7 +11202,7 @@ nested-object-assign@^1.0.1:
resolved "https://registry.yarnpkg.com/nested-object-assign/-/nested-object-assign-1.0.3.tgz#5aca69390d9affe5a612152b5f0843ae399ac597"
integrity sha512-kgq1CuvLyUcbcIuTiCA93cQ2IJFSlRwXcN+hLcb2qLJwC2qrePHGZZa7IipyWqaWF6tQjdax2pQnVxdq19Zzwg==
-next-tick@1:
+next-tick@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
@@ -11568,10 +11407,10 @@ node-pre-gyp@^0.10.0:
semver "^5.3.0"
tar "^4"
-node-releases@^1.0.0-alpha.11, node-releases@^1.1.3:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.7.tgz#b09a10394d0ed8f7778f72bb861dde68b146303b"
- integrity sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA==
+node-releases@^1.0.0-alpha.11, node-releases@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.11.tgz#9a0841a4b0d92b7d5141ed179e764f42ad22724a"
+ integrity sha512-8v1j5KfP+s5WOTa1spNUAOfreajQPN12JXbRR0oDE+YrJBQCXBnNqUDj27EKpPLOoSiU3tKi3xGPB+JaOdUEQQ==
dependencies:
semver "^5.3.0"
@@ -11736,7 +11575,7 @@ npm-logical-tree@^1.2.1:
resolved "https://registry.yarnpkg.com/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88"
integrity sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==
-"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^5.1.2 || 6", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0:
+"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1"
integrity sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==
@@ -11954,9 +11793,9 @@ number-is-nan@^1.0.0:
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
nwsapi@^2.0.7:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f"
- integrity sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg==
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.1.tgz#08d6d75e69fd791bdea31507ffafe8c843b67e9c"
+ integrity sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg==
oauth-sign@~0.9.0:
version "0.9.0"
@@ -12084,10 +11923,10 @@ on-finished@~2.3.0:
dependencies:
ee-first "1.1.1"
-on-headers@~1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
- integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=
+on-headers@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+ integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
once@^1.3.0, once@^1.3.1, once@^1.4.0, once@~1.4.0:
version "1.4.0"
@@ -12113,13 +11952,20 @@ opener@^1.5.1:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
-opn@5.4.0, opn@^5.1.0, opn@^5.3.0, opn@^5.4.0:
+opn@5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035"
integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw==
dependencies:
is-wsl "^1.1.0"
+opn@^5.1.0, opn@^5.3.0, opn@^5.4.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+ integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+ dependencies:
+ is-wsl "^1.1.0"
+
optimist@^0.6.1, optimist@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -12257,9 +12103,9 @@ p-limit@^1.1.0:
p-try "^1.0.0"
p-limit@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.1.0.tgz#1d5a0d20fb12707c758a655f6bbc4386b5930d68"
- integrity sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
+ integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==
dependencies:
p-try "^2.0.0"
@@ -12298,9 +12144,9 @@ p-try@^1.0.0:
integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
p-try@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
- integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.1.0.tgz#c1a0f1030e97de018bb2c718929d2af59463e505"
+ integrity sha512-H2RyIJ7+A3rjkwKC2l5GGtU4H1vkxKCAGsWasNVd0Set+6i4znxbWy6/j16YDPJDWxhsgZiKAstMEP8wCdSpjA==
package-json@^4.0.0:
version "4.0.1"
@@ -13324,11 +13170,12 @@ promzard@^0.3.0:
dependencies:
read "1"
-prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
- version "15.7.1"
- resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.1.tgz#2fa61e0a699d428b40320127733ee2931f05d9d1"
- integrity sha512-f8Lku2z9kERjOCcnDOPm68EBJAO2K00Q5mSgPAUE/gJuBgsYLbVy6owSrtcHj90zt8PvW+z0qaIIgsIhHOa1Qw==
+prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
+ loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
@@ -13562,9 +13409,9 @@ raw-loader@^0.5.1:
integrity sha1-DD0L6u2KAclm2Xh793goElKpeao=
rc-align@^2.4.0:
- version "2.4.3"
- resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a"
- integrity sha512-h5KgyB5IXYR7iKpYFcMr54cuQ2eozPCZ11kbXPG5+6CWvmyJ+c0R/yjndVndiNk2G3MKcTMbJNdDv5DIckLAxQ==
+ version "2.4.5"
+ resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.5.tgz#c941a586f59d1017f23a428f0b468663fb7102ab"
+ integrity sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==
dependencies:
babel-runtime "^6.26.0"
dom-align "^1.7.0"
@@ -13722,28 +13569,28 @@ react-docgen@^3.0.0:
node-dir "^0.1.10"
recast "^0.16.0"
-react-dom@^16.6.3, react-dom@^16.7.0:
- version "16.8.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.1.tgz#ec860f98853d09d39bafd3a6f1e12389d283dbb4"
- integrity sha512-N74IZUrPt6UiDjXaO7UbDDFXeUXnVhZzeRLy/6iqqN1ipfjrhR60Bp5NuBK+rv3GMdqdIuwIl22u1SYwf330bg==
+react-dom@^16.6.3, react-dom@^16.7.0, react-dom@^16.8.4:
+ version "16.8.4"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48"
+ integrity sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
- scheduler "^0.13.1"
+ scheduler "^0.13.4"
react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3":
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.1.1.tgz#ed1db43e09b146805f6d158296e58eae4f768d36"
- integrity sha512-tqIgDUm4XPSFbxelYpcsnayPU79P26ChnszDl5/RDFKfMuHnRxypS+OFfEyAEO1CtqaB3lrecQ2dyNIE2G0TlQ==
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.2.1.tgz#45d09a9a227988dc85674b23ab3c75b6e820dae5"
+ integrity sha512-r+3Bs9InID2lyIEbR8UIRVtpn4jgu1ArFEZgIy8vibJjijLSdNLX7rH9U68BBVD4RD9v44RXbaK4EHLyKXzNQw==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-error-overlay@^5.1.0:
- version "5.1.3"
- resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.3.tgz#16fcbde75ed4dc6161dc6dc959b48e92c6ffa9ad"
- integrity sha512-GoqeM3Xadie7XUApXOjkY3Qhs8RkwB/Za4WMedBGrOKH1eTuKGyoAECff7jiVonJchOx6KZ9i8ILO5XIoHB+Tg==
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.4.tgz#88dfb88857c18ceb3b9f95076f850d7121776991"
+ integrity sha512-fp+U98OMZcnduQ+NSEiQa4s/XMsbp+5KlydmkbESOw4P69iWZ68ZMFM5a2BuE0FgqPBKApJyRuYHR95jM8lAmg==
react-fuzzy@^0.5.2:
version "0.5.2"
@@ -13775,15 +13622,15 @@ react-highlight-words@0.11.0:
prop-types "^15.5.8"
react-hot-loader@^4.3.6:
- version "4.6.5"
- resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.6.5.tgz#059619c8ac2aae9c6e8178ddc2eac535093cdd2e"
- integrity sha512-ZPAJEWVd8KDdm6dcK0iWrnJiGHruLrcbkIpqn/wQmNjnROpsm2nzrWh23Yh3I/XAjB+35pMa/ZgariwGqwFD9A==
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.8.0.tgz#0b7c7dd9407415e23eb8246fdd28b0b839f54cb6"
+ integrity sha512-HY9F0vITYSVmXhAR6tPkMk240nxmoH8+0rca9iO2B82KVguiCiBJkieS0Wb4CeSIzLWecYx3iOcq8dcbnp0bxA==
dependencies:
fast-levenshtein "^2.0.6"
global "^4.3.0"
- hoist-non-react-statics "^2.5.0"
+ hoist-non-react-statics "^3.3.0"
loader-utils "^1.1.0"
- lodash.merge "^4.6.1"
+ lodash "^4.17.11"
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
shallowequal "^1.0.2"
@@ -13810,10 +13657,7 @@ react-inspector@^2.3.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
-react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
- version "16.8.1"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.1.tgz#a80141e246eb894824fb4f2901c0c50ef31d4cdb"
- integrity sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA==
+react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
react-is@^16.8.4:
version "16.8.4"
@@ -13905,21 +13749,21 @@ react-style-proptype@^3.0.0:
prop-types "^15.5.4"
react-table@^6.8.6:
- version "6.9.1"
- resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.9.1.tgz#50a1713a4de90089f14dcb8cf0f313992ad93a20"
- integrity sha512-QWAwEX24kZPjimhrqRMcdJRi0LX+4MWJG7p9WmAPrfEoYG2HTR1odxa5jLPu7sKVkgsUp0PtZ2jxQ4aXabRJNA==
+ version "6.9.2"
+ resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.9.2.tgz#6a59adfeb8d5deced288241ed1c7847035b5ec5f"
+ integrity sha512-sTbNHU8Um0xRtmCd1js873HXnXaMWeBwZoiljuj0l1d44eaqjKyYPK/3HCBbJg1yeE2O5pQJ3Km0tlm9niNL9w==
dependencies:
classnames "^2.2.5"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.5.0, react-test-renderer@^16.7.0:
- version "16.8.1"
- resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.1.tgz#72845ad9269be526126e97853311982f781767be"
- integrity sha512-Bd21TN3+YVl6GZwav6O0T6m5UwGfOj+2+xZH5VH93ToD6M5uclN/c+R1DGX49ueG413KZPUx7Kw3sOYz2aJgfg==
+ version "16.8.4"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.4.tgz#abee4c2c3bf967a8892a7b37f77370c5570d5329"
+ integrity sha512-jQ9Tf/ilIGSr55Cz23AZ/7H3ABEdo9oy2zF9nDHZyhLHDSLKuoILxw2ifpBfuuwQvj4LCoqdru9iZf7gwFH28A==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
- react-is "^16.8.1"
- scheduler "^0.13.1"
+ react-is "^16.8.4"
+ scheduler "^0.13.4"
react-textarea-autosize@^7.0.4:
version "7.1.0"
@@ -13930,9 +13774,9 @@ react-textarea-autosize@^7.0.4:
prop-types "^15.6.0"
react-transition-group@^2.0.0, react-transition-group@^2.2.1:
- version "2.5.3"
- resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.3.tgz#26de363cab19e5c88ae5dbae105c706cf953bb92"
- integrity sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.6.1.tgz#abf4a95e2f13fb9ba83a970a896fedbc5c4856a2"
+ integrity sha512-9DHwCy0aOYEe35frlEN68N9ut/THDQBLnVoQuKTvzF4/s3tk7lqkefCqxK2Nv96fOh6JXk6tQtliygk6tl3bQA==
dependencies:
dom-helpers "^3.3.1"
loose-envify "^1.4.0"
@@ -13964,15 +13808,15 @@ react-virtualized@^9.21.0:
prop-types "^15.6.0"
react-lifecycles-compat "^3.0.4"
-react@^16.6.3, react@^16.7.0:
- version "16.8.1"
- resolved "https://registry.yarnpkg.com/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a"
- integrity sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ==
+react@^16.6.3, react@^16.7.0, react@^16.8.4:
+ version "16.8.4"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768"
+ integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
- scheduler "^0.13.1"
+ scheduler "^0.13.4"
reactcss@^1.2.0:
version "1.2.3"
@@ -14088,20 +13932,7 @@ read@1, read@~1.0.1, read@~1.0.7:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@1.0:
- version "1.0.34"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
- integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "0.0.1"
- string_decoder "~0.10.x"
-
-readable-stream@^3.0.6:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
- integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==
+readable-stream@^3.0.6, readable-stream@^3.1.1:
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
@@ -14245,10 +14076,10 @@ redux@^4.0.0, redux@^4.0.1:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
-regenerate-unicode-properties@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c"
- integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==
+regenerate-unicode-properties@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662"
+ integrity sha512-SbA/iNrBUf6Pv2zU8Ekv1Qbhv92yxL4hiDa2siuxs4KKn4oOoMDHXjAf7+Nz9qinUQ46B1LcWEi/PhJfPWpZWQ==
dependencies:
regenerate "^1.4.0"
@@ -14267,10 +14098,6 @@ regenerator-runtime@^0.12.0, regenerator-runtime@^0.12.1:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
-regenerator-transform@^0.13.3:
- version "0.13.3"
- resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb"
- integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==
dependencies:
private "^0.1.6"
@@ -14304,13 +14131,9 @@ regexp-replace-loader@^1.0.1:
loader-utils "^1.0.2"
regexp-tree@^0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.1.tgz#27b455f9b138ca2e84c090e9aff1ffe2a04d97fa"
- integrity sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw==
- dependencies:
- cli-table3 "^0.5.0"
- colors "^1.1.2"
- yargs "^12.0.5"
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.5.tgz#7cd71fca17198d04b4176efd79713f2998009397"
+ integrity sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ==
regexp.prototype.flags@^1.2.0:
version "1.2.0"
@@ -14329,16 +14152,16 @@ regexpu-core@^1.0.0:
regjsparser "^0.1.4"
regexpu-core@^4.1.3, regexpu-core@^4.2.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32"
- integrity sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA==
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae"
+ integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==
dependencies:
regenerate "^1.4.0"
- regenerate-unicode-properties "^7.0.0"
+ regenerate-unicode-properties "^8.0.2"
regjsgen "^0.5.0"
regjsparser "^0.6.0"
unicode-match-property-ecmascript "^1.0.4"
- unicode-match-property-value-ecmascript "^1.0.2"
+ unicode-match-property-value-ecmascript "^1.1.0"
registry-auth-token@^3.0.1:
version "3.3.2"
@@ -14412,13 +14235,13 @@ render-fragment@^0.1.1:
integrity sha512-+DnAcalJYR8GE5VRuQGGu78Q0GDe8EXnkuk4DF8gbAhIeS6LRt4j+aaggLLj4PtQVfXNC61McXvXI58WqmRleQ==
renderkid@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.2.tgz#12d310f255360c07ad8fde253f6c9e9de372d2aa"
- integrity sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
+ integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==
dependencies:
css-select "^1.1.0"
- dom-converter "~0.2"
- htmlparser2 "~3.3.0"
+ dom-converter "^0.2"
+ htmlparser2 "^3.3.0"
strip-ansi "^3.0.0"
utila "^0.4.0"
@@ -14451,21 +14274,21 @@ request-progress@^2.0.1:
dependencies:
throttleit "^1.0.0"
-request-promise-core@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
- integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
+request-promise-core@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346"
+ integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==
dependencies:
- lodash "^4.13.1"
+ lodash "^4.17.11"
request-promise-native@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5"
- integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
+ integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==
dependencies:
- request-promise-core "1.1.1"
- stealthy-require "^1.1.0"
- tough-cookie ">=2.3.3"
+ request-promise-core "1.1.2"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
request@^2.81.0, request@^2.87.0, request@^2.88.0:
version "2.88.0"
@@ -14671,23 +14494,23 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
inherits "^2.0.1"
rollup-plugin-commonjs@^9.2.0:
- version "9.2.0"
- resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.0.tgz#4604e25069e0c78a09e08faa95dc32dec27f7c89"
- integrity sha512-0RM5U4Vd6iHjL6rLvr3lKBwnPsaVml+qxOGaaNUWN1lSq6S33KhITOfHmvxV3z2vy9Mk4t0g4rNlVaJJsNQPWA==
+ version "9.2.1"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.1.tgz#bb151ca8fa23600c7a03e25f9f0a45b1ee922dac"
+ integrity sha512-X0A/Cp/t+zbONFinBhiTZrfuUaVwRIp4xsbKq/2ohA2CDULa/7ONSJTelqxon+Vds2R2t2qJTqJQucKUC8GKkw==
dependencies:
estree-walker "^0.5.2"
magic-string "^0.25.1"
- resolve "^1.8.1"
+ resolve "^1.10.0"
rollup-pluginutils "^2.3.3"
rollup-plugin-node-resolve@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.0.tgz#9bc6b8205e9936cc0e26bba2415f1ecf1e64d9b2"
- integrity sha512-7Ni+/M5RPSUBfUaP9alwYQiIKnKeXCOHiqBpKUl9kwp3jX5ZJtgXAait1cne6pGEVUUztPD6skIKH9Kq9sNtfw==
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.1.tgz#f95765d174e5daeef9ea6268566141f53aa9d422"
+ integrity sha512-fSS7YDuCe0gYqKsr5OvxMloeZYUSgN43Ypi1WeRZzQcWtHgFayV5tUSPYpxuaioIIWaBXl6NrVk0T2/sKwueLg==
dependencies:
builtin-modules "^3.0.0"
is-module "^1.0.0"
- resolve "^1.8.1"
+ resolve "^1.10.0"
rollup-plugin-sourcemaps@^0.4.2:
version "0.4.2"
@@ -14708,9 +14531,9 @@ rollup-plugin-terser@^4.0.4:
terser "^3.14.1"
rollup-plugin-typescript2@^0.19.2:
- version "0.19.2"
- resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.19.2.tgz#87d9c799cd6e02efbedbba25af12753a1e92b6c2"
- integrity sha512-DRG7SaYX0QzBIz6rII5nm1UkiceS95r8mJjujugybyIueNF3auvzGTHMK62O7As/0q5RHjXsOguWOUv+KJKLFA==
+ version "0.19.3"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.19.3.tgz#713063233461765f030a2baa2640905c2656164f"
+ integrity sha512-lsRqfBCZhMl/tq9AT5YnQvzQWzXtnx3EQYFcHD72gul7nyyoOrzx5yCEH20smpw58v6UkHHZz03FbdLEPoHWjA==
dependencies:
fs-extra "7.0.1"
resolve "1.8.1"
@@ -14727,7 +14550,7 @@ rollup-plugin-visualizer@^0.9.2:
source-map "^0.7.3"
typeface-oswald "0.0.54"
-rollup-pluginutils@2.3.3, rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3:
+rollup-pluginutils@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794"
integrity sha512-2XZwja7b6P5q4RZ5FhyX1+f46xi1Z3qBKigLRZ6VTZjwbN0K1IFGMlwm06Uu0Emcre2Z63l77nq/pzn+KxIEoA==
@@ -14735,14 +14558,22 @@ rollup-pluginutils@2.3.3, rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3:
estree-walker "^0.5.2"
micromatch "^2.3.11"
+rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.5.0.tgz#23be0f05ac3972ea7b08fc7870cb91fde5b23a09"
+ integrity sha512-9Muh1H+XB5f5ONmKMayUoTYR1EZwHbwJJ9oZLrKT5yuTf/RLIQ5mYIGsrERquVucJmjmaAW0Y7+6Qo1Ep+5w3Q==
+ dependencies:
+ estree-walker "^0.6.0"
+ micromatch "^3.1.10"
+
rollup@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.1.2.tgz#8d094b85683b810d0c05a16bd7618cf70d48eba7"
- integrity sha512-OkdMxqMl8pWoQc5D8y1cIinYQPPLV8ZkfLgCzL6SytXeNA2P7UHynEQXI9tYxuAjAMsSyvRaWnyJDLHMxq0XAg==
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.6.0.tgz#4329f4634718197c678d18491724d50d8b7ee76c"
+ integrity sha512-qu9iWyuiOxAuBM8cAwLuqPclYdarIpayrkfQB7aTGTiyYPbvx+qVF33sIznfq4bxZCiytQux/FvZieUBAXivCw==
dependencies:
"@types/estree" "0.0.39"
- "@types/node" "*"
- acorn "^6.0.5"
+ "@types/node" "^11.9.5"
+ acorn "^6.1.1"
rst-selector-parser@^2.2.3:
version "2.2.3"
@@ -14900,10 +14731,10 @@ sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
-scheduler@^0.13.1:
- version "0.13.1"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.1.tgz#1a217df1bfaabaf4f1b92a9127d5d732d85a9591"
- integrity sha512-VJKOkiKIN2/6NOoexuypwSrybx13MY7NSy9RNt8wPvZDMRT1CW6qlpF5jXRToXNHz3uWzbm2elNpZfXfGPqP9A==
+scheduler@^0.13.4:
+ version "0.13.4"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298"
+ integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -15240,9 +15071,9 @@ slash@^2.0.0:
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
slate-base64-serializer@^0.2.36:
- version "0.2.95"
- resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.95.tgz#018ccf749f0f0f62fc2d5f4722fd770d359cf76b"
- integrity sha512-WK8roQUQBM7lHXNS6HYNmMSJ5tJmuoLeZkHJEHWCEl+1op1m5sC2onzBfpIRNP8AijlZ3m+lGlxfLO+3VtBMxw==
+ version "0.2.97"
+ resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.97.tgz#232220da1320cdc918369834940f1c997b1e80f9"
+ integrity sha512-f8TnU8rPz8qzKsGaJJwEzPoR5JQ55kB03nO/aIeXRVg4PPY/Rp9VaEcBP6vmeimH51gf3vpe8S5RUITbW9pXHw==
dependencies:
isomorphic-base64 "^1.0.2"
@@ -15411,17 +15242,17 @@ sockjs@0.3.19:
uuid "^3.0.1"
socks-proxy-agent@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473"
- integrity sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
+ integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==
dependencies:
- agent-base "~4.2.0"
- socks "~2.2.0"
+ agent-base "~4.2.1"
+ socks "~2.3.2"
-socks@~2.2.0:
- version "2.2.3"
- resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.3.tgz#7399ce11e19b2a997153c983a9ccb6306721f2dc"
- integrity sha512-+2r83WaRT3PXYoO/1z+RDEBE7Z2f9YcdQnJ0K/ncXXbV5gJ6wYfNAebYFYiiUjM6E4JyXnPY8cimwyvFYHVUUA==
+socks@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e"
+ integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ==
dependencies:
ip "^1.1.5"
smart-buffer "4.0.2"
@@ -15460,10 +15291,10 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0"
urix "^0.1.0"
-source-map-support@^0.5.3, source-map-support@^0.5.6, source-map-support@~0.5.9:
- version "0.5.10"
- resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c"
- integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==
+source-map-support@^0.5.3, source-map-support@^0.5.6, source-map-support@~0.5.10:
+ version "0.5.11"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2"
+ integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
@@ -15652,7 +15483,7 @@ stdout-stream@^1.4.0:
dependencies:
readable-stream "^2.0.1"
-stealthy-require@^1.1.0:
+stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
@@ -15738,13 +15569,13 @@ string-width@^1.0.1, string-width@^1.0.2:
strip-ansi "^4.0.0"
string-width@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1"
- integrity sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew==
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+ integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
dependencies:
emoji-regex "^7.0.1"
is-fullwidth-code-point "^2.0.0"
- strip-ansi "^5.0.0"
+ strip-ansi "^5.1.0"
string.prototype.matchall@^3.0.0:
version "3.0.1"
@@ -15850,12 +15681,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
dependencies:
ansi-regex "^2.0.0"
-strip-ansi@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f"
- integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==
+strip-ansi@^5.0.0, strip-ansi@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
dependencies:
- ansi-regex "^4.0.0"
+ ansi-regex "^4.1.0"
strip-bom@^2.0.0:
version "2.0.0"
@@ -15967,6 +15798,16 @@ svg-url-loader@^2.3.2:
loader-utils "1.1.0"
svgo@^1.0.0:
+ dependencies:
+ coa "~1.0.1"
+ colors "~1.1.2"
+ csso "~2.3.1"
+ js-yaml "~3.7.0"
+ mkdirp "~0.5.1"
+ sax "~1.2.1"
+ whet.extend "~0.9.9"
+
+svgo@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.0.tgz#305a8fc0f4f9710828c65039bb93d5793225ffc3"
integrity sha512-xBfxJxfk4UeVN8asec9jNxHiv3UAMv/ujwBWGYvQhhMb2u3YTGKkiybPcLFDLq7GLLWE9wa73e0/m8L5nTzQbw==
@@ -15985,24 +15826,16 @@ svgo@^1.0.0:
stable "^0.1.8"
unquote "~1.1.1"
util.promisify "~1.0.0"
-
-svgo@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985"
- integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==
- dependencies:
- coa "~2.0.1"
- colors "~1.1.2"
css-select "^2.0.0"
- css-select-base-adapter "~0.1.0"
+ css-select-base-adapter "^0.1.1"
css-tree "1.0.0-alpha.28"
css-url-regex "^1.1.0"
- csso "^3.5.0"
+ csso "^3.5.1"
js-yaml "^3.12.0"
mkdirp "~0.5.1"
- object.values "^1.0.4"
+ object.values "^1.1.0"
sax "~1.2.4"
- stable "~0.1.6"
+ stable "^0.1.8"
unquote "~1.1.1"
util.promisify "~1.0.0"
@@ -16116,9 +15949,6 @@ term-size@^1.2.0:
execa "^0.7.0"
terser-webpack-plugin@^1.1.0:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9"
- integrity sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==
dependencies:
cacache "^11.0.2"
find-cache-dir "^2.0.0"
@@ -16144,13 +15974,13 @@ terser-webpack-plugin@^1.2.3:
worker-farm "^1.5.2"
terser@^3.14.1, terser@^3.16.1:
- version "3.16.1"
- resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493"
- integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==
+ version "3.17.0"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
+ integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==
dependencies:
- commander "~2.17.1"
+ commander "^2.19.0"
source-map "~0.6.1"
- source-map-support "~0.5.9"
+ source-map-support "~0.5.10"
test-exclude@^5.0.0:
version "5.1.0"
@@ -16346,16 +16176,7 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
-tough-cookie@>=2.3.3:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
- integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
- dependencies:
- ip-regex "^2.1.0"
- psl "^1.1.28"
- punycode "^2.1.1"
-
-tough-cookie@^2.3.4:
+tough-cookie@^2.3.3, tough-cookie@^2.3.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -16574,9 +16395,6 @@ typescript@3.3.3333:
integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==
typescript@^3.2.2:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3.tgz#f1657fc7daa27e1a8930758ace9ae8da31403221"
- integrity sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A==
ua-parser-js@^0.7.18:
version "0.7.19"
@@ -16594,11 +16412,11 @@ uglify-js@2.6.x:
yargs "~3.10.0"
uglify-js@3.4.x, uglify-js@^3.1.4:
- version "3.4.9"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
- integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==
+ version "3.4.10"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
+ integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==
dependencies:
- commander "~2.17.1"
+ commander "~2.19.0"
source-map "~0.6.1"
uglify-to-browserify@~1.0.0:
@@ -16651,15 +16469,15 @@ unicode-match-property-ecmascript@^1.0.4:
unicode-canonical-property-names-ecmascript "^1.0.4"
unicode-property-aliases-ecmascript "^1.0.4"
-unicode-match-property-value-ecmascript@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4"
- integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==
+unicode-match-property-value-ecmascript@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
+ integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
unicode-property-aliases-ecmascript@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0"
- integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
+ integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
unified@^7.0.2:
version "7.1.0"
@@ -17110,12 +16928,11 @@ webpack-core@^0.6.5:
source-list-map "~0.1.7"
source-map "~0.4.1"
-webpack-dev-middleware@^3.4.0:
- version "3.5.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.5.2.tgz#d768b6194f3fe8d72d51feded49de359e8d96ffb"
- integrity sha512-nPmshdt1ckcwWjI0Ubrdp8KroeuprW6xFKYqk0u3MflNMBXvHPnMATsC7/L/enwav2zvLCfj/Usr47qnF3KQyA==
+webpack-dev-middleware@^3.4.0, webpack-dev-middleware@^3.5.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4"
+ integrity sha512-XQmemun8QJexMEvNFbD2BIg4eSKrmSIMrTfnl2nql2Sc6OGAYFyb8rwuYrCjl/IiEYYuyTEiimMscu7EXji/Dw==
dependencies:
- memory-fs "~0.4.1"
mime "^2.3.1"
range-parser "^1.0.3"
webpack-log "^2.0.0"
@@ -17240,14 +17057,14 @@ webpack@4.29.6:
webpack-sources "^1.3.0"
webpack@^4.23.1:
- version "4.29.3"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.3.tgz#e0b406a7b4201ed5e4fb4f84fd7359f9a7db4647"
- integrity sha512-xPJvFeB+8tUflXFq+OgdpiSnsCD5EANyv56co5q8q8+YtEasn5Sj3kzY44mta+csCIEB0vneSxnuaHkOL2h94A==
+ version "4.29.6"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.6.tgz#66bf0ec8beee4d469f8b598d3988ff9d8d90e955"
+ integrity sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw==
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"
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-module-context" "1.8.5"
+ "@webassemblyjs/wasm-edit" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
acorn "^6.0.5"
acorn-dynamic-import "^4.0.0"
ajv "^6.1.0"