From 3a3272e225c986cdeb762197a82f84b84b9e769f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 12 Dec 2017 09:35:57 +0100 Subject: [PATCH 001/666] annotations: allows template variables to be used in tag filter When filtering built in annotations by tag, interpolates the tag with template variables. Fixes #9587 --- .../plugins/datasource/grafana/datasource.ts | 7 +- .../grafana/specs/datasource.jest.ts | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 5ca3c433476..9eb9862094a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; class GrafanaDatasource { /** @ngInject */ - constructor(private backendSrv, private $q) {} + constructor(private backendSrv, private $q, private templateSrv) {} query(options) { return this.backendSrv @@ -58,6 +58,11 @@ class GrafanaDatasource { if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) { return this.$q.when([]); } + const tags = []; + for (let t of params.tags) { + tags.push(this.templateSrv.replace(t)); + } + params.tags = tags; } return this.backendSrv.get('/api/annotations', params); diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts new file mode 100644 index 00000000000..544b04056ac --- /dev/null +++ b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts @@ -0,0 +1,65 @@ +import {GrafanaDatasource} from "../datasource"; +import q from 'q'; +import moment from 'moment'; + +describe('grafana data source', () => { + describe('when executing an annotations query', () => { + let calledBackendSrvParams; + const backendSrvStub = { + get: (url, options) => { + calledBackendSrvParams = options; + return q.resolve([]); + } + }; + + const templateSrvStub = { + replace: val => val.replace('$var', 'replaced') + }; + + const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); + + describe('with tags that have template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['tag1:$var']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('tag1:replaced'); + }); + }); + + describe('with type dashboard', () => { + const options = setupAnnotationQueryOptions( + { + type: 'dashboard', + tags: ['tag1'] + }, + {id: 1} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should remove tags from query options', () => { + expect(calledBackendSrvParams.tags).toBe(undefined); + }); + }); + }); +}); + +function setupAnnotationQueryOptions(annotation, dashboard?) { + return { + annotation: annotation, + dashboard: dashboard, + range: { + from: moment(1432288354), + to: moment(1432288401) + }, + rangeRaw: {from: "now-24h", to: "now"} + }; +} From 069012639af8ed873a776772af22ddf883ae1722 Mon Sep 17 00:00:00 2001 From: Jonathan McCall Date: Fri, 20 Apr 2018 12:17:17 -0400 Subject: [PATCH 002/666] Sort results from GetDashboardTags --- pkg/services/sqlstore/dashboard.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c0848f08863..4999e40d15e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -294,7 +294,8 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { FROM dashboard INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id WHERE dashboard.org_id=? - GROUP BY term` + GROUP BY term + ORDER BY term` query.Result = make([]*m.DashboardTagCloudItem, 0) sess := x.Sql(sql, query.OrgId) From 18e4271abdabaa111feabf656d3f1bc0f8bf0355 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 10:34:57 +0200 Subject: [PATCH 003/666] added span with folder title that is shown for recently and starred, created a new class for folder title --- public/app/core/components/search/search_results.html | 2 +- public/sass/components/_search.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 7435f8d0b7e..9f266ed3a6b 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -33,7 +33,7 @@ -
{{::item.title}}
+
{{::item.title}} {{::item.folderTitle}}
diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 8338a5d72ae..b00168505fa 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -208,6 +208,12 @@ color: $list-item-link-color; } +.search-item__body-folder-title { + color: $text-color-weak; + font-style: italic; + padding-left: 0.25rem; +} + .search-item__icon { padding: 5px; flex: 0 0 auto; From 83a73327cfb42ed5a3bea73497a5b4c7303a020e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 15:16:22 +0200 Subject: [PATCH 004/666] removed italic --- public/sass/components/_search.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index b00168505fa..3b6c1fbcce6 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -210,7 +210,6 @@ .search-item__body-folder-title { color: $text-color-weak; - font-style: italic; padding-left: 0.25rem; } From 8419cc05531a8db0bd3d3ce0a809096189ab3f33 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 4 Jun 2018 13:32:19 +0200 Subject: [PATCH 005/666] made folder text smaller --- public/sass/components/_search.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 3b6c1fbcce6..e2e3336db05 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -211,6 +211,7 @@ .search-item__body-folder-title { color: $text-color-weak; padding-left: 0.25rem; + font-size: $font-size-xs; } .search-item__icon { From 23c97d080ff6892379038e3742d712aa41c5b771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Niemann?= Date: Wed, 13 Jun 2018 09:43:33 +0200 Subject: [PATCH 006/666] added id tag to Panels for html bookmarking on longer Dashboards --- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 290e587eace..457ad4ef56c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -177,7 +177,7 @@ export class DashboardGrid extends React.Component { for (let panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( -
+
); From 757e2b0b7ee264079853a92fe70a706a16230984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Niemann?= Date: Mon, 18 Jun 2018 10:59:44 +0200 Subject: [PATCH 007/666] added comment to reason the id tag --- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 457ad4ef56c..9a451798ff7 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -177,6 +177,7 @@ export class DashboardGrid extends React.Component { for (let panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( + /** panel-id is set for html bookmarks */
From 6e4b199bc20431589ece37785fa0730e1a0e3db1 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 8 Jul 2018 11:07:01 +0200 Subject: [PATCH 008/666] tabs to spaces testing commit permisions :) --- tslint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index e7a51295701..22e123e0364 100644 --- a/tslint.json +++ b/tslint.json @@ -2,7 +2,7 @@ "rules": { "no-string-throw": true, "no-unused-expression": true, - "no-unused-variable": false, + "no-unused-variable": false, "no-use-before-declare": false, "no-duplicate-variable": true, "curly": true, From fc5dba27b87cd2970a06dc6f64dd23c3450260f6 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 8 Jul 2018 11:08:01 +0200 Subject: [PATCH 009/666] revert --- tslint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index 22e123e0364..e7a51295701 100644 --- a/tslint.json +++ b/tslint.json @@ -2,7 +2,7 @@ "rules": { "no-string-throw": true, "no-unused-expression": true, - "no-unused-variable": false, + "no-unused-variable": false, "no-use-before-declare": false, "no-duplicate-variable": true, "curly": true, From 3740d564913ac5fe9f9d1b4e5e80ba3881457622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Jul 2018 09:17:38 +0200 Subject: [PATCH 010/666] wip: redux poc --- package.json | 9 ++++-- public/app/store/configureStore.dev.ts | 11 +++++++ public/app/store/configureStore.prod.ts | 9 ++++++ public/app/store/configureStore.ts | 5 +++ public/app/store/nav/nav.ts | 0 public/app/store/rootReducer.ts | 23 ++++++++++++++ yarn.lock | 42 +++++++++++++++++++++++-- 7 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 public/app/store/configureStore.dev.ts create mode 100644 public/app/store/configureStore.prod.ts create mode 100644 public/app/store/configureStore.ts create mode 100644 public/app/store/nav/nav.ts create mode 100644 public/app/store/rootReducer.ts diff --git a/package.json b/package.json index a43b2adc5be..3523b9eac6d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "expose-loader": "^0.7.3", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", - "fork-ts-checker-webpack-plugin": "^0.4.1", + "fork-ts-checker-webpack-plugin": "^0.4.2", "gaze": "^1.1.2", "glob": "~7.0.0", "grunt": "1.0.1", @@ -90,15 +90,14 @@ "style-loader": "^0.21.0", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", - "ts-loader": "^4.3.0", "ts-jest": "^22.4.6", + "ts-loader": "^4.3.0", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", "typescript": "^2.6.2", "webpack": "^4.8.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", - "fork-ts-checker-webpack-plugin": "^0.4.2", "webpack-cli": "^2.1.4", "webpack-dev-server": "^3.1.0", "webpack-merge": "^4.1.0", @@ -170,9 +169,13 @@ "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", "react-popper": "^0.7.5", + "react-redux": "^5.0.7", "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "react-transition-group": "^2.2.1", + "redux": "^4.0.0", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", diff --git a/public/app/store/configureStore.dev.ts b/public/app/store/configureStore.dev.ts new file mode 100644 index 00000000000..98b1ca19634 --- /dev/null +++ b/public/app/store/configureStore.dev.ts @@ -0,0 +1,11 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import { createLogger } from 'redux-logger'; +import rootReducer from './reducers'; + +export let store; + +export function configureStore() { + const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); +} diff --git a/public/app/store/configureStore.prod.ts b/public/app/store/configureStore.prod.ts new file mode 100644 index 00000000000..3c75e5b850b --- /dev/null +++ b/public/app/store/configureStore.prod.ts @@ -0,0 +1,9 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import rootReducer from './reducers'; + +export let store; + +export function configureStore() { + store = createStore(rootReducer, {}, compose(applyMiddleware(thunk))); +} diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts new file mode 100644 index 00000000000..78c9ea1fdc0 --- /dev/null +++ b/public/app/store/configureStore.ts @@ -0,0 +1,5 @@ +if (process.env.NODE_ENV === 'production') { + module.exports = require('./configureStore.prod'); +} else { + module.exports = require('./configureStore.dev'); +} diff --git a/public/app/store/nav/nav.ts b/public/app/store/nav/nav.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/store/rootReducer.ts b/public/app/store/rootReducer.ts new file mode 100644 index 00000000000..2e6d4cb53cd --- /dev/null +++ b/public/app/store/rootReducer.ts @@ -0,0 +1,23 @@ +import * as ActionTypes from '../actions'; +import { combineReducers } from 'redux'; +import { nav } from './nav'; + +// Updates error message to notify about the failed fetches. +const errorMessage = (state = null, action) => { + const { type, error } = action; + + if (type === ActionTypes.RESET_ERROR_MESSAGE) { + return null; + } else if (error) { + return error; + } + + return state; +}; + +const rootReducer = combineReducers({ + nav, + errorMessage, +}); + +export default rootReducer; diff --git a/yarn.lock b/yarn.lock index 6772d7c14a4..09fc26c2742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3293,6 +3293,10 @@ dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + deep-equal@*, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -5885,7 +5889,7 @@ into-stream@^3.1.0: from2 "^2.1.1" p-is-promise "^1.1.0" -invariant@^2.2.2: +invariant@^2.0.0, invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: @@ -7343,6 +7347,10 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" +lodash-es@^4.17.5: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7974,7 +7982,7 @@ mocha@^4.0.1: mkdirp "0.5.1" supports-color "4.4.0" -moment@^2.18.1: +moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -10074,6 +10082,17 @@ react-reconciler@^0.7.0: object-assign "^4.1.1" prop-types "^15.6.0" +react-redux@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" + dependencies: + hoist-non-react-statics "^2.5.0" + invariant "^2.0.0" + lodash "^4.17.5" + lodash-es "^4.17.5" + loose-envify "^1.1.0" + prop-types "^15.6.0" + react-resizable@1.x: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -10337,6 +10356,23 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + dependencies: + deep-diff "^0.3.5" + +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + +redux@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03" + dependencies: + loose-envify "^1.1.0" + symbol-observable "^1.2.0" + regenerate@^1.2.1: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" @@ -11723,7 +11759,7 @@ symbol-observable@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" -symbol-observable@^1.1.0: +symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" From d85fa66fb475ab96682fdd988a80e46584c3e363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Jul 2018 10:28:20 +0200 Subject: [PATCH 011/666] redid redux poc, old branch was to old and caused to many conflicts --- .../containers/ServerStats/ServerStats.tsx | 4 +++ public/app/core/components/grafana_app.ts | 2 ++ public/app/store/configureStore.dev.ts | 11 ------- public/app/store/configureStore.prod.ts | 9 ------ public/app/store/configureStore.ts | 18 ++++++++--- public/app/store/nav/actions.ts | 30 +++++++++++++++++++ public/app/store/nav/nav.ts | 0 public/app/store/nav/reducers.ts | 30 +++++++++++++++++++ public/app/store/rootReducer.ts | 23 -------------- 9 files changed, 80 insertions(+), 47 deletions(-) delete mode 100644 public/app/store/configureStore.dev.ts delete mode 100644 public/app/store/configureStore.prod.ts create mode 100644 public/app/store/nav/actions.ts delete mode 100644 public/app/store/nav/nav.ts create mode 100644 public/app/store/nav/reducers.ts delete mode 100644 public/app/store/rootReducer.ts diff --git a/public/app/containers/ServerStats/ServerStats.tsx b/public/app/containers/ServerStats/ServerStats.tsx index 761b296855f..bed86b43160 100644 --- a/public/app/containers/ServerStats/ServerStats.tsx +++ b/public/app/containers/ServerStats/ServerStats.tsx @@ -3,6 +3,8 @@ import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import IContainerProps from 'app/containers/IContainerProps'; +import { store } from 'app/store/configureStore'; +import { setNav } from 'app/store/nav/actions'; @inject('nav', 'serverStats') @observer @@ -13,6 +15,8 @@ export class ServerStats extends React.Component { nav.load('cfg', 'admin', 'server-stats'); serverStats.load(); + + store.dispatch(setNav('new', { asd: 'tasd' })); } render() { diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index fd2e32db3a7..fa2c96ade32 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -10,6 +10,7 @@ import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { configureStore } from 'app/store/configureStore'; export class GrafanaCtrl { /** @ngInject */ @@ -24,6 +25,7 @@ export class GrafanaCtrl { backendSrv: BackendSrv, datasourceSrv: DatasourceSrv ) { + configureStore(); createStore({ backendSrv, datasourceSrv }); $scope.init = function() { diff --git a/public/app/store/configureStore.dev.ts b/public/app/store/configureStore.dev.ts deleted file mode 100644 index 98b1ca19634..00000000000 --- a/public/app/store/configureStore.dev.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import { createLogger } from 'redux-logger'; -import rootReducer from './reducers'; - -export let store; - -export function configureStore() { - const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); -} diff --git a/public/app/store/configureStore.prod.ts b/public/app/store/configureStore.prod.ts deleted file mode 100644 index 3c75e5b850b..00000000000 --- a/public/app/store/configureStore.prod.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import rootReducer from './reducers'; - -export let store; - -export function configureStore() { - store = createStore(rootReducer, {}, compose(applyMiddleware(thunk))); -} diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 78c9ea1fdc0..a0dfe576ed6 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,5 +1,15 @@ -if (process.env.NODE_ENV === 'production') { - module.exports = require('./configureStore.prod'); -} else { - module.exports = require('./configureStore.dev'); +import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; +import thunk from 'redux-thunk'; +import { createLogger } from 'redux-logger'; +import { navReducer } from './nav/reducers'; + +const rootReducer = combineReducers({ + nav: navReducer, +}); + +export let store; + +export function configureStore() { + const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); } diff --git a/public/app/store/nav/actions.ts b/public/app/store/nav/actions.ts new file mode 100644 index 00000000000..eca99cc2b90 --- /dev/null +++ b/public/app/store/nav/actions.ts @@ -0,0 +1,30 @@ +// +// Only test actions to test redux & typescript +// + +export enum ActionTypes { + SET_NAV = 'SET_NAV', + SET_QUERY = 'SET_QUERY', +} + +export interface SetNavAction { + type: ActionTypes.SET_NAV; + payload: { + path: string; + query: object; + }; +} + +export interface SetQueryAction { + type: ActionTypes.SET_QUERY; + payload: { + query: object; + }; +} + +export type Action = SetNavAction | SetQueryAction; + +export const setNav = (path: string, query: object): SetNavAction => ({ + type: ActionTypes.SET_NAV, + payload: { path: path, query: query }, +}); diff --git a/public/app/store/nav/nav.ts b/public/app/store/nav/nav.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/public/app/store/nav/reducers.ts b/public/app/store/nav/reducers.ts new file mode 100644 index 00000000000..6e9d6e713a0 --- /dev/null +++ b/public/app/store/nav/reducers.ts @@ -0,0 +1,30 @@ +import { Action, ActionTypes } from './actions'; + +export interface NavState { + path: string; + query: object; +} + +const initialState: NavState = { + path: '/test', + query: {}, +}; + +export const navReducer = (state: NavState = initialState, action: Action): NavState => { + switch (action.type) { + case ActionTypes.SET_NAV: { + return { ...state, path: action.payload.path, query: action.payload.query }; + } + + case ActionTypes.SET_QUERY: { + return { + ...state, + query: action.payload.query, + }; + } + + default: { + return state; + } + } +}; diff --git a/public/app/store/rootReducer.ts b/public/app/store/rootReducer.ts deleted file mode 100644 index 2e6d4cb53cd..00000000000 --- a/public/app/store/rootReducer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as ActionTypes from '../actions'; -import { combineReducers } from 'redux'; -import { nav } from './nav'; - -// Updates error message to notify about the failed fetches. -const errorMessage = (state = null, action) => { - const { type, error } = action; - - if (type === ActionTypes.RESET_ERROR_MESSAGE) { - return null; - } else if (error) { - return error; - } - - return state; -}; - -const rootReducer = combineReducers({ - nav, - errorMessage, -}); - -export default rootReducer; From e944803f10e6bf2ad0bf9f572bc6cd4ed659bff0 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:13:15 -0700 Subject: [PATCH 012/666] Update debian.md added local login info --- docs/sources/installation/debian.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 4bb245a586e..d3d1e3db4b2 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -100,6 +100,8 @@ This will start the `grafana-server` process as the `grafana` user, which was created during the package installation. The default HTTP port is `3000` and default user and group is `admin`. +Default login and password `admin`/ `admin` + To configure the Grafana server to start at boot time: ```bash From bcb11d6747fd8ec960608c052760d1efa386f1c0 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:13:48 -0700 Subject: [PATCH 013/666] Update rpm.md added local login info --- docs/sources/installation/rpm.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 13597b9d921..0a3aaf9995f 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -115,6 +115,8 @@ This will start the `grafana-server` process as the `grafana` user, which is created during package installation. The default HTTP port is `3000`, and default user and group is `admin`. +Default login and password `admin`/ `admin` + To configure the Grafana server to start at boot time: ```bash From a0e1f58815a1bed873fe645816be58a5fc8dd5f4 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:14:25 -0700 Subject: [PATCH 014/666] Update windows.md --- docs/sources/installation/windows.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 5dc87984512..dd8a2a6c3ee 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -31,6 +31,9 @@ on windows. Edit `custom.ini` and uncomment the `http_port` configuration option (`;` is the comment character in ini files) and change it to something like `8080` or similar. That port should not require extra Windows privileges. +Default login and password `admin`/ `admin` + + Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the command line. If you want to run Grafana as windows service, download [NSSM](https://nssm.cc/). It is very easy to add Grafana as a Windows From f34f5008baae06a4f260e27454157921872f1cc1 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:15:23 -0700 Subject: [PATCH 015/666] Update mac.md added local login info --- docs/sources/installation/mac.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index 12ff4adaab9..b09958a58ae 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -60,6 +60,8 @@ Then start Grafana using: brew services start grafana ``` +Default login and password `admin`/ `admin` + ### Configuration From c4308fedea8ee48973d39284e60562db1228fe6b Mon Sep 17 00:00:00 2001 From: Josh Dadak Date: Wed, 25 Jul 2018 14:02:36 +0100 Subject: [PATCH 016/666] Update Configuration.md Perhaps not worded as best it could be, however it would be good to include some information here about the importance of having your Grafana SERVER_ROOT_URL being the same URL listed in your Return URLs in Azure Application. Otherwise Azure Active Directory Auth will not work correctly resulting in an error page being displayed. --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2a799b044b3..8eee32bd616 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -629,7 +629,7 @@ allowed_organizations = team_ids = allowed_organizations = ``` - +Note: It's important to ensure that the SERVER_ROOT_URL in Grafana is set in your Azure Application Return URLs
## [auth.basic] From 87745e6e447f0f4acfd01d9d0984a03477c88c76 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 16:41:21 +0200 Subject: [PATCH 017/666] Explore: label selector for logging - query all available label keys for logs - query all values for each key - build cascader options with label values by key - lots of temporarily added conditions to reuse the promquery field --- public/app/containers/Explore/Explore.tsx | 1 + .../app/containers/Explore/PromQueryField.tsx | 82 +++++++++++++++++-- public/app/containers/Explore/QueryRows.tsx | 3 +- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 9620ac4f91b..bd52cd5ba05 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -564,6 +564,7 @@ export class Explore extends React.Component { onClickHintFix={this.onModifyQueries} onExecuteQuery={this.onSubmit} onRemoveQueryRow={this.onRemoveQueryRow} + supportsLogs={supportsLogs} />
{supportsGraph ? ( diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 1b3ff33971d..ee9496fb024 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -137,12 +137,14 @@ interface PromQueryFieldProps { onQueryChange?: (value: string, override?: boolean) => void; portalPrefix?: string; request?: (url: string) => any; + supportsLogs?: boolean; // To be removed after Logging gets its own query field } interface PromQueryFieldState { histogramMetrics: string[]; labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...] labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] + logLabelOptions: any[]; metrics: string[]; metricsByPrefix: CascaderOption[]; } @@ -171,16 +173,41 @@ class PromQueryField extends React.Component { + let query; + if (selectedOptions.length === 1) { + if (selectedOptions[0].children.length === 0) { + query = selectedOptions[0].value; + } else { + // Ignore click on group + return; + } + } else { + const key = selectedOptions[0].value; + const value = selectedOptions[1].value; + query = `{${key}="${value}"}`; + } + this.onChangeQuery(query, true); + }; + onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => { let query; if (selectedOptions.length === 1) { @@ -380,7 +407,8 @@ class PromQueryField extends React.Component this.fetchLabelValues(key))); @@ -409,6 +437,38 @@ class PromQueryField extends React.Component ({ label: value, value })), + }); + } + const labelValues = { [EMPTY_SELECTOR]: labelValuesByKey }; + this.setState({ labelKeys: labelKeysBySelector, labelValues, logLabelOptions }); + } catch (e) { + console.error(e); + } + } + async fetchLabelValues(key: string) { const url = `/api/v1/label/${key}/values`; try { @@ -463,8 +523,8 @@ class PromQueryField extends React.Component ({ label: hm, value: hm })); const metricsOptions = [ { label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions }, @@ -474,9 +534,15 @@ class PromQueryField extends React.Component
- - - + {supportsLogs ? ( + + + + ) : ( + + + + )}
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a7d91d59033..51adfa81c68 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -44,7 +44,7 @@ class QueryRow extends PureComponent { }; render() { - const { edited, history, query, queryError, queryHint, request } = this.props; + const { edited, history, query, queryError, queryHint, request, supportsLogs } = this.props; return (
@@ -58,6 +58,7 @@ class QueryRow extends PureComponent { onPressEnter={this.onPressEnter} onQueryChange={this.onChangeQuery} request={request} + supportsLogs={supportsLogs} />
From e62c083cf0da76d995ef71e51b5b1e68364ef6e2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Jun 2018 14:03:52 +0900 Subject: [PATCH 018/666] use series matchers to get label name/value --- .../datasource/prometheus/completer.ts | 18 +++++++++------- .../datasource/prometheus/datasource.ts | 8 +++++++ .../prometheus/specs/completer.test.ts | 21 ++++--------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 396a5fc1cd7..3cf4505e16a 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -113,7 +113,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -151,7 +151,7 @@ export class PromCompleter { var labelValues = this.transformToCompletions( _.uniq( result.map(r => { - return r.metric[labelName]; + return r[labelName]; }) ), 'label value' @@ -191,7 +191,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -233,7 +233,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -249,7 +249,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -276,9 +276,11 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - return this.datasource.performInstantQuery({ expr: query }, new Date().getTime() / 1000).then(response => { - this.labelQueryCache[expr] = response.data.data.result; - return response.data.data.result; + let range = this.datasource.getTimeRange(); + let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + return this.datasource.metadataRequest(url).then(response => { + this.labelQueryCache[expr] = response.data.data; + return response.data.data; }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 057bb55b3c3..c019fdc4aab 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,6 +629,14 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } + getTimeRange() { + let range = this.timeSrv.timeRange(); + return { + from: this.getPrometheusTime(range.from, false), + to: this.getPrometheusTime(range.to, true) + }; + } + getOriginalMetricName(labelData) { return this.resultTransformer.getOriginalMetricName(labelData); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 59fcc6592fb..201c8fcb0d7 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function() { +describe('Prometheus editor completer', function () { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -18,22 +18,9 @@ describe('Prometheus editor completer', function() { const backendSrv = {}; const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); - datasourceStub.performInstantQuery = jest.fn(() => - Promise.resolve({ - data: { - data: { - result: [ - { - metric: { - job: 'node', - instance: 'localhost:9100', - }, - }, - ], - }, - }, - }) - ); + datasourceStub.metadataRequest = jest.fn(() => + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); + datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From bf8840255c1e1236ddddfbcf00318e7a85229689 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 27 Jul 2018 11:39:00 +0900 Subject: [PATCH 019/666] Review feedback. --- public/app/plugins/datasource/prometheus/completer.ts | 6 +++--- public/app/plugins/datasource/prometheus/datasource.ts | 6 +++--- .../datasource/prometheus/specs/completer.test.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 3cf4505e16a..5719eeb5ac3 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -264,7 +264,7 @@ export class PromCompleter { return Promise.resolve([]); } - getLabelNameAndValueForExpression(expr, type) { + getLabelNameAndValueForExpression(expr: string, type: string): Promise { if (this.labelQueryCache[expr]) { return Promise.resolve(this.labelQueryCache[expr]); } @@ -276,8 +276,8 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - let range = this.datasource.getTimeRange(); - let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + const { start, end } = this.datasource.getTimeRange(); + const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; return this.datasource.metadataRequest(url).then(response => { this.labelQueryCache[expr] = response.data.data; return response.data.data; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c019fdc4aab..7f4b2fb1c98 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,11 +629,11 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } - getTimeRange() { + getTimeRange(): { start: number; end: number } { let range = this.timeSrv.timeRange(); return { - from: this.getPrometheusTime(range.from, false), - to: this.getPrometheusTime(range.to, true) + start: this.getPrometheusTime(range.from, false), + end: this.getPrometheusTime(range.to, true), }; } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 201c8fcb0d7..7a616c80c74 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function () { +describe('Prometheus editor completer', function() { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -19,8 +19,11 @@ describe('Prometheus editor completer', function () { const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); datasourceStub.metadataRequest = jest.fn(() => - Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); - datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100' } }] } }) + ); + datasourceStub.getTimeRange = jest.fn(() => { + return { start: 1514732400, end: 1514818800 }; + }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From cf58eea1dbbc40df3a2c5ca07a31e343e226c728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 13:24:36 +0200 Subject: [PATCH 020/666] redux: wip progress for using redux --- docker/blocks/openldap/ldap_dev.toml | 1 + .../containers/ServerStats/ServerStats.tsx | 52 -------------- public/app/core/actions/index.ts | 3 + public/app/core/actions/navModel.ts | 11 +++ .../core/components/PageHeader/PageHeader.tsx | 2 +- public/app/core/components/grafana_app.ts | 2 +- public/app/core/reducers/index.ts | 5 ++ public/app/core/reducers/navModel.ts | 64 +++++++++++++++++ .../server-stats}/ServerStats.test.tsx | 0 .../app/features/server-stats/ServerStats.tsx | 69 +++++++++++++++++++ .../__snapshots__/ServerStats.test.tsx.snap | 0 public/app/routes/ReactContainer.tsx | 10 ++- public/app/routes/routes.ts | 2 +- public/app/store/nav/actions.ts | 30 -------- public/app/store/nav/reducers.ts | 30 -------- .../app/{store => stores}/configureStore.ts | 4 +- public/app/types/container.ts | 6 ++ public/app/types/index.ts | 4 ++ public/app/types/navModel.ts | 19 +++++ 19 files changed, 194 insertions(+), 120 deletions(-) delete mode 100644 public/app/containers/ServerStats/ServerStats.tsx create mode 100644 public/app/core/actions/index.ts create mode 100644 public/app/core/actions/navModel.ts create mode 100644 public/app/core/reducers/index.ts create mode 100644 public/app/core/reducers/navModel.ts rename public/app/{containers/ServerStats => features/server-stats}/ServerStats.test.tsx (100%) create mode 100644 public/app/features/server-stats/ServerStats.tsx rename public/app/{containers/ServerStats => features/server-stats}/__snapshots__/ServerStats.test.tsx.snap (100%) delete mode 100644 public/app/store/nav/actions.ts delete mode 100644 public/app/store/nav/reducers.ts rename public/app/{store => stores}/configureStore.ts (86%) create mode 100644 public/app/types/container.ts create mode 100644 public/app/types/index.ts create mode 100644 public/app/types/navModel.ts diff --git a/docker/blocks/openldap/ldap_dev.toml b/docker/blocks/openldap/ldap_dev.toml index e79771b57de..8767ff3c64a 100644 --- a/docker/blocks/openldap/ldap_dev.toml +++ b/docker/blocks/openldap/ldap_dev.toml @@ -72,6 +72,7 @@ email = "email" [[servers.group_mappings]] group_dn = "cn=admins,ou=groups,dc=grafana,dc=org" org_role = "Admin" +grafana_admin = true # The Grafana organization database id, optional, if left out the default org (id 1) will be used # org_id = 1 diff --git a/public/app/containers/ServerStats/ServerStats.tsx b/public/app/containers/ServerStats/ServerStats.tsx deleted file mode 100644 index fe3ef0ecfd1..00000000000 --- a/public/app/containers/ServerStats/ServerStats.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { store } from 'app/store/configureStore'; -import { setNav } from 'app/store/nav/actions'; -import ContainerProps from 'app/containers/ContainerProps'; - -@inject('nav', 'serverStats') -@observer -export class ServerStats extends React.Component { - constructor(props) { - super(props); - const { nav, serverStats } = this.props; - - nav.load('cfg', 'admin', 'server-stats'); - serverStats.load(); - - store.dispatch(setNav('new', { asd: 'tasd' })); - } - - render() { - const { nav, serverStats } = this.props; - return ( -
- -
- - - - - - - - {serverStats.stats.map(StatItem)} -
NameValue
-
-
- ); - } -} - -function StatItem(stat) { - return ( - - {stat.name} - {stat.value} - - ); -} - -export default hot(module)(ServerStats); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts new file mode 100644 index 00000000000..3c23dbbbe54 --- /dev/null +++ b/public/app/core/actions/index.ts @@ -0,0 +1,3 @@ +import { initNav } from './navModel'; + +export { initNav }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts new file mode 100644 index 00000000000..048afd4f8ff --- /dev/null +++ b/public/app/core/actions/navModel.ts @@ -0,0 +1,11 @@ +export type Action = InitNavModelAction; + +export interface InitNavModelAction { + type: 'INIT_NAV_MODEL'; + args: string[]; +} + +export const initNav = (...args: string[]): InitNavModelAction => ({ + type: 'INIT_NAV_MODEL', + args: args, +}); diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index b7bef2495bb..9feddde68ce 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { NavModel, NavModelItem } from '../../nav_model_srv'; +import { NavModel, NavModelItem } from 'app/types'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { toJS } from 'mobx'; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index c1cd0e2b5f2..085f0db0a6d 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -10,7 +10,7 @@ import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { configureStore } from 'app/store/configureStore'; +import { configureStore } from 'app/stores/configureStore'; export class GrafanaCtrl { /** @ngInject */ diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts new file mode 100644 index 00000000000..0779111c16e --- /dev/null +++ b/public/app/core/reducers/index.ts @@ -0,0 +1,5 @@ +import navModel from './navModel'; + +export default { + navModel, +}; diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts new file mode 100644 index 00000000000..c00441c4881 --- /dev/null +++ b/public/app/core/reducers/navModel.ts @@ -0,0 +1,64 @@ +import { Action } from 'app/core/actions/navModel'; +import { NavModel, NavModelItem } from 'app/types'; +import config from 'app/core/config'; + +function getNotFoundModel(): NavModel { + var node: NavModelItem = { + id: 'not-found', + text: 'Page not found', + icon: 'fa fa-fw fa-warning', + subTitle: '404 Error', + url: 'not-found', + }; + + return { + breadcrumbs: [node], + node: node, + main: node, + }; +} + +export const initialState: NavModel = getNotFoundModel(); + +const navModelReducer = (state = initialState, action: Action): NavModel => { + switch (action.type) { + case 'INIT_NAV_MODEL': { + let children = config.bootData.navTree as NavModelItem[]; + let main, node; + const parents = []; + + for (const id of action.args) { + node = children.find(el => el.id === id); + + if (!node) { + throw new Error(`NavItem with id ${id} not found`); + } + + children = node.children; + parents.push(node); + } + + main = parents[parents.length - 2]; + + if (main.children) { + for (const item of main.children) { + item.active = false; + + if (item.url === node.url) { + item.active = true; + } + } + } + + return { + main: main, + node: node, + breadcrumbs: [], + }; + } + } + + return state; +}; + +export default navModelReducer; diff --git a/public/app/containers/ServerStats/ServerStats.test.tsx b/public/app/features/server-stats/ServerStats.test.tsx similarity index 100% rename from public/app/containers/ServerStats/ServerStats.test.tsx rename to public/app/features/server-stats/ServerStats.test.tsx diff --git a/public/app/features/server-stats/ServerStats.tsx b/public/app/features/server-stats/ServerStats.tsx new file mode 100644 index 00000000000..b499fb725a8 --- /dev/null +++ b/public/app/features/server-stats/ServerStats.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { initNav } from 'app/core/actions'; +import { ContainerProps } from 'app/types'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; + +interface Props extends ContainerProps {} + +export class ServerStats extends React.Component { + constructor(props) { + super(props); + + this.props.initNav('cfg', 'admin', 'server-stats'); + // const { nav, serverStats } = this.props; + // + // nav.load('cfg', 'admin', 'server-stats'); + // serverStats.load(); + // + // store.dispatch(setNav('new', { asd: 'tasd' })); + } + + render() { + const { navModel } = this.props; + console.log('render', navModel); + return ( +
+ +

aasd

+
+ ); + // const { nav, serverStats } = this.props; + // return ( + //
+ // + //
+ // + // + // + // + // + // + // + // {serverStats.stats.map(StatItem)} + //
NameValue
+ //
+ //
+ // ); + } +} + +function StatItem(stat) { + return ( + + {stat.name} + {stat.value} + + ); +} + +const mapStateToProps = state => ({ + navModel: state.navModel, +}); + +const mapDispatchToProps = { + initNav, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); diff --git a/public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap similarity index 100% rename from public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap rename to public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index b161a5e7a87..3ed534da587 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -1,18 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'mobx-react'; +import { Provider as ReduxProvider } from 'react-redux'; import coreModule from 'app/core/core_module'; import { store } from 'app/stores/store'; +import { store as reduxStore } from 'app/stores/configureStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ContextSrv } from 'app/core/services/context_srv'; function WrapInProvider(store, Component, props) { return ( - - - + + + + + ); } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index d12711aca5b..7fcab26645f 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,7 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/containers/ServerStats/ServerStats'; +import ServerStats from 'app/features/server-stats/ServerStats'; import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; diff --git a/public/app/store/nav/actions.ts b/public/app/store/nav/actions.ts deleted file mode 100644 index eca99cc2b90..00000000000 --- a/public/app/store/nav/actions.ts +++ /dev/null @@ -1,30 +0,0 @@ -// -// Only test actions to test redux & typescript -// - -export enum ActionTypes { - SET_NAV = 'SET_NAV', - SET_QUERY = 'SET_QUERY', -} - -export interface SetNavAction { - type: ActionTypes.SET_NAV; - payload: { - path: string; - query: object; - }; -} - -export interface SetQueryAction { - type: ActionTypes.SET_QUERY; - payload: { - query: object; - }; -} - -export type Action = SetNavAction | SetQueryAction; - -export const setNav = (path: string, query: object): SetNavAction => ({ - type: ActionTypes.SET_NAV, - payload: { path: path, query: query }, -}); diff --git a/public/app/store/nav/reducers.ts b/public/app/store/nav/reducers.ts deleted file mode 100644 index 6e9d6e713a0..00000000000 --- a/public/app/store/nav/reducers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Action, ActionTypes } from './actions'; - -export interface NavState { - path: string; - query: object; -} - -const initialState: NavState = { - path: '/test', - query: {}, -}; - -export const navReducer = (state: NavState = initialState, action: Action): NavState => { - switch (action.type) { - case ActionTypes.SET_NAV: { - return { ...state, path: action.payload.path, query: action.payload.query }; - } - - case ActionTypes.SET_QUERY: { - return { - ...state, - query: action.payload.query, - }; - } - - default: { - return state; - } - } -}; diff --git a/public/app/store/configureStore.ts b/public/app/stores/configureStore.ts similarity index 86% rename from public/app/store/configureStore.ts rename to public/app/stores/configureStore.ts index a0dfe576ed6..3a7d16da76d 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -1,10 +1,10 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; -import { navReducer } from './nav/reducers'; +import sharedReducers from 'app/core/reducers'; const rootReducer = combineReducers({ - nav: navReducer, + ...sharedReducers }); export let store; diff --git a/public/app/types/container.ts b/public/app/types/container.ts new file mode 100644 index 00000000000..174bc0c8460 --- /dev/null +++ b/public/app/types/container.ts @@ -0,0 +1,6 @@ +import { NavModel } from './navModel'; + +export interface ContainerProps { + navModel: NavModel; + initNav: (...args: string[]) => void; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts new file mode 100644 index 00000000000..43d921e3964 --- /dev/null +++ b/public/app/types/index.ts @@ -0,0 +1,4 @@ +import { NavModel, NavModelItem } from './navModel'; +import { ContainerProps } from './container'; + +export { NavModel, NavModelItem, ContainerProps }; diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts new file mode 100644 index 00000000000..e1a4265847c --- /dev/null +++ b/public/app/types/navModel.ts @@ -0,0 +1,19 @@ +export interface NavModelItem { + text: string; + url: string; + subTitle?: string; + icon?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children?: NavModelItem[]; + target?: string; +} + +export interface NavModel { + breadcrumbs: NavModelItem[]; + main: NavModelItem; + node: NavModelItem; +} From d68007fde37665ff847645bcf22191c5ec7c4fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 14:38:23 +0200 Subject: [PATCH 021/666] wip: redux --- .../app/features/server-stats/ServerStats.tsx | 65 +++++++++++-------- public/app/features/server-stats/api.ts | 26 ++++++++ 2 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 public/app/features/server-stats/api.ts diff --git a/public/app/features/server-stats/ServerStats.tsx b/public/app/features/server-stats/ServerStats.tsx index b499fb725a8..da1fb6e76f7 100644 --- a/public/app/features/server-stats/ServerStats.tsx +++ b/public/app/features/server-stats/ServerStats.tsx @@ -3,53 +3,61 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { initNav } from 'app/core/actions'; import { ContainerProps } from 'app/types'; +import { getServerStats, ServerStat } from './api'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -interface Props extends ContainerProps {} +interface Props extends ContainerProps { + getServerStats: () => Promise; +} -export class ServerStats extends React.Component { +interface State { + stats: ServerStat[]; +} + +export class ServerStats extends React.Component { constructor(props) { super(props); + this.state = { + stats: [], + }; + this.props.initNav('cfg', 'admin', 'server-stats'); - // const { nav, serverStats } = this.props; - // - // nav.load('cfg', 'admin', 'server-stats'); - // serverStats.load(); - // - // store.dispatch(setNav('new', { asd: 'tasd' })); + } + + async componentDidMount() { + try { + const stats = await this.props.getServerStats(); + this.setState({ stats }); + } catch (error) { + console.error(error); + } } render() { const { navModel } = this.props; - console.log('render', navModel); + const { stats } = this.state; + return (
-

aasd

+
+ + + + + + + + {stats.map(StatItem)} +
NameValue
+
); - // const { nav, serverStats } = this.props; - // return ( - //
- // - //
- // - // - // - // - // - // - // - // {serverStats.stats.map(StatItem)} - //
NameValue
- //
- //
- // ); } } -function StatItem(stat) { +function StatItem(stat: ServerStat) { return ( {stat.name} @@ -60,6 +68,7 @@ function StatItem(stat) { const mapStateToProps = state => ({ navModel: state.navModel, + getServerStats: getServerStats, }); const mapDispatchToProps = { diff --git a/public/app/features/server-stats/api.ts b/public/app/features/server-stats/api.ts new file mode 100644 index 00000000000..888cfd4f58f --- /dev/null +++ b/public/app/features/server-stats/api.ts @@ -0,0 +1,26 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; + +export interface ServerStat { + name: string; + value: string; +} + +export const getServerStats = async (): Promise => { + try { + const res = await getBackendSrv().get('api/admin/stats'); + return [ + { name: 'Total users', value: res.users }, + { name: 'Total dashboards', value: res.dashboards }, + { name: 'Active users (seen last 30 days)', value: res.activeUsers }, + { name: 'Total orgs', value: res.orgs }, + { name: 'Total playlists', value: res.playlists }, + { name: 'Total snapshots', value: res.snapshots }, + { name: 'Total dashboard tags', value: res.tags }, + { name: 'Total starred dashboards', value: res.stars }, + { name: 'Total alerts', value: res.alerts }, + ]; + } catch (error) { + console.error(error); + throw error; + } +}; From 593cc5380f6ac4fe14c8fc1c45daccfb5ffe3dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 09:42:32 -0700 Subject: [PATCH 022/666] wip: redux refactor --- public/app/features/alerting/apis/index.ts | 52 ++++++++++++ .../containers}/AlertRuleList.test.tsx | 0 .../alerting/containers}/AlertRuleList.tsx | 83 +++++++++++++------ .../__snapshots__/AlertRuleList.test.tsx.snap | 0 .../ServerStats.test.tsx | 0 .../ServerStats.tsx | 0 .../__snapshots__/ServerStats.test.tsx.snap | 0 .../{server-stats => serverStats}/api.ts | 0 public/app/routes/routes.ts | 4 +- 9 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 public/app/features/alerting/apis/index.ts rename public/app/{containers/AlertRuleList => features/alerting/containers}/AlertRuleList.test.tsx (100%) rename public/app/{containers/AlertRuleList => features/alerting/containers}/AlertRuleList.tsx (73%) rename public/app/{containers/AlertRuleList => features/alerting/containers}/__snapshots__/AlertRuleList.test.tsx.snap (100%) rename public/app/features/{server-stats => serverStats}/ServerStats.test.tsx (100%) rename public/app/features/{server-stats => serverStats}/ServerStats.tsx (100%) rename public/app/features/{server-stats => serverStats}/__snapshots__/ServerStats.test.tsx.snap (100%) rename public/app/features/{server-stats => serverStats}/api.ts (100%) diff --git a/public/app/features/alerting/apis/index.ts b/public/app/features/alerting/apis/index.ts new file mode 100644 index 00000000000..ebfdbd34024 --- /dev/null +++ b/public/app/features/alerting/apis/index.ts @@ -0,0 +1,52 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import alertDef from '../alert_def'; +import moment from 'moment'; + +export interface AlertRule { + id: number; + dashboardId: number; + panelId: number; + name: string; + state: string; + stateText: string; + stateIcon: string; + stateClass: string; + stateAge: string; + info?: string; + url: string; +} + +export function setStateFields(rule, state) { + const stateModel = alertDef.getStateDisplayModel(state); + rule.state = state; + rule.stateText = stateModel.text; + rule.stateIcon = stateModel.iconClass; + rule.stateClass = stateModel.stateClass; + rule.stateAge = moment(rule.newStateDate) + .fromNow() + .replace(' ago', ''); +} + +export const getAlertRules = async (): Promise => { + try { + const rules = await getBackendSrv().get('/api/alerts', {}); + + for (const rule of rules) { + setStateFields(rule, rule.state); + + if (rule.state !== 'paused') { + if (rule.executionError) { + rule.info = 'Execution Error: ' + rule.executionError; + } + if (rule.evalData && rule.evalData.noData) { + rule.info = 'Query returned no data'; + } + } + } + + return rules; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx b/public/app/features/alerting/containers/AlertRuleList.test.tsx similarity index 100% rename from public/app/containers/AlertRuleList/AlertRuleList.test.tsx rename to public/app/features/alerting/containers/AlertRuleList.test.tsx diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/features/alerting/containers/AlertRuleList.tsx similarity index 73% rename from public/app/containers/AlertRuleList/AlertRuleList.tsx rename to public/app/features/alerting/containers/AlertRuleList.tsx index 668136dee6f..665b1508e3e 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/features/alerting/containers/AlertRuleList.tsx @@ -1,16 +1,23 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import classNames from 'classnames'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { AlertRule } from 'app/stores/AlertListStore/AlertListStore'; import appEvents from 'app/core/app_events'; -import ContainerProps from 'app/containers/ContainerProps'; import Highlighter from 'react-highlight-words'; +import { initNav } from 'app/core/actions'; +import { ContainerProps } from 'app/types'; +import { getAlertRules, AlertRule } from '../apis'; -@inject('view', 'nav', 'alertList') -@observer -export class AlertRuleList extends React.Component { +interface Props extends ContainerProps {} + +interface State { + rules: AlertRule[]; + search: string; + stateFilter: string; +} + +export class AlertRuleList extends PureComponent { stateFilters = [ { text: 'All', value: 'all' }, { text: 'OK', value: 'ok' }, @@ -23,19 +30,35 @@ export class AlertRuleList extends React.Component { constructor(props) { super(props); - this.props.nav.load('alerting', 'alert-list'); + this.state = { + rules: [], + search: '', + stateFilter: '', + }; + + this.props.initNav('alerting', 'alert-list'); + } + + componentDidMount() { this.fetchRules(); } onStateFilterChanged = evt => { - this.props.view.updateQuery({ state: evt.target.value }); - this.fetchRules(); + // this.props.view.updateQuery({ state: evt.target.value }); + // this.fetchRules(); }; - fetchRules() { - this.props.alertList.loadRules({ - state: this.props.view.query.get('state') || 'all', - }); + async fetchRules() { + try { + const rules = await getAlertRules(); + this.setState({ rules }); + } catch (error) { + console.error(error); + } + + // this.props.alertList.loadRules({ + // state: this.props.view.query.get('state') || 'all', + // }); } onOpenHowTo = () => { @@ -47,15 +70,16 @@ export class AlertRuleList extends React.Component { }; onSearchQueryChange = evt => { - this.props.alertList.setSearchQuery(evt.target.value); + // this.props.alertList.setSearchQuery(evt.target.value); }; render() { - const { nav, alertList } = this.props; + const { navModel } = this.props; + const { rules, search, stateFilter } = this.state; return (
- +
@@ -64,7 +88,7 @@ export class AlertRuleList extends React.Component { type="text" className="gf-form-input" placeholder="Search alerts" - value={alertList.search} + value={search} onChange={this.onSearchQueryChange} /> @@ -74,7 +98,7 @@ export class AlertRuleList extends React.Component {
- {this.stateFilters.map(AlertStateFilterOption)}
@@ -89,8 +113,8 @@ export class AlertRuleList extends React.Component {
    - {alertList.filteredRules.map(rule => ( - + {rules.map(rule => ( + ))}
@@ -113,10 +137,9 @@ export interface AlertRuleItemProps { search: string; } -@observer export class AlertRuleItem extends React.Component { toggleState = () => { - this.props.rule.togglePaused(); + // this.props.rule.togglePaused(); }; renderText(text: string) { @@ -134,8 +157,8 @@ export class AlertRuleItem extends React.Component { const stateClass = classNames({ fa: true, - 'fa-play': rule.isPaused, - 'fa-pause': !rule.isPaused, + 'fa-play': rule.state === 'paused', + 'fa-pause': rule.state !== 'paused', }); const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; @@ -175,4 +198,12 @@ export class AlertRuleItem extends React.Component { } } -export default hot(module)(AlertRuleList); +const mapStateToProps = state => ({ + navModel: state.navModel, +}); + +const mapDispatchToProps = { + initNav, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap similarity index 100% rename from public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap rename to public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/public/app/features/server-stats/ServerStats.test.tsx b/public/app/features/serverStats/ServerStats.test.tsx similarity index 100% rename from public/app/features/server-stats/ServerStats.test.tsx rename to public/app/features/serverStats/ServerStats.test.tsx diff --git a/public/app/features/server-stats/ServerStats.tsx b/public/app/features/serverStats/ServerStats.tsx similarity index 100% rename from public/app/features/server-stats/ServerStats.tsx rename to public/app/features/serverStats/ServerStats.tsx diff --git a/public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap similarity index 100% rename from public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap rename to public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/features/server-stats/api.ts b/public/app/features/serverStats/api.ts similarity index 100% rename from public/app/features/server-stats/api.ts rename to public/app/features/serverStats/api.ts diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 7fcab26645f..cf45176ecf7 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,8 +1,8 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/features/server-stats/ServerStats'; -import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; +import ServerStats from 'app/features/serverStats/ServerStats'; +import AlertRuleList from 'app/features/alerting/containers/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/containers/Teams/TeamPages'; From 2c85e44ab785681e35bdb855ff650cb148de84f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 09:49:02 -0700 Subject: [PATCH 023/666] wip: moveing things around --- .../features/serverStats/ServerStats.test.tsx | 30 ---- .../app/features/serverStats/ServerStats.tsx | 78 -------- .../__snapshots__/ServerStats.test.tsx.snap | 170 ------------------ public/app/features/serverStats/api.ts | 26 --- public/app/routes/routes.ts | 2 +- 5 files changed, 1 insertion(+), 305 deletions(-) delete mode 100644 public/app/features/serverStats/ServerStats.test.tsx delete mode 100644 public/app/features/serverStats/ServerStats.tsx delete mode 100644 public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap delete mode 100644 public/app/features/serverStats/api.ts diff --git a/public/app/features/serverStats/ServerStats.test.tsx b/public/app/features/serverStats/ServerStats.test.tsx deleted file mode 100644 index a329a47527d..00000000000 --- a/public/app/features/serverStats/ServerStats.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { ServerStats } from './ServerStats'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; - -describe('ServerStats', () => { - it('Should render table with stats', done => { - backendSrv.get.mockReturnValue( - Promise.resolve({ - dashboards: 10, - }) - ); - - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('cfg', 'admin', 'server-stats'), - } - ); - - const page = renderer.create(); - - setTimeout(() => { - expect(page.toJSON()).toMatchSnapshot(); - done(); - }); - }); -}); diff --git a/public/app/features/serverStats/ServerStats.tsx b/public/app/features/serverStats/ServerStats.tsx deleted file mode 100644 index da1fb6e76f7..00000000000 --- a/public/app/features/serverStats/ServerStats.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { connect } from 'react-redux'; -import { initNav } from 'app/core/actions'; -import { ContainerProps } from 'app/types'; -import { getServerStats, ServerStat } from './api'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; - -interface Props extends ContainerProps { - getServerStats: () => Promise; -} - -interface State { - stats: ServerStat[]; -} - -export class ServerStats extends React.Component { - constructor(props) { - super(props); - - this.state = { - stats: [], - }; - - this.props.initNav('cfg', 'admin', 'server-stats'); - } - - async componentDidMount() { - try { - const stats = await this.props.getServerStats(); - this.setState({ stats }); - } catch (error) { - console.error(error); - } - } - - render() { - const { navModel } = this.props; - const { stats } = this.state; - - return ( -
- -
- - - - - - - - {stats.map(StatItem)} -
NameValue
-
-
- ); - } -} - -function StatItem(stat: ServerStat) { - return ( - - {stat.name} - {stat.value} - - ); -} - -const mapStateToProps = state => ({ - navModel: state.navModel, - getServerStats: getServerStats, -}); - -const mapDispatchToProps = { - initNav, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); diff --git a/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap deleted file mode 100644 index eac793ca2ca..00000000000 --- a/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap +++ /dev/null @@ -1,170 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ServerStats Should render table with stats 1`] = ` -
-
-
-
-
- - - - -
-

- admin-Text -

- -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Name - - Value -
- Total dashboards - - 10 -
- Total users - - 0 -
- Active users (seen last 30 days) - - 0 -
- Total orgs - - 0 -
- Total playlists - - 0 -
- Total snapshots - - 0 -
- Total dashboard tags - - 0 -
- Total starred dashboards - - 0 -
- Total alerts - - 0 -
-
-
-`; diff --git a/public/app/features/serverStats/api.ts b/public/app/features/serverStats/api.ts deleted file mode 100644 index 888cfd4f58f..00000000000 --- a/public/app/features/serverStats/api.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; - -export interface ServerStat { - name: string; - value: string; -} - -export const getServerStats = async (): Promise => { - try { - const res = await getBackendSrv().get('api/admin/stats'); - return [ - { name: 'Total users', value: res.users }, - { name: 'Total dashboards', value: res.dashboards }, - { name: 'Active users (seen last 30 days)', value: res.activeUsers }, - { name: 'Total orgs', value: res.orgs }, - { name: 'Total playlists', value: res.playlists }, - { name: 'Total snapshots', value: res.snapshots }, - { name: 'Total dashboard tags', value: res.tags }, - { name: 'Total starred dashboards', value: res.stars }, - { name: 'Total alerts', value: res.alerts }, - ]; - } catch (error) { - console.error(error); - throw error; - } -}; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index cf45176ecf7..7b1e223afe5 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,7 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/features/serverStats/ServerStats'; +import ServerStats from 'app/features/admin/containers/ServerStats'; import AlertRuleList from 'app/features/alerting/containers/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; From 6efe9da10f99448740609845550211c361117086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 09:49:32 -0700 Subject: [PATCH 024/666] wip: moving things around --- public/app/features/admin/apis/index.ts | 26 +++ .../admin/containers/ServerStats.test.tsx | 30 ++++ .../features/admin/containers/ServerStats.tsx | 78 ++++++++ .../__snapshots__/ServerStats.test.tsx.snap | 170 ++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 public/app/features/admin/apis/index.ts create mode 100644 public/app/features/admin/containers/ServerStats.test.tsx create mode 100644 public/app/features/admin/containers/ServerStats.tsx create mode 100644 public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/features/admin/apis/index.ts b/public/app/features/admin/apis/index.ts new file mode 100644 index 00000000000..888cfd4f58f --- /dev/null +++ b/public/app/features/admin/apis/index.ts @@ -0,0 +1,26 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; + +export interface ServerStat { + name: string; + value: string; +} + +export const getServerStats = async (): Promise => { + try { + const res = await getBackendSrv().get('api/admin/stats'); + return [ + { name: 'Total users', value: res.users }, + { name: 'Total dashboards', value: res.dashboards }, + { name: 'Active users (seen last 30 days)', value: res.activeUsers }, + { name: 'Total orgs', value: res.orgs }, + { name: 'Total playlists', value: res.playlists }, + { name: 'Total snapshots', value: res.snapshots }, + { name: 'Total dashboard tags', value: res.tags }, + { name: 'Total starred dashboards', value: res.stars }, + { name: 'Total alerts', value: res.alerts }, + ]; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/containers/ServerStats.test.tsx new file mode 100644 index 00000000000..a329a47527d --- /dev/null +++ b/public/app/features/admin/containers/ServerStats.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { ServerStats } from './ServerStats'; +import { RootStore } from 'app/stores/RootStore/RootStore'; +import { backendSrv, createNavTree } from 'test/mocks/common'; + +describe('ServerStats', () => { + it('Should render table with stats', done => { + backendSrv.get.mockReturnValue( + Promise.resolve({ + dashboards: 10, + }) + ); + + const store = RootStore.create( + {}, + { + backendSrv: backendSrv, + navTree: createNavTree('cfg', 'admin', 'server-stats'), + } + ); + + const page = renderer.create(); + + setTimeout(() => { + expect(page.toJSON()).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx new file mode 100644 index 00000000000..7e96dcf4e0e --- /dev/null +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { initNav } from 'app/core/actions'; +import { ContainerProps } from 'app/types'; +import { getServerStats, ServerStat } from '../apis'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; + +interface Props extends ContainerProps { + getServerStats: () => Promise; +} + +interface State { + stats: ServerStat[]; +} + +export class ServerStats extends React.Component { + constructor(props) { + super(props); + + this.state = { + stats: [], + }; + + this.props.initNav('cfg', 'admin', 'server-stats'); + } + + async componentDidMount() { + try { + const stats = await this.props.getServerStats(); + this.setState({ stats }); + } catch (error) { + console.error(error); + } + } + + render() { + const { navModel } = this.props; + const { stats } = this.state; + + return ( +
+ +
+ + + + + + + + {stats.map(StatItem)} +
NameValue
+
+
+ ); + } +} + +function StatItem(stat: ServerStat) { + return ( + + {stat.name} + {stat.value} + + ); +} + +const mapStateToProps = state => ({ + navModel: state.navModel, + getServerStats: getServerStats, +}); + +const mapDispatchToProps = { + initNav, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); diff --git a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap new file mode 100644 index 00000000000..eac793ca2ca --- /dev/null +++ b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServerStats Should render table with stats 1`] = ` +
+
+
+
+
+ + + + +
+

+ admin-Text +

+ +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Value +
+ Total dashboards + + 10 +
+ Total users + + 0 +
+ Active users (seen last 30 days) + + 0 +
+ Total orgs + + 0 +
+ Total playlists + + 0 +
+ Total snapshots + + 0 +
+ Total dashboard tags + + 0 +
+ Total starred dashboards + + 0 +
+ Total alerts + + 0 +
+
+
+`; From de456f8b7356fe0de98d1d8a86f25deccba6627f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 13:16:20 -0700 Subject: [PATCH 025/666] wip: solid progress on redux -> angular location bridge update --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/location.ts | 13 +++++++ public/app/core/reducers/index.ts | 2 ++ public/app/core/reducers/location.ts | 35 +++++++++++++++++++ public/app/core/services/bridge_srv.ts | 33 +++++++++++++++++ .../alerting/containers/AlertRuleList.tsx | 15 ++++---- public/app/types/index.ts | 3 +- public/app/types/location.ts | 15 ++++++++ 8 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 public/app/core/actions/location.ts create mode 100644 public/app/core/reducers/location.ts create mode 100644 public/app/types/location.ts diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 3c23dbbbe54..7a965f82dd1 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { initNav } from './navModel'; +import { updateLocation } from './location'; -export { initNav }; +export { initNav, updateLocation }; diff --git a/public/app/core/actions/location.ts b/public/app/core/actions/location.ts new file mode 100644 index 00000000000..6f7ac67363e --- /dev/null +++ b/public/app/core/actions/location.ts @@ -0,0 +1,13 @@ +import { LocationUpdate } from 'app/types'; + +export type Action = UpdateLocationAction; + +export interface UpdateLocationAction { + type: 'UPDATE_LOCATION'; + payload: LocationUpdate; +} + +export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({ + type: 'UPDATE_LOCATION', + payload: location, +}); diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index 0779111c16e..98f796981e4 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,5 +1,7 @@ import navModel from './navModel'; +import location from './location'; export default { navModel, + location, }; diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts new file mode 100644 index 00000000000..5676c82844a --- /dev/null +++ b/public/app/core/reducers/location.ts @@ -0,0 +1,35 @@ +import { Action } from 'app/core/actions/location'; +import { LocationState, UrlQueryMap } from 'app/types'; +import { toUrlParams } from 'app/core/utils/url'; + +export const initialState: LocationState = { + url: '', + path: '', + query: {}, + routeParams: {}, +}; + +function renderUrl(path: string, query: UrlQueryMap): string { + if (Object.keys(query).length > 0) { + path += '?' + toUrlParams(query); + } + return path; +} + +const routerReducer = (state = initialState, action: Action): LocationState => { + switch (action.type) { + case 'UPDATE_LOCATION': { + const { path, query, routeParams } = action.payload; + return { + url: renderUrl(path || state.path, query), + path: path || state.path, + query: query || state.query, + routeParams: routeParams || state.routeParams, + }; + } + } + + return state; +}; + +export default routerReducer; diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index bdc2976a94c..29326794ac6 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -1,8 +1,10 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { store } from 'app/stores/store'; +import { store as reduxStore } from 'app/stores/configureStore'; import { reaction } from 'mobx'; import locationUtil from 'app/core/utils/location_util'; +import { updateLocation } from 'app/core/actions'; // Services that handles angular -> mobx store sync & other react <-> angular sync export class BridgeSrv { @@ -19,12 +21,30 @@ export class BridgeSrv { if (store.view.currentUrl !== angularUrl) { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } + const state = reduxStore.getState(); + if (state.location.url !== angularUrl) { + reduxStore.dispatch( + updateLocation({ + path: this.$location.path(), + query: this.$location.search(), + routeParams: this.$route.current.params, + }) + ); + } }); this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); + reduxStore.dispatch( + updateLocation({ + path: this.$location.path(), + query: this.$location.search(), + routeParams: this.$route.current.params, + }) + ); }); + // listen for mobx store changes and update angular reaction( () => store.view.currentUrl, currentUrl => { @@ -39,6 +59,19 @@ export class BridgeSrv { } ); + // Listen for changes in redux location -> update angular location + reduxStore.subscribe(() => { + const state = reduxStore.getState(); + const angularUrl = this.$location.url(); + const url = locationUtil.stripBaseFromUrl(state.location.url); + if (angularUrl !== url) { + this.$timeout(() => { + this.$location.url(url); + }); + console.log('store updating angular $location.url', url); + } + }); + appEvents.on('location-change', payload => { const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href); if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { diff --git a/public/app/features/alerting/containers/AlertRuleList.tsx b/public/app/features/alerting/containers/AlertRuleList.tsx index 665b1508e3e..3c64f490db4 100644 --- a/public/app/features/alerting/containers/AlertRuleList.tsx +++ b/public/app/features/alerting/containers/AlertRuleList.tsx @@ -5,11 +5,13 @@ import classNames from 'classnames'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; -import { initNav } from 'app/core/actions'; +import { initNav, updateLocation } from 'app/core/actions'; import { ContainerProps } from 'app/types'; import { getAlertRules, AlertRule } from '../apis'; -interface Props extends ContainerProps {} +interface Props extends ContainerProps { + updateLocation: typeof updateLocation; +} interface State { rules: AlertRule[]; @@ -44,7 +46,9 @@ export class AlertRuleList extends PureComponent { } onStateFilterChanged = evt => { - // this.props.view.updateQuery({ state: evt.target.value }); + this.props.updateLocation({ + query: { state: evt.target.value }, + }); // this.fetchRules(); }; @@ -113,9 +117,7 @@ export class AlertRuleList extends PureComponent {
    - {rules.map(rule => ( - - ))} + {rules.map(rule => )}
@@ -204,6 +206,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = { initNav, + updateLocation, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 43d921e3964..9cb5ee85c04 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,4 +1,5 @@ import { NavModel, NavModelItem } from './navModel'; import { ContainerProps } from './container'; +import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; -export { NavModel, NavModelItem, ContainerProps }; +export { NavModel, NavModelItem, ContainerProps, LocationState, LocationUpdate, UrlQueryValue, UrlQueryMap }; diff --git a/public/app/types/location.ts b/public/app/types/location.ts new file mode 100644 index 00000000000..4a7f51523a7 --- /dev/null +++ b/public/app/types/location.ts @@ -0,0 +1,15 @@ +export interface LocationUpdate { + path?: string; + query?: UrlQueryMap; + routeParams?: UrlQueryMap; +} + +export interface LocationState { + url: string; + path: string; + query: UrlQueryMap; + routeParams: UrlQueryMap; +} + +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; +export type UrlQueryMap = { [s: string]: UrlQueryValue }; From 2ac202b22f4d2c6f6e076b851f34be155f64c8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 07:11:21 -0700 Subject: [PATCH 026/666] moving things around --- public/app/core/reducers/navModel.ts | 2 -- .../admin/containers/ServerStats.test.tsx | 22 +++++--------- .../features/admin/containers/ServerStats.tsx | 4 +-- .../{containers => }/AlertRuleList.test.tsx | 0 .../{containers => }/AlertRuleList.tsx | 2 +- .../{alert_tab_ctrl.ts => AlertTabCtrl.ts} | 4 +-- ..._edit_ctrl.ts => NotificationsEditCtrl.ts} | 0 ..._list_ctrl.ts => NotificationsListCtrl.ts} | 0 .../__snapshots__/AlertRuleList.test.tsx.snap | 0 public/app/features/alerting/all.ts | 2 -- .../ThresholdMapper.test.ts} | 2 +- .../ThresholdMapper.ts} | 0 .../{alert_def.ts => state/alertDef.ts} | 0 .../alerting/{apis/index.ts => state/apis.ts} | 2 +- public/app/features/all.ts | 3 +- .../annotations/annotation_tooltip.ts | 2 +- public/app/plugins/panel/alertlist/module.ts | 2 +- public/app/plugins/sdk.ts | 2 +- public/app/routes/routes.ts | 2 +- public/app/stores/AlertListStore/helpers.ts | 2 +- public/app/types/container.ts | 3 +- public/app/types/navModel.ts | 2 +- public/test/mocks/common.ts | 29 +++++++++++++++++++ 23 files changed, 53 insertions(+), 34 deletions(-) rename public/app/features/alerting/{containers => }/AlertRuleList.test.tsx (100%) rename public/app/features/alerting/{containers => }/AlertRuleList.tsx (99%) rename public/app/features/alerting/{alert_tab_ctrl.ts => AlertTabCtrl.ts} (99%) rename public/app/features/alerting/{notification_edit_ctrl.ts => NotificationsEditCtrl.ts} (100%) rename public/app/features/alerting/{notifications_list_ctrl.ts => NotificationsListCtrl.ts} (100%) rename public/app/features/alerting/{containers => }/__snapshots__/AlertRuleList.test.tsx.snap (100%) delete mode 100644 public/app/features/alerting/all.ts rename public/app/features/alerting/{specs/threshold_mapper.test.ts => state/ThresholdMapper.test.ts} (97%) rename public/app/features/alerting/{threshold_mapper.ts => state/ThresholdMapper.ts} (100%) rename public/app/features/alerting/{alert_def.ts => state/alertDef.ts} (100%) rename public/app/features/alerting/{apis/index.ts => state/apis.ts} (97%) diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index c00441c4881..4e9a7f8e434 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -12,7 +12,6 @@ function getNotFoundModel(): NavModel { }; return { - breadcrumbs: [node], node: node, main: node, }; @@ -53,7 +52,6 @@ const navModelReducer = (state = initialState, action: Action): NavModel => { return { main: main, node: node, - breadcrumbs: [], }; } } diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/containers/ServerStats.test.tsx index a329a47527d..a89e78cb4ba 100644 --- a/public/app/features/admin/containers/ServerStats.test.tsx +++ b/public/app/features/admin/containers/ServerStats.test.tsx @@ -1,26 +1,18 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { ServerStats } from './ServerStats'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; +import { initNav } from 'test/mocks/common'; +import { ServerStat } from '../apis'; describe('ServerStats', () => { it('Should render table with stats', done => { - backendSrv.get.mockReturnValue( - Promise.resolve({ - dashboards: 10, - }) - ); + const stats: ServerStat[] = [{ name: 'test', value: 'asd' }]; - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('cfg', 'admin', 'server-stats'), - } - ); + let getServerStats = () => { + return Promise.resolve(stats); + }; - const page = renderer.create(); + const page = renderer.create(); setTimeout(() => { expect(page.toJSON()).toMatchSnapshot(); diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx index 7e96dcf4e0e..29611696efa 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { initNav } from 'app/core/actions'; @@ -14,7 +14,7 @@ interface State { stats: ServerStat[]; } -export class ServerStats extends React.Component { +export class ServerStats extends PureComponent { constructor(props) { super(props); diff --git a/public/app/features/alerting/containers/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx similarity index 100% rename from public/app/features/alerting/containers/AlertRuleList.test.tsx rename to public/app/features/alerting/AlertRuleList.test.tsx diff --git a/public/app/features/alerting/containers/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx similarity index 99% rename from public/app/features/alerting/containers/AlertRuleList.tsx rename to public/app/features/alerting/AlertRuleList.tsx index 3c64f490db4..e2e6d1a719a 100644 --- a/public/app/features/alerting/containers/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; import { initNav, updateLocation } from 'app/core/actions'; import { ContainerProps } from 'app/types'; -import { getAlertRules, AlertRule } from '../apis'; +import { getAlertRules, AlertRule } from './state/apis'; interface Props extends ContainerProps { updateLocation: typeof updateLocation; diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/AlertTabCtrl.ts similarity index 99% rename from public/app/features/alerting/alert_tab_ctrl.ts rename to public/app/features/alerting/AlertTabCtrl.ts index a25d37913d4..040b293b244 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -import { ThresholdMapper } from './threshold_mapper'; +import { ThresholdMapper } from './state/ThresholdMapper'; import { QueryPart } from 'app/core/components/query_part/query_part'; -import alertDef from './alert_def'; +import alertDef from './state/alertDef'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/NotificationsEditCtrl.ts similarity index 100% rename from public/app/features/alerting/notification_edit_ctrl.ts rename to public/app/features/alerting/NotificationsEditCtrl.ts diff --git a/public/app/features/alerting/notifications_list_ctrl.ts b/public/app/features/alerting/NotificationsListCtrl.ts similarity index 100% rename from public/app/features/alerting/notifications_list_ctrl.ts rename to public/app/features/alerting/NotificationsListCtrl.ts diff --git a/public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap similarity index 100% rename from public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap rename to public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/public/app/features/alerting/all.ts b/public/app/features/alerting/all.ts deleted file mode 100644 index 91d3a4109e7..00000000000 --- a/public/app/features/alerting/all.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './notifications_list_ctrl'; -import './notification_edit_ctrl'; diff --git a/public/app/features/alerting/specs/threshold_mapper.test.ts b/public/app/features/alerting/state/ThresholdMapper.test.ts similarity index 97% rename from public/app/features/alerting/specs/threshold_mapper.test.ts rename to public/app/features/alerting/state/ThresholdMapper.test.ts index 922d9c8787e..d8ab54234cd 100644 --- a/public/app/features/alerting/specs/threshold_mapper.test.ts +++ b/public/app/features/alerting/state/ThresholdMapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'test/lib/common'; -import { ThresholdMapper } from '../threshold_mapper'; +import { ThresholdMapper } from './threshold_mapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { diff --git a/public/app/features/alerting/threshold_mapper.ts b/public/app/features/alerting/state/ThresholdMapper.ts similarity index 100% rename from public/app/features/alerting/threshold_mapper.ts rename to public/app/features/alerting/state/ThresholdMapper.ts diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/state/alertDef.ts similarity index 100% rename from public/app/features/alerting/alert_def.ts rename to public/app/features/alerting/state/alertDef.ts diff --git a/public/app/features/alerting/apis/index.ts b/public/app/features/alerting/state/apis.ts similarity index 97% rename from public/app/features/alerting/apis/index.ts rename to public/app/features/alerting/state/apis.ts index ebfdbd34024..44cadc05215 100644 --- a/public/app/features/alerting/apis/index.ts +++ b/public/app/features/alerting/state/apis.ts @@ -1,5 +1,5 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; -import alertDef from '../alert_def'; +import alertDef from './alertDef'; import moment from 'moment'; export interface AlertRule { diff --git a/public/app/features/all.ts b/public/app/features/all.ts index df987a8b59b..065f399cae3 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -9,5 +9,6 @@ import './snapshot/all'; import './panel/all'; import './org/all'; import './admin/admin'; -import './alerting/all'; +import './alerting/NotificationsEditCtrl'; +import './alerting/NotificationsListCtrl'; import './styleguide/styleguide'; diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index ed2d797b7bf..0cb0c6a9419 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; -import alertDef from '../alerting/alert_def'; +import alertDef from '../alerting/state/alertDef'; /** @ngInject **/ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, $compile) { diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index b171f590e94..f5a23f4748b 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; -import alertDef from '../../../features/alerting/alert_def'; +import alertDef from '../../../features/alerting/state/alertDef'; import { PanelCtrl } from 'app/plugins/sdk'; import * as dateMath from 'app/core/utils/datemath'; diff --git a/public/app/plugins/sdk.ts b/public/app/plugins/sdk.ts index 2734426bd19..0f183271495 100644 --- a/public/app/plugins/sdk.ts +++ b/public/app/plugins/sdk.ts @@ -1,7 +1,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl'; import { QueryCtrl } from 'app/features/panel/query_ctrl'; -import { alertTab } from 'app/features/alerting/alert_tab_ctrl'; +import { alertTab } from 'app/features/alerting/AlertTabCtrl'; import { loadPluginCss } from 'app/features/plugins/plugin_loader'; export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, alertTab, loadPluginCss }; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 7b1e223afe5..dfd215f7056 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -2,7 +2,7 @@ import './dashboard_loaders'; import './ReactContainer'; import ServerStats from 'app/features/admin/containers/ServerStats'; -import AlertRuleList from 'app/features/alerting/containers/AlertRuleList'; +import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/containers/Teams/TeamPages'; diff --git a/public/app/stores/AlertListStore/helpers.ts b/public/app/stores/AlertListStore/helpers.ts index c460a697967..4d1ddcc30e0 100644 --- a/public/app/stores/AlertListStore/helpers.ts +++ b/public/app/stores/AlertListStore/helpers.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import alertDef from 'app/features/alerting/alert_def'; +import alertDef from 'app/features/alerting/state/alertDef'; export function setStateFields(rule, state) { const stateModel = alertDef.getStateDisplayModel(state); diff --git a/public/app/types/container.ts b/public/app/types/container.ts index 174bc0c8460..98b5248fdd6 100644 --- a/public/app/types/container.ts +++ b/public/app/types/container.ts @@ -1,6 +1,7 @@ import { NavModel } from './navModel'; +import { initNav } from 'app/core/actions'; export interface ContainerProps { navModel: NavModel; - initNav: (...args: string[]) => void; + initNav: typeof initNav; } diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts index e1a4265847c..9464858f967 100644 --- a/public/app/types/navModel.ts +++ b/public/app/types/navModel.ts @@ -9,11 +9,11 @@ export interface NavModelItem { hideFromTabs?: boolean; divider?: boolean; children?: NavModelItem[]; + breadcrumbs?: NavModelItem[]; target?: string; } export interface NavModel { - breadcrumbs: NavModelItem[]; main: NavModelItem; node: NavModelItem; } diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 64d12fdf725..5350636573d 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -1,3 +1,5 @@ +import { NavModel, NavModelItem } from 'app/types'; + export const backendSrv = { get: jest.fn(), getDashboard: jest.fn(), @@ -17,3 +19,30 @@ export function createNavTree(...args) { return root; } + +export function getNavModel(title: string, tabs: string[]): NavModel { + const node: NavModelItem = { + id: title, + text: title, + icon: 'fa fa-fw fa-warning', + subTitle: 'subTitle', + url: title, + children: [], + breadcrumbs: [], + }; + + for (let tab of tabs) { + node.children.push({ + id: tab, + icon: 'icon', + subTitle: 'subTitle', + url: title, + text: title, + }); + } + + return { + node: node, + main: node, + }; +} From 7b06800295189343bb61e6ef56cebe70056cc23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 10:36:36 -0700 Subject: [PATCH 027/666] refactor: changed nav store to use nav index and selector instead of initNav action --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 16 ++-- public/app/core/reducers/index.ts | 4 +- public/app/core/reducers/navModel.ts | 69 ++++----------- public/app/core/selectors/navModel.ts | 39 +++++++++ public/app/features/admin/apis/index.ts | 2 +- .../admin/containers/ServerStats.test.tsx | 7 +- .../features/admin/containers/ServerStats.tsx | 19 ++-- .../__snapshots__/ServerStats.test.tsx.snap | 87 ++++--------------- .../app/features/alerting/AlertRuleList.tsx | 15 ++-- .../alerting/state/ThresholdMapper.test.ts | 2 +- public/app/types/container.ts | 7 -- public/app/types/index.ts | 46 +++++++++- public/app/types/location.ts | 15 ---- public/app/types/navModel.ts | 19 ---- public/test/jest-setup.ts | 21 +++++ public/test/mocks/common.ts | 5 +- 17 files changed, 174 insertions(+), 202 deletions(-) create mode 100644 public/app/core/selectors/navModel.ts delete mode 100644 public/app/types/container.ts delete mode 100644 public/app/types/location.ts delete mode 100644 public/app/types/navModel.ts diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 7a965f82dd1..74b61f845c0 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,3 @@ -import { initNav } from './navModel'; import { updateLocation } from './location'; -export { initNav, updateLocation }; +export { updateLocation }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 048afd4f8ff..56d129fd263 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,11 +1,13 @@ -export type Action = InitNavModelAction; +export type Action = UpdateNavIndexAction; -export interface InitNavModelAction { - type: 'INIT_NAV_MODEL'; - args: string[]; +// this action is not used yet +// kind of just a placeholder, will be need for dynamic pages +// like datasource edit, teams edit page + +export interface UpdateNavIndexAction { + type: 'UPDATE_NAV_INDEX'; } -export const initNav = (...args: string[]): InitNavModelAction => ({ - type: 'INIT_NAV_MODEL', - args: args, +export const updateNavIndex = (): UpdateNavIndexAction => ({ + type: 'UPDATE_NAV_INDEX', }); diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index 98f796981e4..a3f9ca909c9 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,7 +1,7 @@ -import navModel from './navModel'; +import { navIndexReducer as navIndex } from './navModel'; import location from './location'; export default { - navModel, + navIndex, location, }; diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 4e9a7f8e434..26acdb39a3d 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,62 +1,29 @@ import { Action } from 'app/core/actions/navModel'; -import { NavModel, NavModelItem } from 'app/types'; +import { NavModelItem, NavIndex } from 'app/types'; import config from 'app/core/config'; -function getNotFoundModel(): NavModel { - var node: NavModelItem = { - id: 'not-found', - text: 'Page not found', - icon: 'fa fa-fw fa-warning', - subTitle: '404 Error', - url: 'not-found', - }; - - return { - node: node, - main: node, - }; +export function buildInitialState(): NavIndex { + const navIndex: NavIndex = {}; + const rootNodes = config.bootData.navTree as NavModelItem[]; + buildNavIndex(navIndex, rootNodes); + return navIndex; } -export const initialState: NavModel = getNotFoundModel(); +function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) { + for (const node of children) { + navIndex[node.id] = { + ...node, + parentItem: parentItem, + }; -const navModelReducer = (state = initialState, action: Action): NavModel => { - switch (action.type) { - case 'INIT_NAV_MODEL': { - let children = config.bootData.navTree as NavModelItem[]; - let main, node; - const parents = []; - - for (const id of action.args) { - node = children.find(el => el.id === id); - - if (!node) { - throw new Error(`NavItem with id ${id} not found`); - } - - children = node.children; - parents.push(node); - } - - main = parents[parents.length - 2]; - - if (main.children) { - for (const item of main.children) { - item.active = false; - - if (item.url === node.url) { - item.active = true; - } - } - } - - return { - main: main, - node: node, - }; + if (node.children) { + buildNavIndex(navIndex, node.children, node); } } +} +export const initialState: NavIndex = buildInitialState(); + +export const navIndexReducer = (state = initialState, action: Action): NavIndex => { return state; }; - -export default navModelReducer; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts new file mode 100644 index 00000000000..5f2d0318dff --- /dev/null +++ b/public/app/core/selectors/navModel.ts @@ -0,0 +1,39 @@ +import { NavModel, NavModelItem, NavIndex } from 'app/types'; + +function getNotFoundModel(): NavModel { + var node: NavModelItem = { + id: 'not-found', + text: 'Page not found', + icon: 'fa fa-fw fa-warning', + subTitle: '404 Error', + url: 'not-found', + }; + + return { + node: node, + main: node, + }; +} + +export function selectNavNode(navIndex: NavIndex, id: string): NavModel { + if (navIndex[id]) { + const node = navIndex[id]; + const main = { + ...node.parentItem, + }; + + main.children = main.children.map(item => { + return { + ...item, + active: item.url === node.url, + }; + }); + + return { + node: node, + main: main, + }; + } else { + return getNotFoundModel(); + } +} diff --git a/public/app/features/admin/apis/index.ts b/public/app/features/admin/apis/index.ts index 888cfd4f58f..d81fd299493 100644 --- a/public/app/features/admin/apis/index.ts +++ b/public/app/features/admin/apis/index.ts @@ -2,7 +2,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; export interface ServerStat { name: string; - value: string; + value: number; } export const getServerStats = async (): Promise => { diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/containers/ServerStats.test.tsx index a89e78cb4ba..e12dfc3bed4 100644 --- a/public/app/features/admin/containers/ServerStats.test.tsx +++ b/public/app/features/admin/containers/ServerStats.test.tsx @@ -1,18 +1,19 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { ServerStats } from './ServerStats'; -import { initNav } from 'test/mocks/common'; +import { createNavModel } from 'test/mocks/common'; import { ServerStat } from '../apis'; describe('ServerStats', () => { it('Should render table with stats', done => { - const stats: ServerStat[] = [{ name: 'test', value: 'asd' }]; + const navModel = createNavModel('Admin', 'stats'); + const stats: ServerStat[] = [{ name: 'Total dashboards', value: 10 }, { name: 'Total Users', value: 1 }]; let getServerStats = () => { return Promise.resolve(stats); }; - const page = renderer.create(); + const page = renderer.create(); setTimeout(() => { expect(page.toJSON()).toMatchSnapshot(); diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx index 29611696efa..0b44a9af65e 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -1,12 +1,13 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import { initNav } from 'app/core/actions'; -import { ContainerProps } from 'app/types'; +import { NavModel, StoreState } from 'app/types'; +import { selectNavNode } from 'app/core/selectors/navModel'; import { getServerStats, ServerStat } from '../apis'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -interface Props extends ContainerProps { +interface Props { + navModel: NavModel; getServerStats: () => Promise; } @@ -21,8 +22,6 @@ export class ServerStats extends PureComponent { this.state = { stats: [], }; - - this.props.initNav('cfg', 'admin', 'server-stats'); } async componentDidMount() { @@ -66,13 +65,9 @@ function StatItem(stat: ServerStat) { ); } -const mapStateToProps = state => ({ - navModel: state.navModel, +const mapStateToProps = (state: StoreState) => ({ + navModel: selectNavNode(state.navIndex, 'server-stats'), getServerStats: getServerStats, }); -const mapDispatchToProps = { - initNav, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); +export default hot(module)(connect(mapStateToProps)(ServerStats)); diff --git a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap index eac793ca2ca..63de5bcd870 100644 --- a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap +++ b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap @@ -17,8 +17,9 @@ exports[`ServerStats Should render table with stats 1`] = ` - - +
- admin-Text + Admin - +
+ subTitle +
@@ -60,13 +65,13 @@ exports[`ServerStats Should render table with stats 1`] = ` > - server-stats-Text + Admin @@ -101,66 +106,10 @@ exports[`ServerStats Should render table with stats 1`] = ` - Total users + Total Users - 0 - - - - - Active users (seen last 30 days) - - - 0 - - - - - Total orgs - - - 0 - - - - - Total playlists - - - 0 - - - - - Total snapshots - - - 0 - - - - - Total dashboard tags - - - 0 - - - - - Total starred dashboards - - - 0 - - - - - Total alerts - - - 0 + 1 diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index e2e6d1a719a..84994555445 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -5,11 +5,13 @@ import classNames from 'classnames'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; -import { initNav, updateLocation } from 'app/core/actions'; -import { ContainerProps } from 'app/types'; +import { updateLocation } from 'app/core/actions'; +import { selectNavNode } from 'app/core/selectors/navModel'; +import { NavModel, StoreState } from 'app/types'; import { getAlertRules, AlertRule } from './state/apis'; -interface Props extends ContainerProps { +interface Props { + navModel: NavModel; updateLocation: typeof updateLocation; } @@ -37,8 +39,6 @@ export class AlertRuleList extends PureComponent { search: '', stateFilter: '', }; - - this.props.initNav('alerting', 'alert-list'); } componentDidMount() { @@ -200,12 +200,11 @@ export class AlertRuleItem extends React.Component { } } -const mapStateToProps = state => ({ - navModel: state.navModel, +const mapStateToProps = (state: StoreState) => ({ + navModel: selectNavNode(state.navIndex, 'alert-list'), }); const mapDispatchToProps = { - initNav, updateLocation, }; diff --git a/public/app/features/alerting/state/ThresholdMapper.test.ts b/public/app/features/alerting/state/ThresholdMapper.test.ts index d8ab54234cd..8e91d0b6d0a 100644 --- a/public/app/features/alerting/state/ThresholdMapper.test.ts +++ b/public/app/features/alerting/state/ThresholdMapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'test/lib/common'; -import { ThresholdMapper } from './threshold_mapper'; +import { ThresholdMapper } from './ThresholdMapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { diff --git a/public/app/types/container.ts b/public/app/types/container.ts deleted file mode 100644 index 98b5248fdd6..00000000000 --- a/public/app/types/container.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NavModel } from './navModel'; -import { initNav } from 'app/core/actions'; - -export interface ContainerProps { - navModel: NavModel; - initNav: typeof initNav; -} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 9cb5ee85c04..930c08c9eb0 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,5 +1,43 @@ -import { NavModel, NavModelItem } from './navModel'; -import { ContainerProps } from './container'; -import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; +export interface LocationUpdate { + path?: string; + query?: UrlQueryMap; + routeParams?: UrlQueryMap; +} -export { NavModel, NavModelItem, ContainerProps, LocationState, LocationUpdate, UrlQueryValue, UrlQueryMap }; +export interface LocationState { + url: string; + path: string; + query: UrlQueryMap; + routeParams: UrlQueryMap; +} + +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; +export type UrlQueryMap = { [s: string]: UrlQueryValue }; + +export interface NavModelItem { + text: string; + url: string; + subTitle?: string; + icon?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children?: NavModelItem[]; + breadcrumbs?: NavModelItem[]; + target?: string; + parentItem?: NavModelItem; +} + +export interface NavModel { + main: NavModelItem; + node: NavModelItem; +} + +export type NavIndex = { [s: string]: NavModelItem }; + +export interface StoreState { + navIndex: NavIndex; + location: LocationState; +} diff --git a/public/app/types/location.ts b/public/app/types/location.ts deleted file mode 100644 index 4a7f51523a7..00000000000 --- a/public/app/types/location.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface LocationUpdate { - path?: string; - query?: UrlQueryMap; - routeParams?: UrlQueryMap; -} - -export interface LocationState { - url: string; - path: string; - query: UrlQueryMap; - routeParams: UrlQueryMap; -} - -export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; -export type UrlQueryMap = { [s: string]: UrlQueryValue }; diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts deleted file mode 100644 index 9464858f967..00000000000 --- a/public/app/types/navModel.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface NavModelItem { - text: string; - url: string; - subTitle?: string; - icon?: string; - img?: string; - id: string; - active?: boolean; - hideFromTabs?: boolean; - divider?: boolean; - children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; - target?: string; -} - -export interface NavModel { - main: NavModelItem; - node: NavModelItem; -} diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index fed65097ac7..7b326a279b7 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -20,3 +20,24 @@ configure({ adapter: new Adapter() }); const global = window; global.$ = global.jQuery = $; + +const localStorageMock = (function() { + var store = {}; + return { + getItem: function(key) { + return store[key]; + }, + setItem: function(key, value) { + store[key] = value.toString(); + }, + clear: function() { + store = {}; + }, + removeItem: function(key) { + delete store[key]; + }, + }; +})(); + +global.localStorage = localStorageMock; +// Object.defineProperty(window, 'localStorage', { value: localStorageMock }); diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 5350636573d..1c7bdb4f1e2 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -20,7 +20,7 @@ export function createNavTree(...args) { return root; } -export function getNavModel(title: string, tabs: string[]): NavModel { +export function createNavModel(title: string, ...tabs: string[]): NavModel { const node: NavModelItem = { id: title, text: title, @@ -38,9 +38,12 @@ export function getNavModel(title: string, tabs: string[]): NavModel { subTitle: 'subTitle', url: title, text: title, + active: false, }); } + node.children[0].active = true; + return { node: node, main: node, From 2a64d19f5b22ca44f6c3d5ecffec75f62b1fba7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 11:36:03 -0700 Subject: [PATCH 028/666] wip: load alert rules via redux --- public/app/core/reducers/index.ts | 2 +- public/app/core/reducers/location.ts | 4 +- public/app/core/selectors/navModel.ts | 2 +- .../features/admin/containers/ServerStats.tsx | 4 +- .../app/features/alerting/AlertRuleList.tsx | 27 +++++----- public/app/features/alerting/state/actions.ts | 26 ++++++++++ public/app/features/alerting/state/apis.ts | 52 ------------------- .../app/features/alerting/state/reducers.ts | 46 ++++++++++++++++ public/app/stores/configureStore.ts | 4 +- public/app/types/index.ts | 33 ++++++++++++ 10 files changed, 126 insertions(+), 74 deletions(-) create mode 100644 public/app/features/alerting/state/actions.ts delete mode 100644 public/app/features/alerting/state/apis.ts create mode 100644 public/app/features/alerting/state/reducers.ts diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index a3f9ca909c9..be13528c91c 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,5 +1,5 @@ import { navIndexReducer as navIndex } from './navModel'; -import location from './location'; +import { locationReducer as location } from './location'; export default { navIndex, diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 5676c82844a..4591448d082 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -16,7 +16,7 @@ function renderUrl(path: string, query: UrlQueryMap): string { return path; } -const routerReducer = (state = initialState, action: Action): LocationState => { +export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case 'UPDATE_LOCATION': { const { path, query, routeParams } = action.payload; @@ -31,5 +31,3 @@ const routerReducer = (state = initialState, action: Action): LocationState => { return state; }; - -export default routerReducer; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 5f2d0318dff..a7e1c3330bd 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function selectNavNode(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx index 0b44a9af65e..97419ec9301 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { NavModel, StoreState } from 'app/types'; -import { selectNavNode } from 'app/core/selectors/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getServerStats, ServerStat } from '../apis'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; @@ -66,7 +66,7 @@ function StatItem(stat: ServerStat) { } const mapStateToProps = (state: StoreState) => ({ - navModel: selectNavNode(state.navIndex, 'server-stats'), + navModel: getNavModel(state.navIndex, 'server-stats'), getServerStats: getServerStats, }); diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 84994555445..faa46945536 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -6,13 +6,15 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; import { updateLocation } from 'app/core/actions'; -import { selectNavNode } from 'app/core/selectors/navModel'; -import { NavModel, StoreState } from 'app/types'; -import { getAlertRules, AlertRule } from './state/apis'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, AlertRule } from 'app/types'; +import { getAlertRulesAsync } from './state/actions'; interface Props { navModel: NavModel; + alertRules: AlertRule[]; updateLocation: typeof updateLocation; + getAlertRulesAsync: typeof getAlertRulesAsync; } interface State { @@ -49,16 +51,11 @@ export class AlertRuleList extends PureComponent { this.props.updateLocation({ query: { state: evt.target.value }, }); - // this.fetchRules(); + this.fetchRules(); }; async fetchRules() { - try { - const rules = await getAlertRules(); - this.setState({ rules }); - } catch (error) { - console.error(error); - } + await this.props.getAlertRulesAsync(); // this.props.alertList.loadRules({ // state: this.props.view.query.get('state') || 'all', @@ -78,8 +75,8 @@ export class AlertRuleList extends PureComponent { }; render() { - const { navModel } = this.props; - const { rules, search, stateFilter } = this.state; + const { navModel, alertRules } = this.props; + const { search, stateFilter } = this.state; return (
@@ -117,7 +114,7 @@ export class AlertRuleList extends PureComponent {
    - {rules.map(rule => )} + {alertRules.map(rule => )}
@@ -201,11 +198,13 @@ export class AlertRuleItem extends React.Component { } const mapStateToProps = (state: StoreState) => ({ - navModel: selectNavNode(state.navIndex, 'alert-list'), + navModel: getNavModel(state.navIndex, 'alert-list'), + alertRules: state.alertRules, }); const mapDispatchToProps = { updateLocation, + getAlertRulesAsync, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts new file mode 100644 index 00000000000..0f9caa9d47f --- /dev/null +++ b/public/app/features/alerting/state/actions.ts @@ -0,0 +1,26 @@ +import { Dispatch } from 'redux'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { AlertRule } from 'app/types'; + +export interface LoadAlertRulesAction { + type: 'LOAD_ALERT_RULES'; + payload: AlertRule[]; +} + +export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ + type: 'LOAD_ALERT_RULES', + payload: rules, +}); + +export type Action = LoadAlertRulesAction; + +export const getAlertRulesAsync = () => async (dispatch: Dispatch): Promise => { + try { + const rules = await getBackendSrv().get('/api/alerts', {}); + dispatch(loadAlertRules(rules)); + return rules; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/public/app/features/alerting/state/apis.ts b/public/app/features/alerting/state/apis.ts deleted file mode 100644 index 44cadc05215..00000000000 --- a/public/app/features/alerting/state/apis.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; -import alertDef from './alertDef'; -import moment from 'moment'; - -export interface AlertRule { - id: number; - dashboardId: number; - panelId: number; - name: string; - state: string; - stateText: string; - stateIcon: string; - stateClass: string; - stateAge: string; - info?: string; - url: string; -} - -export function setStateFields(rule, state) { - const stateModel = alertDef.getStateDisplayModel(state); - rule.state = state; - rule.stateText = stateModel.text; - rule.stateIcon = stateModel.iconClass; - rule.stateClass = stateModel.stateClass; - rule.stateAge = moment(rule.newStateDate) - .fromNow() - .replace(' ago', ''); -} - -export const getAlertRules = async (): Promise => { - try { - const rules = await getBackendSrv().get('/api/alerts', {}); - - for (const rule of rules) { - setStateFields(rule, rule.state); - - if (rule.state !== 'paused') { - if (rule.executionError) { - rule.info = 'Execution Error: ' + rule.executionError; - } - if (rule.evalData && rule.evalData.noData) { - rule.info = 'Query returned no data'; - } - } - } - - return rules; - } catch (error) { - console.error(error); - throw error; - } -}; diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts new file mode 100644 index 00000000000..0718c511106 --- /dev/null +++ b/public/app/features/alerting/state/reducers.ts @@ -0,0 +1,46 @@ +import { Action } from './actions'; +import { AlertRule } from 'app/types'; +import alertDef from './alertDef'; +import moment from 'moment'; + +export const initialState: AlertRule[] = []; + +export function setStateFields(rule, state) { + const stateModel = alertDef.getStateDisplayModel(state); + rule.state = state; + rule.stateText = stateModel.text; + rule.stateIcon = stateModel.iconClass; + rule.stateClass = stateModel.stateClass; + rule.stateAge = moment(rule.newStateDate) + .fromNow() + .replace(' ago', ''); +} + +export const alertRulesReducer = (state = initialState, action: Action): AlertRule[] => { + switch (action.type) { + case 'LOAD_ALERT_RULES': { + const alertRules = action.payload; + + for (const rule of alertRules) { + setStateFields(rule, rule.state); + + if (rule.state !== 'paused') { + if (rule.executionError) { + rule.info = 'Execution Error: ' + rule.executionError; + } + if (rule.evalData && rule.evalData.noData) { + rule.info = 'Query returned no data'; + } + } + } + + return alertRules; + } + } + + return state; +}; + +export default { + alertRules: alertRulesReducer, +}; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 3a7d16da76d..232f2e30cb8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -2,9 +2,11 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; +import alertingReducers from 'app/features/alerting/state/reducers'; const rootReducer = combineReducers({ - ...sharedReducers + ...sharedReducers, + ...alertingReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 930c08c9eb0..a409f586f33 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,3 +1,7 @@ +// +// Location +// + export interface LocationUpdate { path?: string; query?: UrlQueryMap; @@ -14,6 +18,30 @@ export interface LocationState { export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; export type UrlQueryMap = { [s: string]: UrlQueryValue }; +// +// Alerting +// + +export interface AlertRule { + id: number; + dashboardId: number; + panelId: number; + name: string; + state: string; + stateText: string; + stateIcon: string; + stateClass: string; + stateAge: string; + info?: string; + url: string; + executionError?: string; + evalData?: { noData: boolean }; +} + +// +// NavModel +// + export interface NavModelItem { text: string; url: string; @@ -37,7 +65,12 @@ export interface NavModel { export type NavIndex = { [s: string]: NavModelItem }; +// +// Store +// + export interface StoreState { navIndex: NavIndex; location: LocationState; + alertRules: AlertRule[]; } From 3fd707f321a7c2fbb8081f87b6bc62122e9208da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 12:08:31 -0700 Subject: [PATCH 029/666] redux: progress --- .../app/features/alerting/AlertRuleList.tsx | 28 ++++++++++--------- public/app/features/alerting/state/actions.ts | 6 ++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index faa46945536..03bafe119b0 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -15,12 +15,11 @@ interface Props { alertRules: AlertRule[]; updateLocation: typeof updateLocation; getAlertRulesAsync: typeof getAlertRulesAsync; + stateFilter: string; } interface State { - rules: AlertRule[]; search: string; - stateFilter: string; } export class AlertRuleList extends PureComponent { @@ -37,29 +36,31 @@ export class AlertRuleList extends PureComponent { super(props); this.state = { - rules: [], search: '', - stateFilter: '', }; } componentDidMount() { - this.fetchRules(); + this.fetchRules(this.getStateFilter()); } onStateFilterChanged = evt => { this.props.updateLocation({ query: { state: evt.target.value }, }); - this.fetchRules(); + this.fetchRules(evt.target.value); }; - async fetchRules() { - await this.props.getAlertRulesAsync(); + getStateFilter(): string { + const { stateFilter } = this.props; + if (stateFilter) { + return stateFilter.toString(); + } + return 'all'; + } - // this.props.alertList.loadRules({ - // state: this.props.view.query.get('state') || 'all', - // }); + async fetchRules(stateFilter: string) { + await this.props.getAlertRulesAsync({ state: stateFilter }); } onOpenHowTo = () => { @@ -76,7 +77,7 @@ export class AlertRuleList extends PureComponent { render() { const { navModel, alertRules } = this.props; - const { search, stateFilter } = this.state; + const { search } = this.state; return (
@@ -99,7 +100,7 @@ export class AlertRuleList extends PureComponent {
- {this.stateFilters.map(AlertStateFilterOption)}
@@ -200,6 +201,7 @@ export class AlertRuleItem extends React.Component { const mapStateToProps = (state: StoreState) => ({ navModel: getNavModel(state.navIndex, 'alert-list'), alertRules: state.alertRules, + stateFilter: state.location.query.state, }); const mapDispatchToProps = { diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 0f9caa9d47f..9103b34e81d 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -14,9 +14,11 @@ export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ export type Action = LoadAlertRulesAction; -export const getAlertRulesAsync = () => async (dispatch: Dispatch): Promise => { +export const getAlertRulesAsync = (options: { state: string }) => async ( + dispatch: Dispatch +): Promise => { try { - const rules = await getBackendSrv().get('/api/alerts', {}); + const rules = await getBackendSrv().get('/api/alerts', options); dispatch(loadAlertRules(rules)); return rules; } catch (error) { From 42aaa2b90746eee050bab8495b1d007277e2863f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 12:14:41 -0700 Subject: [PATCH 030/666] redux: improved state handling --- public/app/features/alerting/AlertRuleList.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 03bafe119b0..77e5af520fc 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -41,14 +41,20 @@ export class AlertRuleList extends PureComponent { } componentDidMount() { - this.fetchRules(this.getStateFilter()); + console.log('did mount'); + this.fetchRules(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.stateFilter !== this.props.stateFilter) { + this.fetchRules(); + } } onStateFilterChanged = evt => { this.props.updateLocation({ query: { state: evt.target.value }, }); - this.fetchRules(evt.target.value); }; getStateFilter(): string { @@ -59,8 +65,8 @@ export class AlertRuleList extends PureComponent { return 'all'; } - async fetchRules(stateFilter: string) { - await this.props.getAlertRulesAsync({ state: stateFilter }); + async fetchRules() { + await this.props.getAlertRulesAsync({ state: this.getStateFilter() }); } onOpenHowTo = () => { From 50444c32e00b82c903bdf280499d1c4641cb43f1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 3 Sep 2018 13:46:39 +0200 Subject: [PATCH 031/666] actions and reducers for search filter --- .../features/alerting/AlertRuleItem.test.tsx | 32 +++++ .../app/features/alerting/AlertRuleItem.tsx | 70 +++++++++++ .../app/features/alerting/AlertRuleList.tsx | 112 ++++-------------- .../__snapshots__/AlertRuleItem.test.tsx.snap | 85 +++++++++++++ public/app/features/alerting/state/actions.ts | 21 +++- .../app/features/alerting/state/reducers.ts | 17 +-- .../app/features/alerting/state/selectors.ts | 9 ++ public/app/types/index.ts | 7 +- 8 files changed, 251 insertions(+), 102 deletions(-) create mode 100644 public/app/features/alerting/AlertRuleItem.test.tsx create mode 100644 public/app/features/alerting/AlertRuleItem.tsx create mode 100644 public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap create mode 100644 public/app/features/alerting/state/selectors.ts diff --git a/public/app/features/alerting/AlertRuleItem.test.tsx b/public/app/features/alerting/AlertRuleItem.test.tsx new file mode 100644 index 00000000000..0a1c5cbe437 --- /dev/null +++ b/public/app/features/alerting/AlertRuleItem.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import AlertRuleItem, { Props } from './AlertRuleItem'; + +const setup = (propOverrides?: object) => { + const props: Props = { + rule: { + id: 1, + dashboardId: 1, + panelId: 1, + name: 'Some rule', + state: 'Open', + stateText: 'state text', + stateIcon: 'icon', + stateClass: 'state class', + stateAge: 'age', + url: 'https://something.something.darkside', + }, + search: '', + }; + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx new file mode 100644 index 00000000000..4c8d74cd6e3 --- /dev/null +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import Highlighter from 'react-highlight-words'; +import classNames from 'classnames/bind'; +import { AlertRule } from '../../types'; + +export interface Props { + rule: AlertRule; + search: string; +} + +export default class AlertRuleItem extends React.Component { + toggleState = () => { + // this.props.rule.togglePaused(); + }; + + renderText(text: string) { + return ( + + ); + } + + render() { + const { rule } = this.props; + + const stateClass = classNames({ + fa: true, + 'fa-play': rule.state === 'paused', + 'fa-pause': rule.state !== 'paused', + }); + + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + + return ( +
  • + + + +
    +
    + +
    + {this.renderText(rule.stateText)} + for {rule.stateAge} +
    +
    + {rule.info &&
    {this.renderText(rule.info)}
    } +
    + +
    + + + + +
    +
  • + ); + } +} diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 77e5af520fc..0adadb0f6d0 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -1,21 +1,23 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import classNames from 'classnames'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import AlertRuleItem from './AlertRuleItem'; import appEvents from 'app/core/app_events'; -import Highlighter from 'react-highlight-words'; import { updateLocation } from 'app/core/actions'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, AlertRule } from 'app/types'; -import { getAlertRulesAsync } from './state/actions'; +import { getAlertRulesAsync, setSearchQuery } from './state/actions'; +import { getAlertRuleItems, getSearchQuery } from './state/selectors'; interface Props { navModel: NavModel; alertRules: AlertRule[]; updateLocation: typeof updateLocation; getAlertRulesAsync: typeof getAlertRulesAsync; + setSearchQuery: typeof setSearchQuery; stateFilter: string; + search: string; } interface State { @@ -32,14 +34,6 @@ export class AlertRuleList extends PureComponent { { text: 'Paused', value: 'paused' }, ]; - constructor(props) { - super(props); - - this.state = { - search: '', - }; - } - componentDidMount() { console.log('did mount'); this.fetchRules(); @@ -77,13 +71,21 @@ export class AlertRuleList extends PureComponent { }); }; - onSearchQueryChange = evt => { - // this.props.alertList.setSearchQuery(evt.target.value); + onSearchQueryChange = event => { + const { value } = event.target; + this.props.setSearchQuery(value); }; + alertStateFilterOption({ text, value }) { + return ( + + ); + } + render() { - const { navModel, alertRules } = this.props; - const { search } = this.state; + const { navModel, alertRules, search } = this.props; return (
    @@ -107,7 +109,7 @@ export class AlertRuleList extends PureComponent {
    @@ -130,89 +132,17 @@ export class AlertRuleList extends PureComponent { } } -function AlertStateFilterOption({ text, value }) { - return ( - - ); -} - -export interface AlertRuleItemProps { - rule: AlertRule; - search: string; -} - -export class AlertRuleItem extends React.Component { - toggleState = () => { - // this.props.rule.togglePaused(); - }; - - renderText(text: string) { - return ( - - ); - } - - render() { - const { rule } = this.props; - - const stateClass = classNames({ - fa: true, - 'fa-play': rule.state === 'paused', - 'fa-pause': rule.state !== 'paused', - }); - - const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; - - return ( -
  • - - - -
    -
    - -
    - {this.renderText(rule.stateText)} - for {rule.stateAge} -
    -
    - {rule.info &&
    {this.renderText(rule.info)}
    } -
    - -
    - - - - -
    -
  • - ); - } -} - const mapStateToProps = (state: StoreState) => ({ navModel: getNavModel(state.navIndex, 'alert-list'), - alertRules: state.alertRules, + alertRules: getAlertRuleItems(state.alertRules), stateFilter: state.location.query.state, + search: getSearchQuery(state.alertRules), }); const mapDispatchToProps = { updateLocation, getAlertRulesAsync, + setSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap new file mode 100644 index 00000000000..7d3c446fc55 --- /dev/null +++ b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
  • + + + +
    +
    +
    + + + +
    +
    + + + + + for + age + +
    +
    +
    +
    + + + + +
    +
  • +`; diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 9103b34e81d..3b80cb19c39 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -2,17 +2,32 @@ import { Dispatch } from 'redux'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { AlertRule } from 'app/types'; +export enum ActionTypes { + LoadAlertRules = 'LOAD_ALERT_RULES', + SetSearchQuery = 'SET_SEARCH_QUERY', +} + export interface LoadAlertRulesAction { - type: 'LOAD_ALERT_RULES'; + type: ActionTypes.LoadAlertRules; payload: AlertRule[]; } +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ - type: 'LOAD_ALERT_RULES', + type: ActionTypes.LoadAlertRules, payload: rules, }); -export type Action = LoadAlertRulesAction; +export const setSearchQuery = (query: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: query, +}); + +export type Action = LoadAlertRulesAction | SetSearchQueryAction; export const getAlertRulesAsync = (options: { state: string }) => async ( dispatch: Dispatch diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index 0718c511106..a18d112dd94 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -1,9 +1,9 @@ -import { Action } from './actions'; -import { AlertRule } from 'app/types'; -import alertDef from './alertDef'; import moment from 'moment'; +import { AlertRulesState } from 'app/types'; +import { Action, ActionTypes } from './actions'; +import alertDef from './alertDef'; -export const initialState: AlertRule[] = []; +export const initialState: AlertRulesState = { items: [], searchQuery: '' }; export function setStateFields(rule, state) { const stateModel = alertDef.getStateDisplayModel(state); @@ -16,9 +16,9 @@ export function setStateFields(rule, state) { .replace(' ago', ''); } -export const alertRulesReducer = (state = initialState, action: Action): AlertRule[] => { +export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => { switch (action.type) { - case 'LOAD_ALERT_RULES': { + case ActionTypes.LoadAlertRules: { const alertRules = action.payload; for (const rule of alertRules) { @@ -34,8 +34,11 @@ export const alertRulesReducer = (state = initialState, action: Action): AlertRu } } - return alertRules; + return { items: alertRules, searchQuery: state.searchQuery }; } + + case ActionTypes.SetSearchQuery: + return { items: state.items, searchQuery: action.payload }; } return state; diff --git a/public/app/features/alerting/state/selectors.ts b/public/app/features/alerting/state/selectors.ts new file mode 100644 index 00000000000..7c72520d773 --- /dev/null +++ b/public/app/features/alerting/state/selectors.ts @@ -0,0 +1,9 @@ +export const getSearchQuery = state => state.searchQuery; + +export const getAlertRuleItems = state => { + const regex = new RegExp(state.searchQuery, 'i'); + + return state.items.filter(item => { + return regex.test(item.name) || regex.test(item.stateText) || regex.test(item.info); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index a409f586f33..6aa4dc23d97 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -69,8 +69,13 @@ export type NavIndex = { [s: string]: NavModelItem }; // Store // +export interface AlertRulesState { + items: AlertRule[]; + searchQuery: string; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; - alertRules: AlertRule[]; + alertRules: AlertRulesState; } From 1994ca50167c0ec37f0b07fcd8a66d0f167decf3 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 3 Sep 2018 14:04:44 +0200 Subject: [PATCH 032/666] remove log --- public/app/features/alerting/AlertRuleList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 0adadb0f6d0..6023a1bb142 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -35,7 +35,6 @@ export class AlertRuleList extends PureComponent { ]; componentDidMount() { - console.log('did mount'); this.fetchRules(); } From c958ebd10172964af4dde7f56b67cb994f4cbe7f Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 3 Sep 2018 14:05:12 +0200 Subject: [PATCH 033/666] extend from purecomponent --- public/app/features/alerting/AlertRuleItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 4c8d74cd6e3..7d669771722 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; import classNames from 'classnames/bind'; import { AlertRule } from '../../types'; @@ -8,7 +8,7 @@ export interface Props { search: string; } -export default class AlertRuleItem extends React.Component { +export default class AlertRuleItem extends PureComponent { toggleState = () => { // this.props.rule.togglePaused(); }; From 638370e310155ef9e82e942b3f47e5ddb0d9c470 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 3 Sep 2018 15:44:39 +0200 Subject: [PATCH 034/666] pausing alert need to fix return type on dispatch. Could not test correctly either. --- public/app/features/alerting/AlertRuleItem.tsx | 17 +++++++++++++---- public/app/features/alerting/state/actions.ts | 18 +++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 7d669771722..95c6966ab88 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -1,16 +1,21 @@ import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import Highlighter from 'react-highlight-words'; import classNames from 'classnames/bind'; +import { togglePauseAlertRule } from './state/actions'; import { AlertRule } from '../../types'; export interface Props { rule: AlertRule; search: string; + togglePauseAlertRule: typeof togglePauseAlertRule; } -export default class AlertRuleItem extends PureComponent { - toggleState = () => { - // this.props.rule.togglePaused(); +class AlertRuleItem extends PureComponent { + togglePaused = () => { + const { rule } = this.props; + + this.props.togglePauseAlertRule(rule.id, { paused: rule.state === 'paused' }); }; renderText(text: string) { @@ -56,7 +61,7 @@ export default class AlertRuleItem extends PureComponent { @@ -68,3 +73,7 @@ export default class AlertRuleItem extends PureComponent { ); } } + +export default connect(null, { + togglePauseAlertRule, +})(AlertRuleItem); diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 3b80cb19c39..87afbfff665 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,6 +1,6 @@ import { Dispatch } from 'redux'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AlertRule } from 'app/types'; +import { AlertRule, StoreState } from 'app/types'; export enum ActionTypes { LoadAlertRules = 'LOAD_ALERT_RULES', @@ -41,3 +41,19 @@ export const getAlertRulesAsync = (options: { state: string }) => async ( throw error; } }; + +export const togglePauseAlertRule = (id: number, options: { paused: boolean }) => async ( + // Maybe fix dispatch type? + dispatch: Dispatch, + getState: () => StoreState +): Promise => { + try { + await getBackendSrv().post(`/api/alerts/${id}/pause`, options); + const stateFilter = getState().location.query.state || 'all'; + dispatch(getAlertRulesAsync({ state: stateFilter.toString() })); + return true; + } catch (error) { + console.log(error); + throw error; + } +}; From f4594c8320e67831c8d476db7a840b356f149c84 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 3 Sep 2018 16:58:11 +0200 Subject: [PATCH 035/666] some basic selector tests --- .../features/alerting/AlertRuleItem.test.tsx | 5 +++ .../features/alerting/state/selectors.test.ts | 43 +++++++++++++++++++ public/app/types/index.ts | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 public/app/features/alerting/state/selectors.test.ts diff --git a/public/app/features/alerting/AlertRuleItem.test.tsx b/public/app/features/alerting/AlertRuleItem.test.tsx index 0a1c5cbe437..397c5c5ac0c 100644 --- a/public/app/features/alerting/AlertRuleItem.test.tsx +++ b/public/app/features/alerting/AlertRuleItem.test.tsx @@ -2,6 +2,10 @@ import React from 'react'; import { shallow } from 'enzyme'; import AlertRuleItem, { Props } from './AlertRuleItem'; +jest.mock('react-redux', () => ({ + connect: params => params, +})); + const setup = (propOverrides?: object) => { const props: Props = { rule: { @@ -17,6 +21,7 @@ const setup = (propOverrides?: object) => { url: 'https://something.something.darkside', }, search: '', + togglePauseAlertRule: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/alerting/state/selectors.test.ts b/public/app/features/alerting/state/selectors.test.ts new file mode 100644 index 00000000000..2d6d48caa2d --- /dev/null +++ b/public/app/features/alerting/state/selectors.test.ts @@ -0,0 +1,43 @@ +import { getSearchQuery, getAlertRuleItems } from './selectors'; +import { AlertRulesState } from '../../../types'; + +const defaultState: AlertRulesState = { + items: [], + searchQuery: '', +}; + +const getState = (overrides?: object) => Object.assign(defaultState, overrides); + +describe('Get search query', () => { + it('should get search query', () => { + const state = getState({ searchQuery: 'dashboard' }); + const result = getSearchQuery(state); + + expect(result).toEqual(state.searchQuery); + }); +}); + +describe('Get alert rule items', () => { + it('should get alert rule items', () => { + const state = getState({ + items: [ + { + id: 1, + dashboardId: 1, + panelId: 1, + name: '', + state: '', + stateText: '', + stateIcon: '', + stateClass: '', + stateAge: '', + url: '', + }, + ], + }); + + const result = getAlertRuleItems(state); + + expect(result.length).toEqual(0); + }); +}); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 6aa4dc23d97..1f17962a70b 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -32,8 +32,8 @@ export interface AlertRule { stateIcon: string; stateClass: string; stateAge: string; - info?: string; url: string; + info?: string; executionError?: string; evalData?: { noData: boolean }; } From 5ac5a08e9e9b21ac711bcb2095fa9d10afb80119 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 4 Sep 2018 09:53:24 +0200 Subject: [PATCH 036/666] Fixed a bug in the test and added test for filter alert rules --- .../features/alerting/state/selectors.test.ts | 75 ++++++++++++++++--- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/public/app/features/alerting/state/selectors.test.ts b/public/app/features/alerting/state/selectors.test.ts index 2d6d48caa2d..e853b146c03 100644 --- a/public/app/features/alerting/state/selectors.test.ts +++ b/public/app/features/alerting/state/selectors.test.ts @@ -1,16 +1,8 @@ import { getSearchQuery, getAlertRuleItems } from './selectors'; -import { AlertRulesState } from '../../../types'; - -const defaultState: AlertRulesState = { - items: [], - searchQuery: '', -}; - -const getState = (overrides?: object) => Object.assign(defaultState, overrides); describe('Get search query', () => { it('should get search query', () => { - const state = getState({ searchQuery: 'dashboard' }); + const state = { searchQuery: 'dashboard' }; const result = getSearchQuery(state); expect(result).toEqual(state.searchQuery); @@ -19,7 +11,7 @@ describe('Get search query', () => { describe('Get alert rule items', () => { it('should get alert rule items', () => { - const state = getState({ + const state = { items: [ { id: 1, @@ -34,10 +26,69 @@ describe('Get alert rule items', () => { url: '', }, ], - }); + searchQuery: '', + }; const result = getAlertRuleItems(state); + expect(result.length).toEqual(1); + }); - expect(result.length).toEqual(0); + it('should filter rule items based on search query', () => { + const state = { + items: [ + { + id: 1, + dashboardId: 1, + panelId: 1, + name: 'dashboard', + state: '', + stateText: '', + stateIcon: '', + stateClass: '', + stateAge: '', + url: '', + }, + { + id: 2, + dashboardId: 3, + panelId: 1, + name: 'dashboard2', + state: '', + stateText: '', + stateIcon: '', + stateClass: '', + stateAge: '', + url: '', + }, + { + id: 3, + dashboardId: 5, + panelId: 1, + name: 'hello', + state: '', + stateText: '', + stateIcon: '', + stateClass: '', + stateAge: '', + url: '', + }, + { + id: 4, + dashboardId: 7, + panelId: 1, + name: 'test', + state: '', + stateText: 'dashboard', + stateIcon: '', + stateClass: '', + stateAge: '', + url: '', + }, + ], + searchQuery: 'dashboard', + }; + + const result = getAlertRuleItems(state); + expect(result.length).toEqual(3); }); }); From 22510be450a7930a9a907c4f84385ac75fa79fcc Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 4 Sep 2018 15:00:04 +0200 Subject: [PATCH 037/666] tests --- .../features/alerting/AlertRuleItem.test.tsx | 3 +- .../app/features/alerting/AlertRuleItem.tsx | 2 +- .../features/alerting/AlertRuleList.test.tsx | 186 ++++++++--- .../app/features/alerting/AlertRuleList.tsx | 31 +- .../__snapshots__/AlertRuleList.test.tsx.snap | 309 +++++++++++++----- public/app/features/alerting/state/actions.ts | 8 +- .../features/alerting/state/reducers.test.ts | 91 ++++++ .../app/features/alerting/state/reducers.ts | 35 +- public/app/types/index.ts | 15 + 9 files changed, 511 insertions(+), 169 deletions(-) create mode 100644 public/app/features/alerting/state/reducers.test.ts diff --git a/public/app/features/alerting/AlertRuleItem.test.tsx b/public/app/features/alerting/AlertRuleItem.test.tsx index 397c5c5ac0c..1b356fa5687 100644 --- a/public/app/features/alerting/AlertRuleItem.test.tsx +++ b/public/app/features/alerting/AlertRuleItem.test.tsx @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import AlertRuleItem, { Props } from './AlertRuleItem'; jest.mock('react-redux', () => ({ - connect: params => params, + connect: () => params => params, })); const setup = (propOverrides?: object) => { @@ -23,6 +23,7 @@ const setup = (propOverrides?: object) => { search: '', togglePauseAlertRule: jest.fn(), }; + Object.assign(props, propOverrides); return shallow(); diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 95c6966ab88..0e6b1c5fb90 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -15,7 +15,7 @@ class AlertRuleItem extends PureComponent { togglePaused = () => { const { rule } = this.props; - this.props.togglePauseAlertRule(rule.id, { paused: rule.state === 'paused' }); + this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' }); }; renderText(text: string) { diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index f88ff4522d4..9bcdcd41a3b 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -1,69 +1,159 @@ import React from 'react'; -import moment from 'moment'; -import { AlertRuleList } from './AlertRuleList'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; -import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; +import { shallow } from 'enzyme'; +import AlertRuleList, { Props } from './AlertRuleList'; +import { AlertRule, NavModel } from '../../types'; +import appEvents from '../../core/app_events'; -describe('AlertRuleList', () => { - let page, store; +jest.mock('react-redux', () => ({ + connect: () => params => params, +})); - beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ +jest.mock('../../core/app_events', () => ({ + emit: jest.fn(), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + alertRules: [] as AlertRule[], + updateLocation: jest.fn(), + getAlertRulesAsync: jest.fn(), + setSearchQuery: jest.fn(), + stateFilter: '', + search: '', + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + + return { + wrapper, + instance: wrapper.instance() as AlertRuleList, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render alert rules', () => { + const { wrapper } = setup({ + alertRules: [ { - id: 11, - dashboardId: 58, + id: 1, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', panelId: 3, - name: 'Panel Title alert', + name: 'TestData - Always OK', state: 'ok', - newStateDate: moment() - .subtract(5, 'minutes') - .format(), + newStateDate: '2018-09-04T10:01:01+02:00', + evalDate: '0001-01-01T00:00:00Z', evalData: {}, executionError: '', - url: 'd/ufkcofof/my-goal', - canEdit: true, + url: '/d/ggHbN42mk/alerting-with-testdata', }, - ]) - ); + { + id: 3, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', + panelId: 3, + name: 'TestData - ok', + state: 'ok', + newStateDate: '2018-09-04T10:01:01+02:00', + evalDate: '0001-01-01T00:00:00Z', + evalData: {}, + executionError: 'error', + url: '/d/ggHbN42mk/alerting-with-testdata', + }, + ], + }); - store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('alerting', 'alert-list'), - } - ); - - page = mount(); + expect(wrapper).toMatchSnapshot(); }); +}); - it('should call api to get rules', () => { - expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts'); +describe('Life cycle', () => { + describe('component did mount', () => { + it('should call fetchrules', () => { + const { instance } = setup(); + instance.fetchRules = jest.fn(); + instance.componentDidMount(); + expect(instance.fetchRules).toHaveBeenCalled(); + }); }); - it('should render 1 rule', () => { - page.update(); - const ruleNode = page.find('.alert-rule-item'); - expect(toJson(ruleNode)).toMatchSnapshot(); + describe('component did update', () => { + it('should call fetchrules if props differ', () => { + const { instance } = setup(); + instance.fetchRules = jest.fn(); + + instance.componentDidUpdate({ stateFilter: 'ok' }); + + expect(instance.fetchRules).toHaveBeenCalled(); + }); + }); +}); + +describe('Functions', () => { + describe('Get state filter', () => { + it('should get all if prop is not set', () => { + const { instance } = setup(); + + const stateFilter = instance.getStateFilter(); + + expect(stateFilter).toEqual('all'); + }); + + it('should return state filter if set', () => { + const { instance } = setup({ + stateFilter: 'ok', + }); + + const stateFilter = instance.getStateFilter(); + + expect(stateFilter).toEqual('ok'); + }); + }); + + describe('State filter changed', () => { + it('should update location', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'alerting' } }; + + instance.onStateFilterChanged(mockEvent); + + expect(instance.props.updateLocation).toHaveBeenCalledWith({ query: { state: 'alerting' } }); + }); }); + + describe('Open how to', () => { + it('should emit show-modal event', () => { + const { instance } = setup(); - it('toggle state should change pause rule if not paused', async () => { - backendSrv.post.mockReturnValue( - Promise.resolve({ - state: 'paused', - }) - ); + instance.onOpenHowTo(); + + expect(appEvents.emit).toHaveBeenCalledWith('show-modal', { + src: 'public/app/features/alerting/partials/alert_howto.html', + modalClass: 'confirm-modal', + model: {}, + }); + }); + }); - page.find('.fa-pause').simulate('click'); + describe('Search query change', () => { + it('should set search query', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'dashboard' } }; - // wait for api call to resolve - await Promise.resolve(); - page.update(); + instance.onSearchQueryChange(mockEvent); - expect(store.alertList.rules[0].state).toBe('paused'); - expect(page.find('.fa-play')).toHaveLength(1); + expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard'); + }); }); }); diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 6023a1bb142..e8458e72f11 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -10,7 +10,7 @@ import { NavModel, StoreState, AlertRule } from 'app/types'; import { getAlertRulesAsync, setSearchQuery } from './state/actions'; import { getAlertRuleItems, getSearchQuery } from './state/selectors'; -interface Props { +export interface Props { navModel: NavModel; alertRules: AlertRule[]; updateLocation: typeof updateLocation; @@ -20,11 +20,7 @@ interface Props { search: string; } -interface State { - search: string; -} - -export class AlertRuleList extends PureComponent { +class AlertRuleList extends PureComponent { stateFilters = [ { text: 'All', value: 'all' }, { text: 'OK', value: 'ok' }, @@ -44,11 +40,9 @@ export class AlertRuleList extends PureComponent { } } - onStateFilterChanged = evt => { - this.props.updateLocation({ - query: { state: evt.target.value }, - }); - }; + async fetchRules() { + await this.props.getAlertRulesAsync({ state: this.getStateFilter() }); + } getStateFilter(): string { const { stateFilter } = this.props; @@ -58,9 +52,11 @@ export class AlertRuleList extends PureComponent { return 'all'; } - async fetchRules() { - await this.props.getAlertRulesAsync({ state: this.getStateFilter() }); - } + onStateFilterChanged = event => { + this.props.updateLocation({ + query: { state: event.target.value }, + }); + }; onOpenHowTo = () => { appEvents.emit('show-modal', { @@ -75,13 +71,13 @@ export class AlertRuleList extends PureComponent { this.props.setSearchQuery(value); }; - alertStateFilterOption({ text, value }) { + alertStateFilterOption = ({ text, value }) => { return ( ); - } + }; render() { const { navModel, alertRules, search } = this.props; @@ -112,14 +108,11 @@ export class AlertRuleList extends PureComponent {
    - -
      {alertRules.map(rule => )} diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap index f408f6409be..99869ba6126 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap @@ -1,103 +1,254 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AlertRuleList should render 1 rule 1`] = ` -
    1. - - - +exports[`Render should render alert rules 1`] = ` +
      +
      - - +
      + +
      + +
      +
        + + +
      +
      +
      +`; + +exports[`Render should render component 1`] = ` +
      +
      -
      +
      + +
      + +
      +
      + +
      +
        - +
      -
    2. +
    `; diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 87afbfff665..2dff257685f 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,6 +1,6 @@ import { Dispatch } from 'redux'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AlertRule, StoreState } from 'app/types'; +import { AlertRuleApi, StoreState } from 'app/types'; export enum ActionTypes { LoadAlertRules = 'LOAD_ALERT_RULES', @@ -9,7 +9,7 @@ export enum ActionTypes { export interface LoadAlertRulesAction { type: ActionTypes.LoadAlertRules; - payload: AlertRule[]; + payload: AlertRuleApi[]; } export interface SetSearchQueryAction { @@ -17,7 +17,7 @@ export interface SetSearchQueryAction { payload: string; } -export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ +export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({ type: ActionTypes.LoadAlertRules, payload: rules, }); @@ -31,7 +31,7 @@ export type Action = LoadAlertRulesAction | SetSearchQueryAction; export const getAlertRulesAsync = (options: { state: string }) => async ( dispatch: Dispatch -): Promise => { +): Promise => { try { const rules = await getBackendSrv().get('/api/alerts', options); dispatch(loadAlertRules(rules)); diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts new file mode 100644 index 00000000000..96ca7bacf6c --- /dev/null +++ b/public/app/features/alerting/state/reducers.test.ts @@ -0,0 +1,91 @@ +import { ActionTypes, Action } from './actions'; +import { alertRulesReducer, initialState } from './reducers'; +import { AlertRuleApi } from '../../../types'; + +describe('Alert rules', () => { + const payload: AlertRuleApi[] = [ + { + id: 2, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', + panelId: 4, + name: 'TestData - Always Alerting', + state: 'alerting', + newStateDate: '2018-09-04T10:00:30+02:00', + evalDate: '0001-01-01T00:00:00Z', + evalData: { evalMatches: [{ metric: 'A-series', tags: null, value: 215 }] }, + executionError: '', + url: '/d/ggHbN42mk/alerting-with-testdata', + }, + { + id: 1, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', + panelId: 3, + name: 'TestData - Always OK', + state: 'ok', + newStateDate: '2018-09-04T10:01:01+02:00', + evalDate: '0001-01-01T00:00:00Z', + evalData: {}, + executionError: '', + url: '/d/ggHbN42mk/alerting-with-testdata', + }, + { + id: 3, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', + panelId: 3, + name: 'TestData - ok', + state: 'ok', + newStateDate: '2018-09-04T10:01:01+02:00', + evalDate: '0001-01-01T00:00:00Z', + evalData: {}, + executionError: 'error', + url: '/d/ggHbN42mk/alerting-with-testdata', + }, + { + id: 4, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', + panelId: 3, + name: 'TestData - Paused', + state: 'paused', + newStateDate: '2018-09-04T10:01:01+02:00', + evalDate: '0001-01-01T00:00:00Z', + evalData: {}, + executionError: 'error', + url: '/d/ggHbN42mk/alerting-with-testdata', + }, + { + id: 5, + dashboardId: 7, + dashboardUid: 'ggHbN42mk', + dashboardSlug: 'alerting-with-testdata', + panelId: 3, + name: 'TestData - Ok', + state: 'ok', + newStateDate: '2018-09-04T10:01:01+02:00', + evalDate: '0001-01-01T00:00:00Z', + evalData: { + noData: true, + }, + executionError: 'error', + url: '/d/ggHbN42mk/alerting-with-testdata', + }, + ]; + + it('should set alert rules', () => { + const action: Action = { + type: ActionTypes.LoadAlertRules, + payload: payload, + }; + + const result = alertRulesReducer(initialState, action); + + expect(result.items).toEqual(payload); + }); +}); diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index a18d112dd94..73feb3cb260 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -1,40 +1,41 @@ import moment from 'moment'; -import { AlertRulesState } from 'app/types'; +import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types'; import { Action, ActionTypes } from './actions'; import alertDef from './alertDef'; export const initialState: AlertRulesState = { items: [], searchQuery: '' }; -export function setStateFields(rule, state) { +function convertToAlertRule(rule, state): AlertRule { const stateModel = alertDef.getStateDisplayModel(state); - rule.state = state; rule.stateText = stateModel.text; rule.stateIcon = stateModel.iconClass; rule.stateClass = stateModel.stateClass; rule.stateAge = moment(rule.newStateDate) .fromNow() .replace(' ago', ''); + + if (rule.state !== 'paused') { + if (rule.executionError) { + rule.info = 'Execution Error: ' + rule.executionError; + } + if (rule.evalData && rule.evalData.noData) { + rule.info = 'Query returned no data'; + } + } + + return rule; } export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => { switch (action.type) { case ActionTypes.LoadAlertRules: { - const alertRules = action.payload; + const alertRules: AlertRuleApi[] = action.payload; - for (const rule of alertRules) { - setStateFields(rule, rule.state); + const alertRulesViewModel: AlertRule[] = alertRules.map(rule => { + return convertToAlertRule(rule, rule.state); + }); - if (rule.state !== 'paused') { - if (rule.executionError) { - rule.info = 'Execution Error: ' + rule.executionError; - } - if (rule.evalData && rule.evalData.noData) { - rule.info = 'Query returned no data'; - } - } - } - - return { items: alertRules, searchQuery: state.searchQuery }; + return { items: alertRulesViewModel, searchQuery: state.searchQuery }; } case ActionTypes.SetSearchQuery: diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 1f17962a70b..debfcf58ac8 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -22,6 +22,21 @@ export type UrlQueryMap = { [s: string]: UrlQueryValue }; // Alerting // +export interface AlertRuleApi { + id: number; + dashboardId: number; + dashboardUid: string; + dashboardSlug: string; + panelId: number; + name: string; + state: string; + newStateDate: string; + evalDate: string; + evalData?: object; + executionError: string; + url: string; +} + export interface AlertRule { id: number; dashboardId: number; From 41dcd7641b273903de4a1f0d1274cb4b151bf7e5 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 4 Sep 2018 16:13:51 +0200 Subject: [PATCH 038/666] removed unused mobx state --- public/app/containers/ContainerProps.ts | 4 -- .../AlertListStore/AlertListStore.test.ts | 66 ------------------- .../stores/AlertListStore/AlertListStore.ts | 47 ------------- public/app/stores/AlertListStore/AlertRule.ts | 34 ---------- public/app/stores/AlertListStore/helpers.ts | 13 ---- public/app/stores/RootStore/RootStore.ts | 8 --- .../app/stores/ServerStatsStore/ServerStat.ts | 6 -- .../ServerStatsStore/ServerStatsStore.ts | 24 ------- 8 files changed, 202 deletions(-) delete mode 100644 public/app/stores/AlertListStore/AlertListStore.test.ts delete mode 100644 public/app/stores/AlertListStore/AlertListStore.ts delete mode 100644 public/app/stores/AlertListStore/AlertRule.ts delete mode 100644 public/app/stores/AlertListStore/helpers.ts delete mode 100644 public/app/stores/ServerStatsStore/ServerStat.ts delete mode 100644 public/app/stores/ServerStatsStore/ServerStatsStore.ts diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts index 97889278fdc..903edd83567 100644 --- a/public/app/containers/ContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -1,16 +1,12 @@ import { SearchStore } from './../stores/SearchStore/SearchStore'; -import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore'; import { NavStore } from './../stores/NavStore/NavStore'; import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { AlertListStore } from './../stores/AlertListStore/AlertListStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; import { FolderStore } from './../stores/FolderStore/FolderStore'; interface ContainerProps { search: typeof SearchStore.Type; - serverStats: typeof ServerStatsStore.Type; nav: typeof NavStore.Type; - alertList: typeof AlertListStore.Type; permissions: typeof PermissionsStore.Type; view: typeof ViewStore.Type; folder: typeof FolderStore.Type; diff --git a/public/app/stores/AlertListStore/AlertListStore.test.ts b/public/app/stores/AlertListStore/AlertListStore.test.ts deleted file mode 100644 index f0ab24b6cfc..00000000000 --- a/public/app/stores/AlertListStore/AlertListStore.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { AlertListStore } from './AlertListStore'; -import { backendSrv } from 'test/mocks/common'; -import moment from 'moment'; - -function getRule(name, state, info) { - return { - id: 11, - dashboardId: 58, - panelId: 3, - name: name, - state: state, - newStateDate: moment() - .subtract(5, 'minutes') - .format(), - evalData: {}, - executionError: '', - url: 'db/mygool', - stateText: state, - stateIcon: 'fa', - stateClass: 'asd', - stateAge: '10m', - info: info, - canEdit: true, - }; -} - -describe('AlertListStore', () => { - let store; - - beforeAll(() => { - store = AlertListStore.create( - { - rules: [ - getRule('Europe', 'OK', 'backend-01'), - getRule('Google', 'ALERTING', 'backend-02'), - getRule('Amazon', 'PAUSED', 'backend-03'), - getRule('West-Europe', 'PAUSED', 'backend-03'), - ], - search: '', - }, - { - backendSrv: backendSrv, - } - ); - }); - - it('search should filter list on name', () => { - store.setSearchQuery('urope'); - expect(store.filteredRules).toHaveLength(2); - }); - - it('search should filter list on state', () => { - store.setSearchQuery('ale'); - expect(store.filteredRules).toHaveLength(1); - }); - - it('search should filter list on info', () => { - store.setSearchQuery('-0'); - expect(store.filteredRules).toHaveLength(4); - }); - - it('search should be equal', () => { - store.setSearchQuery('alert'); - expect(store.search).toBe('alert'); - }); -}); diff --git a/public/app/stores/AlertListStore/AlertListStore.ts b/public/app/stores/AlertListStore/AlertListStore.ts deleted file mode 100644 index c2b9f5e4962..00000000000 --- a/public/app/stores/AlertListStore/AlertListStore.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; -import { AlertRule as AlertRuleModel } from './AlertRule'; -import { setStateFields } from './helpers'; - -type AlertRuleType = typeof AlertRuleModel.Type; -export interface AlertRule extends AlertRuleType {} - -export const AlertListStore = types - .model('AlertListStore', { - rules: types.array(AlertRuleModel), - stateFilter: types.optional(types.string, 'all'), - search: types.optional(types.string, ''), - }) - .views(self => ({ - get filteredRules() { - const regex = new RegExp(self.search, 'i'); - return self.rules.filter(alert => { - return regex.test(alert.name) || regex.test(alert.stateText) || regex.test(alert.info); - }); - }, - })) - .actions(self => ({ - loadRules: flow(function* load(filters) { - const backendSrv = getEnv(self).backendSrv; - self.stateFilter = filters.state; // store state filter used in api query - const apiRules = yield backendSrv.get('/api/alerts', filters); - self.rules.clear(); - - for (const rule of apiRules) { - setStateFields(rule, rule.state); - - if (rule.state !== 'paused') { - if (rule.executionError) { - rule.info = 'Execution Error: ' + rule.executionError; - } - if (rule.evalData && rule.evalData.noData) { - rule.info = 'Query returned no data'; - } - } - - self.rules.push(AlertRuleModel.create(rule)); - } - }), - setSearchQuery(query: string) { - self.search = query; - }, - })); diff --git a/public/app/stores/AlertListStore/AlertRule.ts b/public/app/stores/AlertListStore/AlertRule.ts deleted file mode 100644 index 9c039be6ec2..00000000000 --- a/public/app/stores/AlertListStore/AlertRule.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; -import { setStateFields } from './helpers'; - -export const AlertRule = types - .model('AlertRule', { - id: types.identifier(types.number), - dashboardId: types.number, - panelId: types.number, - name: types.string, - state: types.string, - stateText: types.string, - stateIcon: types.string, - stateClass: types.string, - stateAge: types.string, - info: types.optional(types.string, ''), - url: types.string, - }) - .views(self => ({ - get isPaused() { - return self.state === 'paused'; - }, - })) - .actions(self => ({ - /** - * will toggle alert rule paused state - */ - togglePaused: flow(function* togglePaused() { - const backendSrv = getEnv(self).backendSrv; - const payload = { paused: !self.isPaused }; - const res = yield backendSrv.post(`/api/alerts/${self.id}/pause`, payload); - setStateFields(self, res.state); - self.info = ''; - }), - })); diff --git a/public/app/stores/AlertListStore/helpers.ts b/public/app/stores/AlertListStore/helpers.ts deleted file mode 100644 index 4d1ddcc30e0..00000000000 --- a/public/app/stores/AlertListStore/helpers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import moment from 'moment'; -import alertDef from 'app/features/alerting/state/alertDef'; - -export function setStateFields(rule, state) { - const stateModel = alertDef.getStateDisplayModel(state); - rule.state = state; - rule.stateText = stateModel.text; - rule.stateIcon = stateModel.iconClass; - rule.stateClass = stateModel.stateClass; - rule.stateAge = moment(rule.newStateDate) - .fromNow() - .replace(' ago', ''); -} diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index bb85a85d9dd..5853744a68f 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,8 +1,6 @@ import { types } from 'mobx-state-tree'; import { SearchStore } from './../SearchStore/SearchStore'; -import { ServerStatsStore } from './../ServerStatsStore/ServerStatsStore'; import { NavStore } from './../NavStore/NavStore'; -import { AlertListStore } from './../AlertListStore/AlertListStore'; import { ViewStore } from './../ViewStore/ViewStore'; import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; @@ -12,13 +10,7 @@ export const RootStore = types.model({ search: types.optional(SearchStore, { sections: [], }), - serverStats: types.optional(ServerStatsStore, { - stats: [], - }), nav: types.optional(NavStore, {}), - alertList: types.optional(AlertListStore, { - rules: [], - }), permissions: types.optional(PermissionsStore, { fetching: false, items: [], diff --git a/public/app/stores/ServerStatsStore/ServerStat.ts b/public/app/stores/ServerStatsStore/ServerStat.ts deleted file mode 100644 index bd819a51e76..00000000000 --- a/public/app/stores/ServerStatsStore/ServerStat.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { types } from 'mobx-state-tree'; - -export const ServerStat = types.model('ServerStat', { - name: types.string, - value: types.optional(types.number, 0), -}); diff --git a/public/app/stores/ServerStatsStore/ServerStatsStore.ts b/public/app/stores/ServerStatsStore/ServerStatsStore.ts deleted file mode 100644 index d27285d7a3b..00000000000 --- a/public/app/stores/ServerStatsStore/ServerStatsStore.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; -import { ServerStat } from './ServerStat'; - -export const ServerStatsStore = types - .model('ServerStatsStore', { - stats: types.array(ServerStat), - error: types.optional(types.string, ''), - }) - .actions(self => ({ - load: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.get('/api/admin/stats'); - self.stats.clear(); - self.stats.push(ServerStat.create({ name: 'Total dashboards', value: res.dashboards })); - self.stats.push(ServerStat.create({ name: 'Total users', value: res.users })); - self.stats.push(ServerStat.create({ name: 'Active users (seen last 30 days)', value: res.activeUsers })); - self.stats.push(ServerStat.create({ name: 'Total orgs', value: res.orgs })); - self.stats.push(ServerStat.create({ name: 'Total playlists', value: res.playlists })); - self.stats.push(ServerStat.create({ name: 'Total snapshots', value: res.snapshots })); - self.stats.push(ServerStat.create({ name: 'Total dashboard tags', value: res.tags })); - self.stats.push(ServerStat.create({ name: 'Total starred dashboards', value: res.stars })); - self.stats.push(ServerStat.create({ name: 'Total alerts', value: res.alerts })); - }), - })); From 1e5ad4da7894d1b617ac221fd06e2798a90c2893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 4 Sep 2018 12:59:46 -0700 Subject: [PATCH 039/666] redux: minor changes to redux thunk actions and use of typings --- .../features/alerting/AlertRuleList.test.tsx | 2 +- .../app/features/alerting/AlertRuleList.tsx | 2 +- public/app/features/alerting/state/actions.ts | 34 ++++++------------- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 9bcdcd41a3b..3c0c410d1a9 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import AlertRuleList, { Props } from './AlertRuleList'; +import { AlertRuleList, Props } from './AlertRuleList'; import { AlertRule, NavModel } from '../../types'; import appEvents from '../../core/app_events'; diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index e8458e72f11..4b48da47256 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -20,7 +20,7 @@ export interface Props { search: string; } -class AlertRuleList extends PureComponent { +export class AlertRuleList extends PureComponent { stateFilters = [ { text: 'All', value: 'all' }, { text: 'OK', value: 'ok' }, diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 2dff257685f..ca50d9e1038 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,6 +1,6 @@ -import { Dispatch } from 'redux'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { AlertRuleApi, StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; export enum ActionTypes { LoadAlertRules = 'LOAD_ALERT_RULES', @@ -29,31 +29,19 @@ export const setSearchQuery = (query: string): SetSearchQueryAction => ({ export type Action = LoadAlertRulesAction | SetSearchQueryAction; -export const getAlertRulesAsync = (options: { state: string }) => async ( - dispatch: Dispatch -): Promise => { - try { +type ThunkResult = ThunkAction; + +export function getAlertRulesAsync(options: { state: string }): ThunkResult { + return async dispatch => { const rules = await getBackendSrv().get('/api/alerts', options); dispatch(loadAlertRules(rules)); - return rules; - } catch (error) { - console.error(error); - throw error; - } -}; + }; +} -export const togglePauseAlertRule = (id: number, options: { paused: boolean }) => async ( - // Maybe fix dispatch type? - dispatch: Dispatch, - getState: () => StoreState -): Promise => { - try { +export function togglePauseAlertRule(id: number, options: { paused: boolean }): ThunkResult { + return async (dispatch, getState) => { await getBackendSrv().post(`/api/alerts/${id}/pause`, options); const stateFilter = getState().location.query.state || 'all'; dispatch(getAlertRulesAsync({ state: stateFilter.toString() })); - return true; - } catch (error) { - console.log(error); - throw error; - } -}; + }; +} From 68767acb1c8a027a2dde7967d64a2adba24c80bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 4 Sep 2018 13:15:45 -0700 Subject: [PATCH 040/666] fix: Updated test --- public/app/features/alerting/AlertRuleList.test.tsx | 6 +----- .../alerting/__snapshots__/AlertRuleList.test.tsx.snap | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 3c0c410d1a9..70892d86589 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -4,10 +4,6 @@ import { AlertRuleList, Props } from './AlertRuleList'; import { AlertRule, NavModel } from '../../types'; import appEvents from '../../core/app_events'; -jest.mock('react-redux', () => ({ - connect: () => params => params, -})); - jest.mock('../../core/app_events', () => ({ emit: jest.fn(), })); @@ -93,7 +89,7 @@ describe('Life cycle', () => { const { instance } = setup(); instance.fetchRules = jest.fn(); - instance.componentDidUpdate({ stateFilter: 'ok' }); + instance.componentDidUpdate({ stateFilter: 'ok' } as Props); expect(instance.fetchRules).toHaveBeenCalled(); }); diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap index 99869ba6126..46000d974db 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap @@ -101,7 +101,7 @@ exports[`Render should render alert rules 1`] = `
      - - Date: Tue, 4 Sep 2018 22:38:18 -0700 Subject: [PATCH 041/666] mobx: removed unused SearchStore --- public/app/containers/ContainerProps.ts | 2 -- .../core/components/search/SearchResult.tsx | 14 ++-------- public/app/stores/RootStore/RootStore.ts | 4 --- public/app/stores/SearchStore/ResultItem.ts | 10 ------- .../stores/SearchStore/SearchResultSection.ts | 27 ------------------- public/app/stores/SearchStore/SearchStore.ts | 22 --------------- 6 files changed, 2 insertions(+), 77 deletions(-) delete mode 100644 public/app/stores/SearchStore/ResultItem.ts delete mode 100644 public/app/stores/SearchStore/SearchResultSection.ts delete mode 100644 public/app/stores/SearchStore/SearchStore.ts diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts index 903edd83567..ce09b992f80 100644 --- a/public/app/containers/ContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -1,11 +1,9 @@ -import { SearchStore } from './../stores/SearchStore/SearchStore'; import { NavStore } from './../stores/NavStore/NavStore'; import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; import { FolderStore } from './../stores/FolderStore/FolderStore'; interface ContainerProps { - search: typeof SearchStore.Type; nav: typeof NavStore.Type; permissions: typeof PermissionsStore.Type; view: typeof ViewStore.Type; diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx index 3141d29ac7f..13333c168f9 100644 --- a/public/app/core/components/search/SearchResult.tsx +++ b/public/app/core/components/search/SearchResult.tsx @@ -1,22 +1,13 @@ import React from 'react'; import classNames from 'classnames'; -import { observer } from 'mobx-react'; -import { store } from 'app/stores/store'; -export interface SearchResultProps { - search: any; -} - -@observer -export class SearchResult extends React.Component { +export class SearchResult extends React.Component { constructor(props) { super(props); this.state = { - search: store.search, + search: '', }; - - store.search.query(); } render() { @@ -30,7 +21,6 @@ export interface SectionProps { section: any; } -@observer export class SearchResultSection extends React.Component { constructor(props) { super(props); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 5853744a68f..fba25e5f015 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,5 +1,4 @@ import { types } from 'mobx-state-tree'; -import { SearchStore } from './../SearchStore/SearchStore'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; import { FolderStore } from './../FolderStore/FolderStore'; @@ -7,9 +6,6 @@ import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; import { TeamsStore } from './../TeamsStore/TeamsStore'; export const RootStore = types.model({ - search: types.optional(SearchStore, { - sections: [], - }), nav: types.optional(NavStore, {}), permissions: types.optional(PermissionsStore, { fetching: false, diff --git a/public/app/stores/SearchStore/ResultItem.ts b/public/app/stores/SearchStore/ResultItem.ts deleted file mode 100644 index eb0ff021526..00000000000 --- a/public/app/stores/SearchStore/ResultItem.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { types } from 'mobx-state-tree'; - -export const ResultItem = types.model('ResultItem', { - id: types.identifier(types.number), - folderId: types.optional(types.number, 0), - title: types.string, - url: types.string, - icon: types.string, - folderTitle: types.optional(types.string, ''), -}); diff --git a/public/app/stores/SearchStore/SearchResultSection.ts b/public/app/stores/SearchStore/SearchResultSection.ts deleted file mode 100644 index 70b3ad48e96..00000000000 --- a/public/app/stores/SearchStore/SearchResultSection.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { types } from 'mobx-state-tree'; -import { ResultItem } from './ResultItem'; - -export const SearchResultSection = types - .model('SearchResultSection', { - id: types.identifier(), - title: types.string, - icon: types.string, - expanded: types.boolean, - items: types.array(ResultItem), - }) - .actions(self => ({ - toggle() { - self.expanded = !self.expanded; - - for (let i = 0; i < 100; i++) { - self.items.push( - ResultItem.create({ - id: i, - title: 'Dashboard ' + self.items.length, - icon: 'gicon gicon-dashboard', - url: 'asd', - }) - ); - } - }, - })); diff --git a/public/app/stores/SearchStore/SearchStore.ts b/public/app/stores/SearchStore/SearchStore.ts deleted file mode 100644 index 36897f05f38..00000000000 --- a/public/app/stores/SearchStore/SearchStore.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { types } from 'mobx-state-tree'; -import { SearchResultSection } from './SearchResultSection'; - -export const SearchStore = types - .model('SearchStore', { - sections: types.array(SearchResultSection), - }) - .actions(self => ({ - query() { - for (let i = 0; i < 100; i++) { - self.sections.push( - SearchResultSection.create({ - id: 'starred' + i, - title: 'starred', - icon: 'fa fa-fw fa-star-o', - expanded: false, - items: [], - }) - ); - } - }, - })); From 306c3e6c10fe755e58ddfe622749f4bd0f2c11bd Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 12:34:32 +0200 Subject: [PATCH 042/666] creating types, actions, reducer --- .../teams}/TeamGroupSync.tsx | 0 .../Teams => features/teams}/TeamList.tsx | 17 ++++++++- .../Teams => features/teams}/TeamMembers.tsx | 0 .../Teams => features/teams}/TeamPages.tsx | 0 .../Teams => features/teams}/TeamSettings.tsx | 0 public/app/features/teams/state/actions.ts | 28 +++++++++++++++ public/app/features/teams/state/reducers.ts | 14 ++++++++ public/app/features/teams/state/selectors.ts | 1 + public/app/routes/routes.ts | 4 +-- public/app/types/index.ts | 36 +++++++++++++++++++ 10 files changed, 97 insertions(+), 3 deletions(-) rename public/app/{containers/Teams => features/teams}/TeamGroupSync.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamList.tsx (89%) rename public/app/{containers/Teams => features/teams}/TeamMembers.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamPages.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamSettings.tsx (100%) create mode 100644 public/app/features/teams/state/actions.ts create mode 100644 public/app/features/teams/state/reducers.ts create mode 100644 public/app/features/teams/state/selectors.ts diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx similarity index 100% rename from public/app/containers/Teams/TeamGroupSync.tsx rename to public/app/features/teams/TeamGroupSync.tsx diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx similarity index 89% rename from public/app/containers/Teams/TeamList.tsx rename to public/app/features/teams/TeamList.tsx index d0feee75184..79d71c33596 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; @@ -6,6 +7,8 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { loadTeams } from './state/actions'; +import { getTeams } from './state/selectors'; interface Props { nav: typeof NavStore.Type; @@ -108,4 +111,16 @@ export class TeamList extends React.Component { } } -export default hot(module)(TeamList); +function mapStateToProps(state) { + return { + teams: getTeams(state), + }; +} + +function mapDispatchToProps() { + return { + loadTeams, + }; +} + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx similarity index 100% rename from public/app/containers/Teams/TeamMembers.tsx rename to public/app/features/teams/TeamMembers.tsx diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx similarity index 100% rename from public/app/containers/Teams/TeamPages.tsx rename to public/app/features/teams/TeamPages.tsx diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx similarity index 100% rename from public/app/containers/Teams/TeamSettings.tsx rename to public/app/features/teams/TeamSettings.tsx diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts new file mode 100644 index 00000000000..fafd2091217 --- /dev/null +++ b/public/app/features/teams/state/actions.ts @@ -0,0 +1,28 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState, Team } from '../../../types'; + +export enum ActionTypes { + LoadTeams = 'LOAD_TEAMS', +} + +export interface LoadTeamsAction { + type: ActionTypes.LoadTeams; + payload: Team[]; +} + +export type Action = LoadTeamsAction; + +type ThunkResult = ThunkAction; + +const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ + type: ActionTypes.LoadTeams, + payload: teams, +}); + +export function loadTeams(): ThunkResult { + return async dispatch => { + const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); + dispatch(teamsLoaded(teams)); + }; +} diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts new file mode 100644 index 00000000000..a104ae2e21c --- /dev/null +++ b/public/app/features/teams/state/reducers.ts @@ -0,0 +1,14 @@ +import { TeamsState } from '../../../types'; +import { Action } from './actions'; + +const initialState: TeamsState = { teams: [] }; + +export const teamsReducer = (state = initialState, action: Action): TeamsState => { + switch (action.type) { + } + return state; +}; + +export default { + teams: teamsReducer, +}; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts new file mode 100644 index 00000000000..f1f66695e65 --- /dev/null +++ b/public/app/features/teams/state/selectors.ts @@ -0,0 +1 @@ +export const getTeams = state => state.teams; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index dfd215f7056..1fd1a474cd3 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/containers/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import TeamPages from 'app/containers/Teams/TeamPages'; -import TeamList from 'app/containers/Teams/TeamList'; +import TeamPages from 'app/features/teams/TeamPages'; +import TeamList from 'app/features/teams/TeamList'; /** @ngInject **/ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..73cb05c26c1 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,9 @@ // Location // +import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; +import { types } from 'mobx-state-tree'; + export interface LocationUpdate { path?: string; query?: UrlQueryMap; @@ -53,6 +56,34 @@ export interface AlertRule { evalData?: { noData: boolean }; } +// +// Teams +// + +export interface Team { + id: number; + name: string; + avatarUrl: string; + email: string; + memberCount: number; + search?: string; + members?: TeamMember[]; + groups?: TeamGroup[]; +} + +export interface TeamMember { + userId: number; + teamId: number; + avatarUrl: string; + email: string; + login: string; +} + +export interface TeamGroup { + groupId: string; + teamId: number; +} + // // NavModel // @@ -89,8 +120,13 @@ export interface AlertRulesState { searchQuery: string; } +export interface TeamsState { + teams: Team[]; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; alertRules: AlertRulesState; + teams: TeamsState; } From 167f0098193475a0ece4c280d26d1dfa4fee5d1a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:13:21 +0200 Subject: [PATCH 043/666] load teams and store in redux --- .../app/features/alerting/AlertRuleList.tsx | 4 +- public/app/features/teams/TeamList.test.tsx | 61 ++++++ public/app/features/teams/TeamList.tsx | 65 +++--- .../__snapshots__/TeamList.test.tsx.snap | 204 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 4 +- public/app/features/teams/state/reducers.ts | 4 +- public/app/stores/configureStore.ts | 2 + public/app/types/index.ts | 3 - 8 files changed, 305 insertions(+), 42 deletions(-) create mode 100644 public/app/features/teams/TeamList.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamList.test.tsx.snap diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 4b48da47256..d30ba0ba802 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -115,7 +115,9 @@ export class AlertRuleList extends PureComponent {
      - {alertRules.map(rule => )} + {alertRules.map(rule => ( + {}} /> + ))}
    diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx new file mode 100644 index 00000000000..e7db3edfd3d --- /dev/null +++ b/public/app/features/teams/TeamList.test.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamList, Props } from './TeamList'; +import { NavModel, Team } from '../../types'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teams: [] as Team[], + loadTeams: jest.fn(), + search: '', + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamList; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render teams table', () => { + const { wrapper } = setup({ + teams: [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ], + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadTeams', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadTeams).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => {}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 79d71c33596..801b66d24fc 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,44 +1,38 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { NavModel, Team } from '../../types'; import { loadTeams } from './state/actions'; import { getTeams } from './state/selectors'; +import { getNavModel } from 'app/core/selectors/navModel'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - backendSrv: BackendSrv; +export interface Props { + navModel: NavModel; + teams: Team[]; + loadTeams: typeof loadTeams; + search: string; } -@inject('nav', 'teams') -@observer -export class TeamList extends React.Component { - constructor(props) { - super(props); - - this.props.nav.load('cfg', 'teams'); +export class TeamList extends PureComponent { + componentDidMount() { this.fetchTeams(); } - fetchTeams() { - this.props.teams.loadTeams(); + async fetchTeams() { + await this.props.loadTeams(); } - deleteTeam(team: Team) { - this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); - } - - onSearchQueryChange = evt => { - this.props.teams.setSearchQuery(evt.target.value); + deleteTeam = (team: Team) => { + console.log('delete team', team); }; - renderTeamMember(team: Team): JSX.Element { + onSearchQueryChange = event => { + console.log('set search', event.target.value); + }; + + renderTeamMember(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -65,10 +59,11 @@ export class TeamList extends React.Component { } render() { - const { nav, teams } = this.props; + const { navModel, teams, search } = this.props; + return (
    - +
    @@ -77,7 +72,7 @@ export class TeamList extends React.Component { type="text" className="gf-form-input" placeholder="Search teams" - value={teams.search} + value={search} onChange={this.onSearchQueryChange} /> @@ -102,7 +97,7 @@ export class TeamList extends React.Component { - {teams.filteredTeams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeamMember(team))}
    @@ -113,14 +108,14 @@ export class TeamList extends React.Component { function mapStateToProps(state) { return { - teams: getTeams(state), + navModel: getNavModel(state.navIndex, 'teams'), + teams: getTeams(state.teams), + search: '', }; } -function mapDispatchToProps() { - return { - loadTeams, - }; -} +const mapDispatchToProps = { + loadTeams, +}; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap new file mode 100644 index 00000000000..c93dafde1c6 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    +
    +
    + +
    + +
    + + + + + + + + + +
    + + Name + + Email + + Members + +
    +
    +
    +
    +`; + +exports[`Render should render teams table 1`] = ` +
    + +
    +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + +
    + + Name + + Email + + Members + +
    + + + + + + test + + + + test@test.com + + + + 1 + + + +
    +
    +
    +
    +`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index fafd2091217..35853bd73e8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -22,7 +22,7 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ export function loadTeams(): ThunkResult { return async dispatch => { - const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); - dispatch(teamsLoaded(teams)); + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + dispatch(teamsLoaded(response.teams)); }; } diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index a104ae2e21c..968c69d862c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,10 +1,12 @@ import { TeamsState } from '../../../types'; -import { Action } from './actions'; +import { Action, ActionTypes } from './actions'; const initialState: TeamsState = { teams: [] }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { + case ActionTypes.LoadTeams: + return { teams: action.payload }; } return state; }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 232f2e30cb8..a79c59a5fc1 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -3,10 +3,12 @@ import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; +import teamsReducers from 'app/features/teams/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, + ...teamsReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 73cb05c26c1..beb3253787a 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,9 +2,6 @@ // Location // -import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; -import { types } from 'mobx-state-tree'; - export interface LocationUpdate { path?: string; query?: UrlQueryMap; From 7e340b7aa5c1016c934a68d33597672e38e36584 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:32:51 +0200 Subject: [PATCH 044/666] delete team --- public/app/features/teams/TeamList.test.tsx | 38 +++++++++++++-------- public/app/features/teams/TeamList.tsx | 6 ++-- public/app/features/teams/state/actions.ts | 10 ++++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index e7db3edfd3d..7e4caff80ae 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TeamList, Props } from './TeamList'; +import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; const setup = (propOverrides?: object) => { @@ -8,6 +8,7 @@ const setup = (propOverrides?: object) => { navModel: {} as NavModel, teams: [] as Team[], loadTeams: jest.fn(), + deleteTeam: jest.fn(), search: '', }; @@ -22,6 +23,17 @@ const setup = (propOverrides?: object) => { }; }; +const mockTeam: Team = { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], +}; + describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -30,18 +42,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ], + teams: [mockTeam], }); expect(wrapper).toMatchSnapshot(); @@ -58,4 +59,13 @@ describe('Life cycle', () => { }); }); -describe('Functions', () => {}); +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.deleteTeam(mockTeam); + + expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 801b66d24fc..df8776920d7 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,7 +4,7 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams } from './state/actions'; +import { loadTeams, deleteTeam } from './state/actions'; import { getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -12,6 +12,7 @@ export interface Props { navModel: NavModel; teams: Team[]; loadTeams: typeof loadTeams; + deleteTeam: typeof deleteTeam; search: string; } @@ -25,7 +26,7 @@ export class TeamList extends PureComponent { } deleteTeam = (team: Team) => { - console.log('delete team', team); + this.props.deleteTeam(team.id); }; onSearchQueryChange = event => { @@ -116,6 +117,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadTeams, + deleteTeam, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35853bd73e8..6afed1828c1 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -26,3 +26,13 @@ export function loadTeams(): ThunkResult { dispatch(teamsLoaded(response.teams)); }; } + +export function deleteTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .delete(`/api/teams/${id}`) + .then(() => { + dispatch(loadTeams()); + }); + }; +} From f68ac2021873bcc827de00423a5af90daab34faa Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 16:49:36 +0200 Subject: [PATCH 045/666] set search query action and tests --- public/app/features/teams/TeamList.test.tsx | 30 +-- public/app/features/teams/TeamList.tsx | 20 +- .../app/features/teams/__mocks__/teamMocks.ts | 32 +++ .../__snapshots__/TeamList.test.tsx.snap | 204 +++++++++++++++++- public/app/features/teams/state/actions.ts | 13 +- .../app/features/teams/state/reducers.test.ts | 41 ++++ public/app/features/teams/state/reducers.ts | 7 +- .../features/teams/state/selectors.test.ts | 25 +++ public/app/features/teams/state/selectors.ts | 10 +- public/app/types/index.ts | 1 + 10 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 public/app/features/teams/__mocks__/teamMocks.ts create mode 100644 public/app/features/teams/state/reducers.test.ts create mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 7e4caff80ae..6c12f1357e5 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; +import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { @@ -9,7 +10,8 @@ const setup = (propOverrides?: object) => { teams: [] as Team[], loadTeams: jest.fn(), deleteTeam: jest.fn(), - search: '', + setSearchQuery: jest.fn(), + searchQuery: '', }; Object.assign(props, propOverrides); @@ -23,17 +25,6 @@ const setup = (propOverrides?: object) => { }; }; -const mockTeam: Team = { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], -}; - describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -42,7 +33,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [mockTeam], + teams: getMultipleMockTeams(5), }); expect(wrapper).toMatchSnapshot(); @@ -63,9 +54,20 @@ describe('Functions', () => { describe('Delete team', () => { it('should call delete team', () => { const { instance } = setup(); - instance.deleteTeam(mockTeam); + instance.deleteTeam(getMockTeam()); expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); }); }); + + describe('on search query change', () => { + it('should call setSearchQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'test' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test'); + }); + }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index df8776920d7..a95f0f17a27 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,8 +4,8 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams, deleteTeam } from './state/actions'; -import { getTeams } from './state/selectors'; +import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; +import { getSearchQuery, getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; export interface Props { @@ -13,7 +13,8 @@ export interface Props { teams: Team[]; loadTeams: typeof loadTeams; deleteTeam: typeof deleteTeam; - search: string; + setSearchQuery: typeof setSearchQuery; + searchQuery: string; } export class TeamList extends PureComponent { @@ -30,10 +31,10 @@ export class TeamList extends PureComponent { }; onSearchQueryChange = event => { - console.log('set search', event.target.value); + this.props.setSearchQuery(event.target.value); }; - renderTeamMember(team: Team) { + renderTeam(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -60,7 +61,7 @@ export class TeamList extends PureComponent { } render() { - const { navModel, teams, search } = this.props; + const { navModel, teams, searchQuery } = this.props; return (
    @@ -73,7 +74,7 @@ export class TeamList extends PureComponent { type="text" className="gf-form-input" placeholder="Search teams" - value={search} + value={searchQuery} onChange={this.onSearchQueryChange} /> @@ -98,7 +99,7 @@ export class TeamList extends PureComponent { - {teams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeam(team))}
    @@ -111,13 +112,14 @@ function mapStateToProps(state) { return { navModel: getNavModel(state.navIndex, 'teams'), teams: getTeams(state.teams), - search: '', + searchQuery: getSearchQuery(state.teams), }; } const mapDispatchToProps = { loadTeams, deleteTeam, + setSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts new file mode 100644 index 00000000000..34405d2ce91 --- /dev/null +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -0,0 +1,32 @@ +import { Team } from '../../../types'; + +export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { + let teams: Team[] = []; + for (let i = 1; i <= numberOfTeams; i++) { + teams.push({ + id: i, + name: `test-${i}`, + avatarUrl: 'some/url/', + email: `test-${i}@test.com`, + memberCount: i, + search: '', + members: [], + groups: [], + }); + } + + return teams; +}; + +export const getMockTeam = (): Team => { + return { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index c93dafde1c6..6ea189f5dbd 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -167,7 +167,7 @@ exports[`Render should render teams table 1`] = ` - test + test-1 - test@test.com + test-1@test.com + + + + + + + + + test-2 + + + + + test-2@test.com + + + + + 2 + + + + + + + + + + + + + + + test-3 + + + + + test-3@test.com + + + + + 3 + + + + + + + + + + + + + + + test-4 + + + + + test-4@test.com + + + + + 4 + + + + + + + + + + + + + + + test-5 + + + + + test-5@test.com + + + + + 5 + + + + + +
    diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 6afed1828c1..5914a932ad0 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -4,6 +4,7 @@ import { StoreState, Team } from '../../../types'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + SetSearchQuery = 'SET_SEARCH_QUERY', } export interface LoadTeamsAction { @@ -11,7 +12,12 @@ export interface LoadTeamsAction { payload: Team[]; } -export type Action = LoadTeamsAction; +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + +export type Action = LoadTeamsAction | SetSearchQueryAction; type ThunkResult = ThunkAction; @@ -20,6 +26,11 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ payload: teams, }); +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: searchQuery, +}); + export function loadTeams(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts new file mode 100644 index 00000000000..e115d311e37 --- /dev/null +++ b/public/app/features/teams/state/reducers.test.ts @@ -0,0 +1,41 @@ +import { Action, ActionTypes } from './actions'; +import { initialState, teamsReducer } from './reducers'; + +describe('teams reducer', () => { + it('should set teams', () => { + const payload = [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.teams).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test'; + + const action: Action = { + type: ActionTypes.SetSearchQuery, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.searchQuery).toEqual('test'); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 968c69d862c..673fd240668 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,12 +1,15 @@ import { TeamsState } from '../../../types'; import { Action, ActionTypes } from './actions'; -const initialState: TeamsState = { teams: [] }; +export const initialState: TeamsState = { teams: [], searchQuery: '' }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: - return { teams: action.payload }; + return { ...state, teams: action.payload }; + + case ActionTypes.SetSearchQuery: + return { ...state, searchQuery: action.payload }; } return state; }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts new file mode 100644 index 00000000000..66fd07444ce --- /dev/null +++ b/public/app/features/teams/state/selectors.test.ts @@ -0,0 +1,25 @@ +import { getTeams } from './selectors'; +import { getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState } from '../../../types'; + +describe('Team selectors', () => { + describe('Get teams', () => { + const mockTeams = getMultipleMockTeams(5); + + it('should return teams if no search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '' }; + + const teams = getTeams(mockState); + + expect(teams).toEqual(mockTeams); + }); + + it('Should filter teams if search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' }; + + const teams = getTeams(mockState); + + expect(teams.length).toEqual(1); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index f1f66695e65..632bb2cd02a 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1 +1,9 @@ -export const getTeams = state => state.teams; +export const getSearchQuery = state => state.searchQuery; + +export const getTeams = state => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.teams.filter(team => { + return regex.test(team.name); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index beb3253787a..b867a8f6989 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -119,6 +119,7 @@ export interface AlertRulesState { export interface TeamsState { teams: Team[]; + searchQuery: string; } export interface StoreState { From 6b863e3b0f424229de23149d6f31492a89976160 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 7 Sep 2018 10:21:10 +0200 Subject: [PATCH 046/666] Fix quoting to handle non-string values --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index fd0987f2761..04464978140 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -44,15 +44,15 @@ export default class PostgresQuery { } quoteIdentifier(value) { - return '"' + value.replace(/"/g, '""') + '"'; + return '"' + String(value).replace(/"/g, '""') + '"'; } quoteLiteral(value) { - return "'" + value.replace(/'/g, "''") + "'"; + return "'" + String(value).replace(/'/g, "''") + "'"; } escapeLiteral(value) { - return value.replace(/'/g, "''"); + return String(value).replace(/'/g, "''"); } hasTimeGroup() { From 05bfc3651626b82845626820e23c73e4fd57b2ce Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 14:32:09 +0200 Subject: [PATCH 047/666] Teampages page --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 14 ++- public/app/core/reducers/navModel.ts | 18 ++- public/app/core/selectors/location.ts | 3 + public/app/core/selectors/navModel.ts | 2 +- public/app/features/teams/TeamGroupSync.tsx | 12 +- public/app/features/teams/TeamMembers.tsx | 27 ++--- public/app/features/teams/TeamPages.test.tsx | 63 +++++++++++ public/app/features/teams/TeamPages.tsx | 107 +++++++++++------- public/app/features/teams/TeamSettings.tsx | 10 +- .../features/teams/__mocks__/navModelMock.ts | 59 ++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 87 ++++++++++++++ public/app/features/teams/state/actions.ts | 57 +++++++++- .../app/features/teams/state/reducers.test.ts | 6 +- public/app/features/teams/state/reducers.ts | 17 ++- public/app/features/teams/state/selectors.ts | 2 + public/app/types/index.ts | 7 +- 17 files changed, 410 insertions(+), 84 deletions(-) create mode 100644 public/app/core/selectors/location.ts create mode 100644 public/app/features/teams/TeamPages.test.tsx create mode 100644 public/app/features/teams/__mocks__/navModelMock.ts create mode 100644 public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 74b61f845c0..b4b9b21126e 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { updateLocation } from './location'; +import { updateNavIndex } from './navModel'; -export { updateLocation }; +export { updateLocation, updateNavIndex }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 56d129fd263..96465c6ef60 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,3 +1,9 @@ +import { NavModelItem } from '../../types'; + +export enum ActionTypes { + UpdateNavIndex = 'UPDATE_NAV_INDEX', +} + export type Action = UpdateNavIndexAction; // this action is not used yet @@ -5,9 +11,11 @@ export type Action = UpdateNavIndexAction; // like datasource edit, teams edit page export interface UpdateNavIndexAction { - type: 'UPDATE_NAV_INDEX'; + type: ActionTypes.UpdateNavIndex; + payload: NavModelItem; } -export const updateNavIndex = (): UpdateNavIndexAction => ({ - type: 'UPDATE_NAV_INDEX', +export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({ + type: ActionTypes.UpdateNavIndex, + payload: item, }); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 26acdb39a3d..ac0e51854e7 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,5 +1,5 @@ -import { Action } from 'app/core/actions/navModel'; -import { NavModelItem, NavIndex } from 'app/types'; +import { Action, ActionTypes } from 'app/core/actions/navModel'; +import { NavIndex, NavModelItem } from 'app/types'; import config from 'app/core/config'; export function buildInitialState(): NavIndex { @@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem? export const initialState: NavIndex = buildInitialState(); export const navIndexReducer = (state = initialState, action: Action): NavIndex => { + switch (action.type) { + case ActionTypes.UpdateNavIndex: + const newPages = {}; + const payload = action.payload; + + for (const node of payload.children) { + newPages[node.id] = { + ...node, + parentItem: payload, + }; + } + + return { ...state, ...newPages }; + } return state; }; diff --git a/public/app/core/selectors/location.ts b/public/app/core/selectors/location.ts new file mode 100644 index 00000000000..adc31f47e89 --- /dev/null +++ b/public/app/core/selectors/location.ts @@ -0,0 +1,3 @@ +export const getRouteParamsId = state => state.routeParams.id; + +export const getRouteParamsPage = state => state.routeParams.page; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index a7e1c3330bd..8b3a3edd84e 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,7 +1,7 @@ import { NavModel, NavModelItem, NavIndex } from 'app/types'; function getNotFoundModel(): NavModel { - var node: NavModelItem = { + const node: NavModelItem = { id: 'not-found', text: 'Page not found', icon: 'fa fa-fw fa-warning', diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index a3b2e4aed14..6562820d717 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Team, TeamGroup } from '../../types'; interface Props { team: Team; @@ -16,7 +15,6 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -@observer export class TeamGroupSync extends React.Component { constructor(props) { super(props); @@ -24,7 +22,7 @@ export class TeamGroupSync extends React.Component { } componentDidMount() { - this.props.team.loadGroups(); + // this.props.team.loadGroups(); } renderGroup(group: TeamGroup) { @@ -49,12 +47,12 @@ export class TeamGroupSync extends React.Component { }; onAddGroup = () => { - this.props.team.addGroup(this.state.newGroupId); + // this.props.team.addGroup(this.state.newGroupId); this.setState({ isAdding: false, newGroupId: '' }); }; onRemoveGroup = (group: TeamGroup) => { - this.props.team.removeGroup(group.groupId); + // this.props.team.removeGroup(group.groupId); }; isNewGroupValid() { @@ -63,7 +61,7 @@ export class TeamGroupSync extends React.Component { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups.values(); + const groups = this.props.team.groups; return (
    diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index b06a547063a..32eb0d09b63 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,10 +1,9 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { Team, TeamMember } from '../../types'; interface Props { team: Team; @@ -15,27 +14,26 @@ interface State { newTeamMember?: User; } -@observer -export class TeamMembers extends React.Component { +export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; } componentDidMount() { - this.props.team.loadMembers(); + // this.props.team.loadMembers(); } onSearchQueryChange = evt => { - this.props.team.setSearchQuery(evt.target.value); + // this.props.team.setSearchQuery(evt.target.value); }; removeMember(member: TeamMember) { - this.props.team.removeMember(member); + // this.props.team.removeMember(member); } removeMemberConfirmed(member: TeamMember) { - this.props.team.removeMember(member); + // this.props.team.removeMember(member); } renderMember(member: TeamMember) { @@ -62,16 +60,15 @@ export class TeamMembers extends React.Component { }; onAddUserToTeam = async () => { - await this.props.team.addMember(this.state.newTeamMember.id); - await this.props.team.loadMembers(); - this.setState({ newTeamMember: null }); + // await this.props.team.addMember(this.state.newTeamMember.id); + // await this.props.team.loadMembers(); + // this.setState({ newTeamMember: null }); }; render() { const { newTeamMember, isAdding } = this.state; - const members = this.props.team.filteredMembers; - const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); const { team } = this.props; + const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return (
    @@ -124,7 +121,7 @@ export class TeamMembers extends React.Component { - {members.map(member => this.renderMember(member))} + {team.members && team.members.map(member => this.renderMember(member))}
    diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx new file mode 100644 index 00000000000..65084d0dc47 --- /dev/null +++ b/public/app/features/teams/TeamPages.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamPages, Props } from './TeamPages'; +import { NavModel, Team } from '../../types'; +import { getMockTeam } from './__mocks__/teamMocks'; + +jest.mock('app/core/config', () => ({ + buildInfo: { isEnterprise: true }, +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teamId: 1, + loadTeam: jest.fn(), + pageName: 'members', + team: {} as Team, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance(); + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render member page if team not empty', () => { + const { wrapper } = setup({ + team: getMockTeam(), + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render settings page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'settings', + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render group sync page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'groupsync', + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2abc9c51535..606e254e7ec 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,77 +1,106 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import _ from 'lodash'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import config from 'app/core/config'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { ViewStore } from 'app/stores/ViewStore/ViewStore'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; +import { NavModel, Team } from '../../types'; +import { loadTeam } from './state/actions'; +import { getTeam } from './state/selectors'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - view: typeof ViewStore.Type; +export interface Props { + team: Team; + loadTeam: typeof loadTeam; + teamId: number; + pageName: string; + navModel: NavModel; } -@inject('nav', 'teams', 'view') -@observer -export class TeamPages extends React.Component { +interface State { isSyncEnabled: boolean; - currentPage: string; +} +enum PageTypes { + Members = 'members', + Settings = 'settings', + GroupSync = 'groupsync', +} + +export class TeamPages extends PureComponent { constructor(props) { super(props); - this.isSyncEnabled = config.buildInfo.isEnterprise; - this.currentPage = this.getCurrentPage(); + this.state = { + isSyncEnabled: config.buildInfo.isEnterprise, + }; + } + componentDidMount() { this.loadTeam(); } async loadTeam() { - const { teams, nav, view } = this.props; + const { loadTeam, teamId } = this.props; - await teams.loadById(view.routeParams.get('id')); - - nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); - } - - getCurrentTeam(): Team { - const { teams, view } = this.props; - return teams.map.get(view.routeParams.get('id')); + await loadTeam(teamId); } getCurrentPage() { const pages = ['members', 'settings', 'groupsync']; - const currentPage = this.props.view.routeParams.get('page'); + const currentPage = this.props.pageName; return _.includes(pages, currentPage) ? currentPage : pages[0]; } - render() { - const { nav } = this.props; - const currentTeam = this.getCurrentTeam(); + renderPage() { + const { team } = this.props; + const { isSyncEnabled } = this.state; + const currentPage = this.getCurrentPage(); - if (!nav.main) { - return null; + switch (currentPage) { + case PageTypes.Members: + return ; + + case PageTypes.Settings: + return ; + + case PageTypes.GroupSync: + return isSyncEnabled && ; } + return null; + } + + render() { + const { team, navModel } = this.props; + return (
    - - {currentTeam && ( -
    - {this.currentPage === 'members' && } - {this.currentPage === 'settings' && } - {this.currentPage === 'groupsync' && this.isSyncEnabled && } -
    - )} + + {team && Object.keys(team).length !== 0 &&
    {this.renderPage()}
    }
    ); } } -export default hot(module)(TeamPages); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + const pageName = getRouteParamsPage(state.location) || 'members'; + + return { + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + teamId: teamId, + pageName: pageName, + team: getTeam(state.team), + }; +} + +const mapDispatchToProps = { + loadTeam, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages)); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 0de60a0b16c..6e3c90d93f9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,30 +1,28 @@ import React from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team } from 'app/stores/TeamsStore/TeamsStore'; import { Label } from 'app/core/components/Forms/Forms'; +import { Team } from '../../types'; interface Props { team: Team; } -@observer export class TeamSettings extends React.Component { constructor(props) { super(props); } onChangeName = evt => { - this.props.team.setName(evt.target.value); + // this.props.team.setName(evt.target.value); }; onChangeEmail = evt => { - this.props.team.setEmail(evt.target.value); + // this.props.team.setEmail(evt.target.value); }; onUpdate = evt => { evt.preventDefault(); - this.props.team.update(); + // this.props.team.update(); }; render() { diff --git a/public/app/features/teams/__mocks__/navModelMock.ts b/public/app/features/teams/__mocks__/navModelMock.ts new file mode 100644 index 00000000000..7aa8515ee13 --- /dev/null +++ b/public/app/features/teams/__mocks__/navModelMock.ts @@ -0,0 +1,59 @@ +export const getMockNavModel = (pageName: string) => { + return { + node: { + active: false, + icon: 'gicon gicon-team', + id: `team-${pageName}-2`, + text: `${pageName}`, + url: 'org/teams/edit/2/members', + parentItem: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }, + main: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: true, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap new file mode 100644 index 00000000000..3c19d726e41 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    +`; + +exports[`Render should render group sync page 1`] = ` +
    + +
    + +
    +
    +`; + +exports[`Render should render member page if team not empty 1`] = ` +
    + +
    + +
    +
    +`; + +exports[`Render should render settings page 1`] = ` +
    + +
    + +
    +
    +`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5914a932ad0..35d07157dec 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,9 +1,12 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team } from '../../../types'; +import { updateNavIndex } from '../../../core/actions'; +import { UpdateNavIndexAction } from '../../../core/actions/navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', } @@ -12,20 +15,30 @@ export interface LoadTeamsAction { payload: Team[]; } +export interface LoadTeamAction { + type: ActionTypes.LoadTeam; + payload: Team; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction; +export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ type: ActionTypes.LoadTeams, payload: teams, }); +const teamLoaded = (team: Team): LoadTeamAction => ({ + type: ActionTypes.LoadTeam, + payload: team, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -38,6 +51,44 @@ export function loadTeams(): ThunkResult { }; } +function buildNavModel(team: Team): NavModelItem { + return { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; +} + +export function loadTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .get(`/api/teams/${id}`) + .then(response => { + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index e115d311e37..0ab64a78e41 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from './actions'; -import { initialState, teamsReducer } from './reducers'; +import { initialTeamsState, teamsReducer } from './reducers'; describe('teams reducer', () => { it('should set teams', () => { @@ -21,7 +21,7 @@ describe('teams reducer', () => { payload, }; - const result = teamsReducer(initialState, action); + const result = teamsReducer(initialTeamsState, action); expect(result.teams).toEqual(payload); }); @@ -34,7 +34,7 @@ describe('teams reducer', () => { payload, }; - const result = teamsReducer(initialState, action); + const result = teamsReducer(initialTeamsState, action); expect(result.searchQuery).toEqual('test'); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 673fd240668..56a2f83cd8d 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,9 +1,10 @@ -import { TeamsState } from '../../../types'; +import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; -export const initialState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; -export const teamsReducer = (state = initialState, action: Action): TeamsState => { +export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: return { ...state, teams: action.payload }; @@ -14,6 +15,16 @@ export const teamsReducer = (state = initialState, action: Action): TeamsState = return state; }; +export const teamReducer = (state = initialTeamState, action: Action): TeamState => { + switch (action.type) { + case ActionTypes.LoadTeam: + return { ...state, team: action.payload }; + } + + return state; +}; + export default { teams: teamsReducer, + team: teamReducer, }; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 632bb2cd02a..40940cbae52 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,7 @@ export const getSearchQuery = state => state.searchQuery; +export const getTeam = state => state.team; + export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b867a8f6989..8b0010b6561 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -96,7 +96,7 @@ export interface NavModelItem { hideFromTabs?: boolean; divider?: boolean; children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; + breadcrumbs?: { title: string; url: string }[]; target?: string; parentItem?: NavModelItem; } @@ -122,6 +122,11 @@ export interface TeamsState { searchQuery: string; } +export interface TeamState { + team: Team; + searchQuery: string; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; From c179926a2771490275b08d17fd2bbb55d1769954 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 7 Sep 2018 16:56:31 +0200 Subject: [PATCH 048/666] changelog: release 5.2.4 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b89e925e826..91a69548231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,10 @@ These are new features that's still being worked on and are in an experimental p * **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224) * **Backend**: Upgrade to golang 1.11 [#13030](https://github.com/grafana/grafana/issues/13030) +# 5.2.4 (2018-09-07) + +* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079) + # 5.2.3 (2018-08-29) ### Important fix for LDAP & OAuth login vulnerability From 7a117a6b6c1be25edc17de75be3246fefb30a21b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 7 Sep 2018 16:57:00 +0200 Subject: [PATCH 049/666] release 5.2.4 --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 7b36131fea2..bce09c3283b 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.2.3", - "testing": "5.2.3" + "stable": "5.2.4", + "testing": "5.2.4" } From a440d3510a36af850fb003ad39741f991d8c521b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 Sep 2018 17:55:38 +0200 Subject: [PATCH 050/666] renaming things in admin --- public/app/core/selectors/navModel.ts | 2 +- ...n_edit_org_ctrl.ts => AdminEditOrgCtrl.ts} | 4 +-- ...edit_user_ctrl.ts => AdminEditUserCtrl.ts} | 5 +--- ...list_orgs_ctrl.ts => AdminListOrgsCtrl.ts} | 4 +-- ...st_users_ctrl.ts => AdminListUsersCtrl.ts} | 0 .../{containers => }/ServerStats.test.tsx | 4 +-- .../admin/{containers => }/ServerStats.tsx | 2 +- .../__snapshots__/ServerStats.test.tsx.snap | 0 .../app/features/admin/{admin.ts => index.ts} | 26 ++++++------------- .../admin/{apis/index.ts => state/apis.ts} | 0 public/app/features/all.ts | 2 +- public/app/routes/routes.ts | 2 +- public/test/jest-setup.ts | 12 ++++----- public/test/mocks/common.ts | 2 +- 14 files changed, 24 insertions(+), 41 deletions(-) rename public/app/features/admin/{admin_edit_org_ctrl.ts => AdminEditOrgCtrl.ts} (88%) rename public/app/features/admin/{admin_edit_user_ctrl.ts => AdminEditUserCtrl.ts} (95%) rename public/app/features/admin/{admin_list_orgs_ctrl.ts => AdminListOrgsCtrl.ts} (84%) rename public/app/features/admin/{admin_list_users_ctrl.ts => AdminListUsersCtrl.ts} (100%) rename public/app/features/admin/{containers => }/ServerStats.test.tsx (89%) rename public/app/features/admin/{containers => }/ServerStats.tsx (96%) rename public/app/features/admin/{containers => }/__snapshots__/ServerStats.test.tsx.snap (100%) rename public/app/features/admin/{admin.ts => index.ts} (57%) rename public/app/features/admin/{apis/index.ts => state/apis.ts} (100%) diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index a7e1c3330bd..8b3a3edd84e 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,7 +1,7 @@ import { NavModel, NavModelItem, NavIndex } from 'app/types'; function getNotFoundModel(): NavModel { - var node: NavModelItem = { + const node: NavModelItem = { id: 'not-found', text: 'Page not found', icon: 'fa fa-fw fa-warning', diff --git a/public/app/features/admin/admin_edit_org_ctrl.ts b/public/app/features/admin/AdminEditOrgCtrl.ts similarity index 88% rename from public/app/features/admin/admin_edit_org_ctrl.ts rename to public/app/features/admin/AdminEditOrgCtrl.ts index ec3f8548023..3117c5f0f9b 100644 --- a/public/app/features/admin/admin_edit_org_ctrl.ts +++ b/public/app/features/admin/AdminEditOrgCtrl.ts @@ -1,6 +1,5 @@ -import angular from 'angular'; -export class AdminEditOrgCtrl { +export default class AdminEditOrgCtrl { /** @ngInject */ constructor($scope, $routeParams, backendSrv, $location, navModelSrv) { $scope.init = () => { @@ -48,4 +47,3 @@ export class AdminEditOrgCtrl { } } -angular.module('grafana.controllers').controller('AdminEditOrgCtrl', AdminEditOrgCtrl); diff --git a/public/app/features/admin/admin_edit_user_ctrl.ts b/public/app/features/admin/AdminEditUserCtrl.ts similarity index 95% rename from public/app/features/admin/admin_edit_user_ctrl.ts rename to public/app/features/admin/AdminEditUserCtrl.ts index c34ccdc1cad..bf72c1746aa 100644 --- a/public/app/features/admin/admin_edit_user_ctrl.ts +++ b/public/app/features/admin/AdminEditUserCtrl.ts @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash'; -export class AdminEditUserCtrl { +export default class AdminEditUserCtrl { /** @ngInject */ constructor($scope, $routeParams, backendSrv, $location, navModelSrv) { $scope.user = {}; @@ -117,5 +116,3 @@ export class AdminEditUserCtrl { $scope.init(); } } - -angular.module('grafana.controllers').controller('AdminEditUserCtrl', AdminEditUserCtrl); diff --git a/public/app/features/admin/admin_list_orgs_ctrl.ts b/public/app/features/admin/AdminListOrgsCtrl.ts similarity index 84% rename from public/app/features/admin/admin_list_orgs_ctrl.ts rename to public/app/features/admin/AdminListOrgsCtrl.ts index 0513752aa3e..9190f7f494e 100644 --- a/public/app/features/admin/admin_list_orgs_ctrl.ts +++ b/public/app/features/admin/AdminListOrgsCtrl.ts @@ -1,6 +1,5 @@ -import angular from 'angular'; -export class AdminListOrgsCtrl { +export default class AdminListOrgsCtrl { /** @ngInject */ constructor($scope, backendSrv, navModelSrv) { $scope.init = () => { @@ -33,4 +32,3 @@ export class AdminListOrgsCtrl { } } -angular.module('grafana.controllers').controller('AdminListOrgsCtrl', AdminListOrgsCtrl); diff --git a/public/app/features/admin/admin_list_users_ctrl.ts b/public/app/features/admin/AdminListUsersCtrl.ts similarity index 100% rename from public/app/features/admin/admin_list_users_ctrl.ts rename to public/app/features/admin/AdminListUsersCtrl.ts diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/ServerStats.test.tsx similarity index 89% rename from public/app/features/admin/containers/ServerStats.test.tsx rename to public/app/features/admin/ServerStats.test.tsx index e12dfc3bed4..cbcc580f612 100644 --- a/public/app/features/admin/containers/ServerStats.test.tsx +++ b/public/app/features/admin/ServerStats.test.tsx @@ -2,14 +2,14 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { ServerStats } from './ServerStats'; import { createNavModel } from 'test/mocks/common'; -import { ServerStat } from '../apis'; +import { ServerStat } from './state/apis'; describe('ServerStats', () => { it('Should render table with stats', done => { const navModel = createNavModel('Admin', 'stats'); const stats: ServerStat[] = [{ name: 'Total dashboards', value: 10 }, { name: 'Total Users', value: 1 }]; - let getServerStats = () => { + const getServerStats = () => { return Promise.resolve(stats); }; diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx similarity index 96% rename from public/app/features/admin/containers/ServerStats.tsx rename to public/app/features/admin/ServerStats.tsx index 97419ec9301..40be87ed4d3 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { NavModel, StoreState } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; -import { getServerStats, ServerStat } from '../apis'; +import { getServerStats, ServerStat } from './state/apis'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; interface Props { diff --git a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap similarity index 100% rename from public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap rename to public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/index.ts similarity index 57% rename from public/app/features/admin/admin.ts rename to public/app/features/admin/index.ts index 00e98821779..7fc14791ea7 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/index.ts @@ -1,7 +1,7 @@ -import AdminListUsersCtrl from './admin_list_users_ctrl'; -import './admin_list_orgs_ctrl'; -import './admin_edit_org_ctrl'; -import './admin_edit_user_ctrl'; +import AdminListUsersCtrl from './AdminListUsersCtrl'; +import AdminEditUserCtrl from './AdminEditUserCtrl'; +import AdminListOrgsCtrl from './AdminListOrgsCtrl'; +import AdminEditOrgCtrl from './AdminEditOrgCtrl'; import coreModule from 'app/core/core_module'; @@ -27,21 +27,11 @@ class AdminHomeCtrl { } } -export class AdminStatsCtrl { - stats: any; - navModel: any; +coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl); +coreModule.controller('AdminEditUserCtrl', AdminEditUserCtrl); - /** @ngInject */ - constructor(backendSrv: any, navModelSrv) { - this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-stats', 1); - - backendSrv.get('/api/admin/stats').then(stats => { - this.stats = stats; - }); - } -} +coreModule.controller('AdminListOrgsCtrl', AdminListOrgsCtrl); +coreModule.controller('AdminEditOrgCtrl', AdminEditOrgCtrl); coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl); coreModule.controller('AdminHomeCtrl', AdminHomeCtrl); -coreModule.controller('AdminStatsCtrl', AdminStatsCtrl); -coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl); diff --git a/public/app/features/admin/apis/index.ts b/public/app/features/admin/state/apis.ts similarity index 100% rename from public/app/features/admin/apis/index.ts rename to public/app/features/admin/state/apis.ts diff --git a/public/app/features/all.ts b/public/app/features/all.ts index 065f399cae3..0285e9c352a 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -8,7 +8,7 @@ import './playlist/all'; import './snapshot/all'; import './panel/all'; import './org/all'; -import './admin/admin'; +import './admin'; import './alerting/NotificationsEditCtrl'; import './alerting/NotificationsListCtrl'; import './styleguide/styleguide'; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 86afe685887..0fd76a8c8eb 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,7 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/features/admin/containers/ServerStats'; +import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index f97e4ec4b91..9079754dc28 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -21,19 +21,19 @@ configure({ adapter: new Adapter() }); const global = window as any; global.$ = global.jQuery = $; -const localStorageMock = (function() { - var store = {}; +const localStorageMock = (() => { + let store = {}; return { - getItem: function(key) { + getItem: key => { return store[key]; }, - setItem: function(key, value) { + setItem: (key, value) => { store[key] = value.toString(); }, - clear: function() { + clear: () => { store = {}; }, - removeItem: function(key) { + removeItem: key => { delete store[key]; }, }; diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 1c7bdb4f1e2..385f72621a9 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -31,7 +31,7 @@ export function createNavModel(title: string, ...tabs: string[]): NavModel { breadcrumbs: [], }; - for (let tab of tabs) { + for (const tab of tabs) { node.children.push({ id: tab, icon: 'icon', From 59b3bfd34293e46ef19c091e45f6be7d16fd309b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 18:01:59 +0200 Subject: [PATCH 051/666] team members, bug in fetching team --- public/app/features/teams/TeamMembers.tsx | 68 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 10 +++ public/app/features/teams/state/actions.ts | 67 +++++++++++++++++- .../app/features/teams/state/reducers.test.ts | 37 ++++++---- public/app/features/teams/state/reducers.ts | 8 ++- public/app/features/teams/state/selectors.ts | 8 ++- public/app/types/index.ts | 3 +- 8 files changed, 161 insertions(+), 44 deletions(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 32eb0d09b63..115fb40e184 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,12 +1,21 @@ import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { Team, TeamMember } from '../../types'; +import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; +import { getSearchMemberQuery, getTeam } from './state/selectors'; +import { getRouteParamsId } from '../../core/selectors/location'; interface Props { team: Team; + searchMemberQuery: string; + loadTeamMembers: typeof loadTeamMembers; + addTeamMember: typeof addTeamMember; + removeTeamMember: typeof removeTeamMember; + setSearchMemberQuery: typeof setSearchMemberQuery; } interface State { @@ -21,20 +30,29 @@ export class TeamMembers extends PureComponent { } componentDidMount() { - // this.props.team.loadMembers(); + this.props.loadTeamMembers(); } - onSearchQueryChange = evt => { - // this.props.team.setSearchQuery(evt.target.value); + onSearchQueryChange = event => { + this.props.setSearchMemberQuery(event.target.value); }; removeMember(member: TeamMember) { - // this.props.team.removeMember(member); + this.props.removeTeamMember(member.userId); } - removeMemberConfirmed(member: TeamMember) { - // this.props.team.removeMember(member); - } + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onUserSelected = (user: User) => { + this.setState({ newTeamMember: user }); + }; + + onAddUserToTeam = async () => { + this.props.addTeamMember(this.state.newTeamMember.id); + this.setState({ newTeamMember: null }); + }; renderMember(member: TeamMember) { return ( @@ -51,23 +69,9 @@ export class TeamMembers extends PureComponent { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onUserSelected = (user: User) => { - this.setState({ newTeamMember: user }); - }; - - onAddUserToTeam = async () => { - // await this.props.team.addMember(this.state.newTeamMember.id); - // await this.props.team.loadMembers(); - // this.setState({ newTeamMember: null }); - }; - render() { const { newTeamMember, isAdding } = this.state; - const { team } = this.props; + const { team, searchMemberQuery } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -79,7 +83,7 @@ export class TeamMembers extends PureComponent { type="text" className="gf-form-input" placeholder="Search members" - value={team.search} + value={searchMemberQuery} onChange={this.onSearchQueryChange} /> @@ -129,4 +133,20 @@ export class TeamMembers extends PureComponent { } } -export default hot(module)(TeamMembers); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + searchMemberQuery: getSearchMemberQuery(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamMembers, + addTeamMember, + removeTeamMember, + setSearchMemberQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 606e254e7ec..4395c0bfbef 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -63,7 +63,7 @@ export class TeamPages extends PureComponent { switch (currentPage) { case PageTypes.Members: - return ; + return ; case PageTypes.Settings: return ; @@ -95,7 +95,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), teamId: teamId, pageName: pageName, - team: getTeam(state.team), + team: getTeam(state.team, teamId), }; } diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 34405d2ce91..21c0cf012f0 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -30,3 +30,13 @@ export const getMockTeam = (): Team => { groups: [], }; }; + +export const getMockTeamMember = () => { + return { + userId: 1, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: 'testUser', + }; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35d07157dec..e407737bb20 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,6 +1,6 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; @@ -8,6 +8,8 @@ export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', + SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', + LoadTeamMembers = 'TEAM_MEMBERS_LOADED', } export interface LoadTeamsAction { @@ -20,12 +22,27 @@ export interface LoadTeamAction { payload: Team; } +export interface LoadTeamMembersAction { + type: ActionTypes.LoadTeamMembers; + payload: TeamMember[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; +export interface SetSearchMemberQueryAction { + type: ActionTypes.SetSearchMemberQuery; + payload: string; +} + +export type Action = + | LoadTeamsAction + | SetSearchQueryAction + | LoadTeamAction + | LoadTeamMembersAction + | SetSearchMemberQueryAction; type ThunkResult = ThunkAction; @@ -39,6 +56,16 @@ const teamLoaded = (team: Team): LoadTeamAction => ({ payload: team, }); +const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({ + type: ActionTypes.LoadTeamMembers, + payload: teamMembers, +}); + +export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ + type: ActionTypes.SetSearchMemberQuery, + payload: searchQuery, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -89,6 +116,42 @@ export function loadTeam(id: number): ThunkResult { }; } +export function loadTeamMembers(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/members`) + .then(response => { + dispatch(teamMembersLoaded(response)); + }); + }; +} + +export function addTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/members`, { userId: id }) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + +export function removeTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/members/${id}`) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 0ab64a78e41..492ec71ba4b 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,20 +1,10 @@ import { Action, ActionTypes } from './actions'; -import { initialTeamsState, teamsReducer } from './reducers'; +import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers'; +import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks'; describe('teams reducer', () => { it('should set teams', () => { - const payload = [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ]; + const payload = [getMockTeam()]; const action: Action = { type: ActionTypes.LoadTeams, @@ -39,3 +29,24 @@ describe('teams reducer', () => { expect(result.searchQuery).toEqual('test'); }); }); + +describe('team reducer', () => { + it('should set team members', () => { + const mockTeamMember = getMockTeamMember(); + const mockTeam = getMockTeam(); + const state = { + ...initialTeamState, + team: mockTeam, + }; + + const action: Action = { + type: ActionTypes.LoadTeamMembers, + payload: [mockTeamMember], + }; + + const result = teamReducer(state, action); + const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + + expect(result).toEqual(expectedState); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 56a2f83cd8d..e30fddb22a5 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -2,7 +2,7 @@ import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -19,6 +19,12 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState switch (action.type) { case ActionTypes.LoadTeam: return { ...state, team: action.payload }; + + case ActionTypes.LoadTeamMembers: + return { ...state, team: { ...state.team, members: action.payload } }; + + case ActionTypes.SetSearchMemberQuery: + return { ...state, searchMemberQuery: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 40940cbae52..d6142adf157 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,6 +1,12 @@ export const getSearchQuery = state => state.searchQuery; +export const getSearchMemberQuery = state => state.searchMemberQuery; -export const getTeam = state => state.team; +export const getTeam = (state, currentTeamId) => { + if (state.team.id === currentTeamId) { + console.log('yes'); + return state.team; + } +}; export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 8b0010b6561..27ae3dbe19b 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -124,7 +124,7 @@ export interface TeamsState { export interface TeamState { team: Team; - searchQuery: string; + searchMemberQuery: string; } export interface StoreState { @@ -132,4 +132,5 @@ export interface StoreState { location: LocationState; alertRules: AlertRulesState; teams: TeamsState; + team: TeamState; } From 116fb50530412401ff048ac0fc483975e3c85ac4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 8 Sep 2018 08:24:53 +0200 Subject: [PATCH 052/666] Fix query builder queries for interval start This changes the rate and increase queries to not calculate a value when there is no previous value. This also adds an order by metric column to prevent inconsistent series ordering in the legend. --- .../plugins/datasource/postgres/postgres_query.ts | 9 +++++++-- .../postgres/specs/postgres_query.test.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index fd0987f2761..36b7f6edfc1 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -187,7 +187,8 @@ export default class PostgresQuery { case 'increase': curr = query; prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev; + query += ' WHEN ' + prev + ' IS NULL THEN NULL ELSE ' + curr + ' END)'; break; case 'rate': let timeColumn = this.target.timeColumn; @@ -197,7 +198,8 @@ export default class PostgresQuery { curr = query; prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev; + query += ' WHEN ' + prev + ' IS NULL THEN NULL ELSE ' + curr + ' END)'; query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; break; default: @@ -279,6 +281,9 @@ export default class PostgresQuery { query += this.buildGroupClause(); query += '\nORDER BY 1'; + if (this.hasMetricColumn()) { + query += ',2'; + } return query; } diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts index 0d6f61a8748..42b143c01c8 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts @@ -72,7 +72,9 @@ describe('PostgresQuery', () => { { type: 'window', params: ['increase'] }, ]; expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN v >= lag(v) OVER (ORDER BY time) THEN v - lag(v) OVER (ORDER BY time) ELSE v END) AS "a"' + '(CASE WHEN v >= lag(v) OVER (ORDER BY time) ' + + 'THEN v - lag(v) OVER (ORDER BY time) ' + + 'WHEN lag(v) OVER (ORDER BY time) IS NULL THEN NULL ELSE v END) AS "a"' ); }); @@ -96,7 +98,9 @@ describe('PostgresQuery', () => { { type: 'window', params: ['increase'] }, ]; expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN v >= lag(v) OVER (PARTITION BY host ORDER BY time) THEN v - lag(v) OVER (PARTITION BY host ORDER BY time) ELSE v END) AS "a"' + '(CASE WHEN v >= lag(v) OVER (PARTITION BY host ORDER BY time) ' + + 'THEN v - lag(v) OVER (PARTITION BY host ORDER BY time) ' + + 'WHEN lag(v) OVER (PARTITION BY host ORDER BY time) IS NULL THEN NULL ELSE v END) AS "a"' ); column = [ { type: 'column', params: ['v'] }, @@ -106,7 +110,8 @@ describe('PostgresQuery', () => { ]; expect(query.buildValueColumn(column)).toBe( '(CASE WHEN max(v) >= lag(max(v)) OVER (PARTITION BY host ORDER BY time) ' + - 'THEN max(v) - lag(max(v)) OVER (PARTITION BY host ORDER BY time) ELSE max(v) END) AS "a"' + 'THEN max(v) - lag(max(v)) OVER (PARTITION BY host ORDER BY time) ' + + 'WHEN lag(max(v)) OVER (PARTITION BY host ORDER BY time) IS NULL THEN NULL ELSE max(v) END) AS "a"' ); }); @@ -149,7 +154,7 @@ describe('PostgresQuery', () => { expect(query.buildQuery()).toBe(result); query.target.metricColumn = 'm'; - result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM table\nORDER BY 1'; + result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM table\nORDER BY 1,2'; expect(query.buildQuery()).toBe(result); }); }); From 6bdaf57ae7b340e6b04a7143a70fe6b19395da80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 09:29:53 +0200 Subject: [PATCH 053/666] refactor: changed AlertRuleItem pause action to callback --- .../features/alerting/AlertRuleItem.test.tsx | 2 +- .../app/features/alerting/AlertRuleItem.tsx | 20 +++++-------------- .../features/alerting/AlertRuleList.test.tsx | 1 + .../app/features/alerting/AlertRuleList.tsx | 17 ++++++++++++++-- .../__snapshots__/AlertRuleItem.test.tsx.snap | 2 +- .../__snapshots__/AlertRuleList.test.tsx.snap | 6 ++++-- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/public/app/features/alerting/AlertRuleItem.test.tsx b/public/app/features/alerting/AlertRuleItem.test.tsx index 1b356fa5687..bd37e127c39 100644 --- a/public/app/features/alerting/AlertRuleItem.test.tsx +++ b/public/app/features/alerting/AlertRuleItem.test.tsx @@ -21,7 +21,7 @@ const setup = (propOverrides?: object) => { url: 'https://something.something.darkside', }, search: '', - togglePauseAlertRule: jest.fn(), + onTogglePause: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 0e6b1c5fb90..f47a6348303 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -1,23 +1,15 @@ import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; import Highlighter from 'react-highlight-words'; import classNames from 'classnames/bind'; -import { togglePauseAlertRule } from './state/actions'; import { AlertRule } from '../../types'; export interface Props { rule: AlertRule; search: string; - togglePauseAlertRule: typeof togglePauseAlertRule; + onTogglePause: () => void; } -class AlertRuleItem extends PureComponent { - togglePaused = () => { - const { rule } = this.props; - - this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' }); - }; - +class AlertRuleItem extends PureComponent { renderText(text: string) { return ( { } render() { - const { rule } = this.props; + const { rule, onTogglePause } = this.props; const stateClass = classNames({ fa: true, @@ -61,7 +53,7 @@ class AlertRuleItem extends PureComponent { @@ -74,6 +66,4 @@ class AlertRuleItem extends PureComponent { } } -export default connect(null, { - togglePauseAlertRule, -})(AlertRuleItem); +export default AlertRuleItem; diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 70892d86589..2d1cf653540 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -15,6 +15,7 @@ const setup = (propOverrides?: object) => { updateLocation: jest.fn(), getAlertRulesAsync: jest.fn(), setSearchQuery: jest.fn(), + togglePauseAlertRule: jest.fn(), stateFilter: '', search: '', }; diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 4b48da47256..d25fc659af5 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events'; import { updateLocation } from 'app/core/actions'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, AlertRule } from 'app/types'; -import { getAlertRulesAsync, setSearchQuery } from './state/actions'; +import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions'; import { getAlertRuleItems, getSearchQuery } from './state/selectors'; export interface Props { @@ -16,6 +16,7 @@ export interface Props { updateLocation: typeof updateLocation; getAlertRulesAsync: typeof getAlertRulesAsync; setSearchQuery: typeof setSearchQuery; + togglePauseAlertRule: typeof togglePauseAlertRule; stateFilter: string; search: string; } @@ -71,6 +72,10 @@ export class AlertRuleList extends PureComponent { this.props.setSearchQuery(value); }; + onTogglePause = (rule: AlertRule) => { + this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' }); + }; + alertStateFilterOption = ({ text, value }) => { return (
      - {alertRules.map(rule => )} + {alertRules.map(rule => ( + this.onTogglePause(rule)} + /> + ))}
    @@ -135,6 +147,7 @@ const mapDispatchToProps = { updateLocation, getAlertRulesAsync, setSearchQuery, + togglePauseAlertRule, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap index 7d3c446fc55..f686127ebf3 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap @@ -64,7 +64,7 @@ exports[`Render should render component 1`] = ` >
    @@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent { } function mapStateToProps(state) { - const teamId = getRouteParamsId(state.location); - return { - team: getTeam(state.team, teamId), + members: getTeamMembers(state.team), searchMemberQuery: getSearchMemberQuery(state.team), }; } @@ -149,4 +145,4 @@ const mapDispatchToProps = { setSearchMemberQuery, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 4395c0bfbef..2528c3c87b8 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -41,10 +41,10 @@ export class TeamPages extends PureComponent { } componentDidMount() { - this.loadTeam(); + this.fetchTeam(); } - async loadTeam() { + async fetchTeam() { const { loadTeam, teamId } = this.props; await loadTeam(teamId); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 21c0cf012f0..7050997c387 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,4 +1,4 @@ -import { Team } from '../../../types'; +import { Team, TeamMember } from '../../../types'; export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { let teams: Team[] = []; @@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { avatarUrl: 'some/url/', email: `test-${i}@test.com`, memberCount: i, - search: '', - members: [], - groups: [], }); } @@ -25,13 +22,26 @@ export const getMockTeam = (): Team => { avatarUrl: 'some/url/', email: 'test@test.com', memberCount: 1, - search: '', - members: [], - groups: [], }; }; -export const getMockTeamMember = () => { +export const getMockTeamMembers = (amount: number): TeamMember[] => { + let teamMembers: TeamMember[] = []; + + for (let i = 1; i <= amount; i++) { + teamMembers.push({ + userId: i, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: `testUser-${i}`, + }); + } + + return teamMembers; +}; + +export const getMockTeamMember = (): TeamMember => { return { userId: 1, teamId: 1, diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap new file mode 100644 index 00000000000..2a42897e2b9 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    +
    + +
    +
    + +
    + +
    + +
    + Add Team Member +
    +
    + +
    +
    +
    +
    + + + + + + + + +
    + + Name + + Email + +
    +
    +
    +`; + +exports[`Render should render team members 1`] = ` +
    +
    +
    + +
    +
    + +
    + +
    + +
    + Add Team Member +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + Name + + Email + +
    + + + testUser-1 + + test@test.com + + +
    + + + testUser-2 + + test@test.com + + +
    + + + testUser-3 + + test@test.com + + +
    + + + testUser-4 + + test@test.com + + +
    + + + testUser-5 + + test@test.com + + +
    +
    +
    +`; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 3c19d726e41..563d3d3bb99 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> @@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
    - +
    `; @@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index e407737bb20..4786edf60a8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { + console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 492ec71ba4b..7f7a33d60ac 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -31,22 +31,42 @@ describe('teams reducer', () => { }); describe('team reducer', () => { + it('should set team', () => { + const payload = getMockTeam(); + + const action: Action = { + type: ActionTypes.LoadTeam, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.team).toEqual(payload); + }); + it('should set team members', () => { const mockTeamMember = getMockTeamMember(); - const mockTeam = getMockTeam(); - const state = { - ...initialTeamState, - team: mockTeam, - }; const action: Action = { type: ActionTypes.LoadTeamMembers, payload: [mockTeamMember], }; - const result = teamReducer(state, action); - const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + const result = teamReducer(initialTeamState, action); - expect(result).toEqual(expectedState); + expect(result.members).toEqual([mockTeamMember]); + }); + + it('should set member search query', () => { + const payload = 'member'; + + const action: Action = { + type: ActionTypes.SetSearchMemberQuery, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.searchMemberQuery).toEqual('member'); }); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index e30fddb22a5..f02ade60923 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,8 +1,13 @@ -import { Team, TeamsState, TeamState } from '../../../types'; +import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; +export const initialTeamState: TeamState = { + team: {} as Team, + members: [] as TeamMember[], + groups: [] as TeamGroup[], + searchMemberQuery: '', +}; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState return { ...state, team: action.payload }; case ActionTypes.LoadTeamMembers: - return { ...state, team: { ...state.team, members: action.payload } }; + return { ...state, members: action.payload }; case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 66fd07444ce..e1b11cf288b 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,8 +1,8 @@ -import { getTeams } from './selectors'; -import { getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState } from '../../../types'; +import { getTeam, getTeams } from './selectors'; +import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState, TeamState } from '../../../types'; -describe('Team selectors', () => { +describe('Teams selectors', () => { describe('Get teams', () => { const mockTeams = getMultipleMockTeams(5); @@ -23,3 +23,17 @@ describe('Team selectors', () => { }); }); }); + +describe('Team selectors', () => { + describe('Get team', () => { + const mockTeam = getMockTeam(); + + it('should return team if matching with location team', () => { + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + + const team = getTeam(mockState, '1'); + + expect(team).toEqual(mockTeam); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index d6142adf157..5e22f96eaf7 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; export const getTeam = (state, currentTeamId) => { - if (state.team.id === currentTeamId) { - console.log('yes'); + if (state.team.id === parseInt(currentTeamId)) { return state.team; } }; @@ -15,3 +14,11 @@ export const getTeams = state => { return regex.test(team.name); }); }; + +export const getTeamMembers = state => { + const regex = RegExp(state.searchMemberQuery, 'i'); + + return state.members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 27ae3dbe19b..35cd9a41f4e 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -63,9 +63,6 @@ export interface Team { avatarUrl: string; email: string; memberCount: number; - search?: string; - members?: TeamMember[]; - groups?: TeamGroup[]; } export interface TeamMember { @@ -124,6 +121,8 @@ export interface TeamsState { export interface TeamState { team: Team; + members: TeamMember[]; + groups: TeamGroup[]; searchMemberQuery: string; } From 841bd5817de3102c7310625aff94ed6d58d03bb2 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:27:33 +0200 Subject: [PATCH 065/666] test for team member selector --- .../features/teams/state/selectors.test.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index e1b11cf288b..5f338069bbb 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,6 +1,6 @@ -import { getTeam, getTeams } from './selectors'; -import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState, TeamState } from '../../../types'; +import { getTeam, getTeamMembers, getTeams } from './selectors'; +import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { Team, TeamGroup, TeamsState, TeamState } from '../../../types'; describe('Teams selectors', () => { describe('Get teams', () => { @@ -29,11 +29,28 @@ describe('Team selectors', () => { const mockTeam = getMockTeam(); it('should return team if matching with location team', () => { - const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] }; const team = getTeam(mockState, '1'); expect(team).toEqual(mockTeam); }); }); + + describe('Get members', () => { + const mockTeamMembers = getMockTeamMembers(5); + + it('should return team members', () => { + const mockState: TeamState = { + team: {} as Team, + searchMemberQuery: '', + members: mockTeamMembers, + groups: [] as TeamGroup[], + }; + + const members = getTeamMembers(mockState); + + expect(members).toEqual(mockTeamMembers); + }); + }); }); From 59b5b146daaa7655a0a77594206a6c637db7041a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 14:12:38 +0200 Subject: [PATCH 066/666] wip: began folder to redux migration --- .../ManageDashboards/FolderSettings.tsx | 160 ---------------- .../FolderSettingsPage.test.tsx} | 0 .../manage-dashboards/FolderSettingsPage.tsx | 180 ++++++++++++++++++ .../manage-dashboards/state/actions.ts | 29 +++ .../manage-dashboards/state/reducers.ts | 0 public/app/routes/routes.ts | 4 +- public/app/types/dashboard.ts | 7 + public/app/types/index.ts | 4 + 8 files changed, 222 insertions(+), 162 deletions(-) delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx rename public/app/{containers/ManageDashboards/FolderSettings.test.tsx => features/manage-dashboards/FolderSettingsPage.test.tsx} (100%) create mode 100644 public/app/features/manage-dashboards/FolderSettingsPage.tsx create mode 100644 public/app/features/manage-dashboards/state/actions.ts create mode 100644 public/app/features/manage-dashboards/state/reducers.ts create mode 100644 public/app/types/dashboard.ts diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

    Loading

    ; - } - - return ( -
    - -
    -

    Folder Settings

    - -
    -
    -
    - - -
    -
    - - -
    - -
    -
    -
    - ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx similarity index 100% rename from public/app/containers/ManageDashboards/FolderSettings.test.tsx rename to public/app/features/manage-dashboards/FolderSettingsPage.test.tsx diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx new file mode 100644 index 00000000000..4ed6743a8dc --- /dev/null +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -0,0 +1,180 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState } from 'app/types'; +import { getFolderByUid } from './state/actions'; + +export interface Props { + navModel: NavModel; + folderUid: string; + getFolderByUid: typeof getFolderByUid; +} + +export class FolderSettingsPage extends PureComponent { + // formSnapshot: any; + // + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + // + // loadStore() { + // const { nav, folder, view } = this.props; + // + // return folder.load(view.routeParams.get('uid') as string).then(res => { + // this.formSnapshot = getSnapshot(folder); + // view.updatePathAndQuery(`${res.url}/settings`, {}, {}); + // + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }); + // } + + // onTitleChange(evt) { + // this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); + // } + // + // getFormSnapshot() { + // if (!this.formSnapshot) { + // this.formSnapshot = getSnapshot(this.props.folder); + // } + // + // return this.formSnapshot; + // } + // + // save(evt) { + // if (evt) { + // evt.stopPropagation(); + // evt.preventDefault(); + // } + // + // const { nav, folder, view } = this.props; + // + // folder + // .saveFolder({ overwrite: false }) + // .then(newUrl => { + // view.updatePathAndQuery(newUrl, {}, {}); + // + // appEvents.emit('dashboard-saved'); + // appEvents.emit('alert-success', ['Folder saved']); + // }) + // .then(() => { + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }) + // .catch(this.handleSaveFolderError.bind(this)); + // } + // + // delete(evt) { + // if (evt) { + // evt.stopPropagation(); + // evt.preventDefault(); + // } + // + // const { folder, view } = this.props; + // const title = folder.folder.title; + // + // appEvents.emit('confirm-modal', { + // title: 'Delete', + // text: `Do you want to delete this folder and all its dashboards?`, + // icon: 'fa-trash', + // yesText: 'Delete', + // onConfirm: () => { + // return folder.deleteFolder().then(() => { + // appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); + // view.updatePathAndQuery('dashboards', '', ''); + // }); + // }, + // }); + // } + // + // handleSaveFolderError(err) { + // if (err.data && err.data.status === 'version-mismatch') { + // err.isHandled = true; + // + // const { nav, folder, view } = this.props; + // + // appEvents.emit('confirm-modal', { + // title: 'Conflict', + // text: 'Someone else has updated this folder.', + // text2: 'Would you still like to save this folder?', + // yesText: 'Save & Overwrite', + // icon: 'fa-warning', + // onConfirm: () => { + // folder + // .saveFolder({ overwrite: true }) + // .then(newUrl => { + // view.updatePathAndQuery(newUrl, {}, {}); + // + // appEvents.emit('dashboard-saved'); + // appEvents.emit('alert-success', ['Folder saved']); + // }) + // .then(() => { + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }); + // }, + // }); + // } + // } + + render() { + const { navModel } = this.props; + + // if (!folder.folder || !nav.main) { + // return

    Loading

    ; + // } + + return ( +
    + +
    +

    Folder Settings

    +
    +
    + ); + } + + // asd() { + //
    + //
    + //
    + // + // + //
    + //
    + // + // + //
    + // + //
    + // + // } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), + folderUid: uid, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts new file mode 100644 index 00000000000..ab5e1212d5f --- /dev/null +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -0,0 +1,29 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO } from 'app/types'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export type Action = LoadFolderAction; + +type ThunkResult = ThunkAction; + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index a0b070cbcb4..4c50bd65de9 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -4,7 +4,7 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; import TeamPages from 'app/containers/Teams/TeamPages'; import TeamList from 'app/containers/Teams/TeamList'; @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts new file mode 100644 index 00000000000..3ec82842934 --- /dev/null +++ b/public/app/types/dashboard.ts @@ -0,0 +1,7 @@ +export interface FolderDTO { + id: number; + title: string; + url: string; + version: number; + hasAcl: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..e2d9cf8933f 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,3 +1,7 @@ +import { FolderDTO } from './dashboard'; + +export { FolderDTO }; + // // Location // From b1fe0c4c7e015b657fffe917638303950b2661c0 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 15:53:58 +0200 Subject: [PATCH 067/666] team settings --- public/app/features/teams/TeamPages.tsx | 2 +- .../app/features/teams/TeamSettings.test.tsx | 44 ++++++++++++++ public/app/features/teams/TeamSettings.tsx | 59 ++++++++++++++----- .../__snapshots__/TeamPages.test.tsx.snap | 12 +--- .../__snapshots__/TeamSettings.test.tsx.snap | 57 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 14 +++++ 6 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 public/app/features/teams/TeamSettings.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2528c3c87b8..a4ab4a06d4d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -66,7 +66,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.Settings: - return ; + return ; case PageTypes.GroupSync: return isSyncEnabled && ; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx new file mode 100644 index 00000000000..2e40a0e3c44 --- /dev/null +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamSettings } from './TeamSettings'; +import { getMockTeam } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + team: getMockTeam(), + updateTeam: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamSettings; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should update team', () => { + const { instance } = setup(); + const mockEvent = { preventDefault: jest.fn() }; + + instance.setState({ + name: 'test11', + }); + + instance.onUpdate(mockEvent); + + expect(instance.props.updateTeam).toHaveBeenCalledWith('test11', 'test@test.com'); + }); +}); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 6e3c90d93f9..ef9a5ae0b70 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,41 +1,58 @@ import React from 'react'; -import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import { Label } from 'app/core/components/Forms/Forms'; import { Team } from '../../types'; +import { updateTeam } from './state/actions'; +import { getRouteParamsId } from '../../core/selectors/location'; +import { getTeam } from './state/selectors'; -interface Props { +export interface Props { team: Team; + updateTeam: typeof updateTeam; } -export class TeamSettings extends React.Component { +interface State { + name: string; + email: string; +} + +export class TeamSettings extends React.Component { constructor(props) { super(props); + + this.state = { + name: props.team.name, + email: props.team.email, + }; } - onChangeName = evt => { - // this.props.team.setName(evt.target.value); + onChangeName = event => { + this.setState({ name: event.target.value }); }; - onChangeEmail = evt => { - // this.props.team.setEmail(evt.target.value); + onChangeEmail = event => { + this.setState({ email: event.target.value }); }; - onUpdate = evt => { - evt.preventDefault(); - // this.props.team.update(); + onUpdate = event => { + const { name, email } = this.state; + event.preventDefault(); + this.props.updateTeam(name, email); }; render() { + const { name, email } = this.state; + return (

    Team Settings

    -
    +
    @@ -47,14 +64,14 @@ export class TeamSettings extends React.Component {
    -
    @@ -64,4 +81,16 @@ export class TeamSettings extends React.Component { } } -export default hot(module)(TeamSettings); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + }; +} + +const mapDispatchToProps = { + updateTeam, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings); diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 563d3d3bb99..73f3fde4093 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -52,17 +52,7 @@ exports[`Render should render settings page 1`] = `
    - +
    `; diff --git a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap new file mode 100644 index 00000000000..0f6573ccf90 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +

    + Team Settings +

    + +
    + + Name + + +
    +
    + + Email + + +
    +
    + +
    + +
    +`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 4786edf60a8..5b203d0a502 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -153,6 +153,20 @@ export function removeTeamMember(id: number): ThunkResult { }; } +export function updateTeam(name: string, email: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + await getBackendSrv() + .put(`/api/teams/${team.id}`, { + name, + email, + }) + .then(() => { + dispatch(loadTeam(team.id)); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() From 0cfcf2685e66af76895664c86f562952d63ca812 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 16:58:17 +0200 Subject: [PATCH 068/666] actions for group sync --- .../app/features/teams/TeamGroupSync.test.tsx | 0 public/app/features/teams/TeamGroupSync.tsx | 81 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 3 +- public/app/features/teams/state/actions.ts | 68 +++++++++++++++- public/app/features/teams/state/reducers.ts | 3 + public/app/features/teams/state/selectors.ts | 1 + 6 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 public/app/features/teams/TeamGroupSync.test.tsx diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 6562820d717..39fdd8d413e 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,11 +1,16 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import { Team, TeamGroup } from '../../types'; +import { TeamGroup } from '../../types'; +import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; +import { getTeamGroups } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + groups: TeamGroup[]; + loadTeamGroups: typeof loadTeamGroups; + addTeamGroup: typeof addTeamGroup; + removeTeamGroup: typeof removeTeamGroup; } interface State { @@ -15,14 +20,39 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -export class TeamGroupSync extends React.Component { +export class TeamGroupSync extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newGroupId: '' }; } componentDidMount() { - // this.props.team.loadGroups(); + this.fetchTeamGroups(); + } + + async fetchTeamGroups() { + await this.props.loadTeamGroups(); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onNewGroupIdChanged = evt => { + this.setState({ newGroupId: evt.target.value }); + }; + + onAddGroup = () => { + this.props.addTeamGroup(this.state.newGroupId); + this.setState({ isAdding: false, newGroupId: '' }); + }; + + onRemoveGroup = (group: TeamGroup) => { + this.props.removeTeamGroup(group.groupId); + }; + + isNewGroupValid() { + return this.state.newGroupId.length > 1; } renderGroup(group: TeamGroup) { @@ -38,30 +68,9 @@ export class TeamGroupSync extends React.Component { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onNewGroupIdChanged = evt => { - this.setState({ newGroupId: evt.target.value }); - }; - - onAddGroup = () => { - // this.props.team.addGroup(this.state.newGroupId); - this.setState({ isAdding: false, newGroupId: '' }); - }; - - onRemoveGroup = (group: TeamGroup) => { - // this.props.team.removeGroup(group.groupId); - }; - - isNewGroupValid() { - return this.state.newGroupId.length > 1; - } - render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups; + const groups = this.props.groups; return (
    @@ -144,4 +153,16 @@ export class TeamGroupSync extends React.Component { } } -export default hot(module)(TeamGroupSync); +function mapStateToProps(state) { + return { + groups: getTeamGroups(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamGroups, + addTeamGroup, + removeTeamGroup, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index a4ab4a06d4d..f28bde518d2 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -57,7 +57,6 @@ export class TeamPages extends PureComponent { } renderPage() { - const { team } = this.props; const { isSyncEnabled } = this.state; const currentPage = this.getCurrentPage(); @@ -69,7 +68,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.GroupSync: - return isSyncEnabled && ; + return isSyncEnabled && ; } return null; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5b203d0a502..9b3ab3a8177 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,9 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; +import config from 'app/core/config'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -10,6 +11,7 @@ export enum ActionTypes { SetSearchQuery = 'SET_SEARCH_QUERY', SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', LoadTeamMembers = 'TEAM_MEMBERS_LOADED', + LoadTeamGroups = 'TEAM_GROUPS_LOADED', } export interface LoadTeamsAction { @@ -27,6 +29,11 @@ export interface LoadTeamMembersAction { payload: TeamMember[]; } +export interface LoadTeamGroupsAction { + type: ActionTypes.LoadTeamGroups; + payload: TeamGroup[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; @@ -42,7 +49,8 @@ export type Action = | SetSearchQueryAction | LoadTeamAction | LoadTeamMembersAction - | SetSearchMemberQueryAction; + | SetSearchMemberQueryAction + | LoadTeamGroupsAction; type ThunkResult = ThunkAction; @@ -61,6 +69,11 @@ const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => payload: teamMembers, }); +const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({ + type: ActionTypes.LoadTeamGroups, + payload: teamGroups, +}); + export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ type: ActionTypes.SetSearchMemberQuery, payload: searchQuery, @@ -79,7 +92,7 @@ export function loadTeams(): ThunkResult { } function buildNavModel(team: Team): NavModelItem { - return { + const navModel = { img: team.avatarUrl, id: 'team-' + team.id, subTitle: 'Manage members & settings', @@ -103,6 +116,18 @@ function buildNavModel(team: Team): NavModelItem { }, ], }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: 'team-settings', + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; } export function loadTeam(id: number): ThunkResult { @@ -117,7 +142,6 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { - console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; @@ -167,6 +191,42 @@ export function updateTeam(name: string, email: string): ThunkResult { }; } +export function loadTeamGroups(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/groups`) + .then(response => { + dispatch(teamGroupsLoaded(response)); + }); + }; +} + +export function addTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + +export function removeTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/groups/${groupId}`) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index f02ade60923..4af36f2e01c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -30,6 +30,9 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; + + case ActionTypes.LoadTeamGroups: + return { ...state, groups: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 5e22f96eaf7..416e293ec78 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,6 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; +export const getTeamGroups = state => state.groups; export const getTeam = (state, currentTeamId) => { if (state.team.id === parseInt(currentTeamId)) { From 679ffbfd8320490c20bb02acc1648557764734df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 21:49:04 +0200 Subject: [PATCH 069/666] wip: progress on redux folder store --- public/app/core/actions/index.ts | 4 +- .../manage-dashboards/state/actions.ts | 39 ++++++++++++++++++- .../manage-dashboards/state/reducers.ts | 20 ++++++++++ public/app/stores/configureStore.ts | 2 + public/app/types/dashboard.ts | 12 +++++- public/app/types/index.ts | 5 ++- 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index b4b9b21126e..451a13dae99 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,4 @@ import { updateLocation } from './location'; -import { updateNavIndex } from './navModel'; +import { updateNavIndex, UpdateNavIndexAction } from './navModel'; -export { updateLocation, updateNavIndex }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction }; diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index ab5e1212d5f..b3243c7bf2b 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,7 +1,8 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO } from 'app/types'; +import { FolderDTO, NavModelItem } from 'app/types'; +import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -19,11 +20,45 @@ export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ export type Action = LoadFolderAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; +function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { const folder = await getBackendSrv().getFolderByUid(uid); dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); }; } diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index e69de29bb2d..1eb873f5bd0 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -0,0 +1,20 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = null; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + canSave: false, + hasChanged: false, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..5aa5ccc5f41 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; +import manageDashboardsReducers from 'app/features/manage-dashboards/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...manageDashboardsReducers, }); export let store; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 3ec82842934..576432d413e 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,7 +1,17 @@ export interface FolderDTO { id: number; + uid: string; title: string; url: string; version: number; - hasAcl: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 221a64b48d4..bc54cea35cb 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,6 +1,6 @@ -import { FolderDTO } from './dashboard'; +import { FolderDTO, FolderState } from './dashboard'; -export { FolderDTO }; +export { FolderDTO, FolderState }; // // Location @@ -136,4 +136,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } From 55e42b5fffa7766a1d3aa7586ccf85d6174e529b Mon Sep 17 00:00:00 2001 From: Mike Sollanych Date: Mon, 10 Sep 2018 13:30:29 -0700 Subject: [PATCH 070/666] Adding Centrify configuration for Oauth Just some simple directions for configuring Centrify to enable oauth login! --- docs/sources/auth/generic-oauth.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 802424f180b..0f8c2bd6856 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -174,6 +174,36 @@ allowed_organizations = allowed_organizations = ``` +## Set up OAuth2 with Centrify + +1. Create a new Custom OpenID Connect application configuration in the Centrify dashboard. + +2. Create a memorable unique Application ID, e.g. "grafana", "grafana_aws", etc. + +3. Put in other basic configuration (name, description, logo, category) + +4. On the Trust tab, generate a long password and put it into the OpenID Connect Client Secret field. + +5. Put the URL to the front page of your Grafana instance into the "Resource Application URL" field. + +6. Add an authorized Redirect URI like https://your-grafana-server/login/generic_oauth + +7. Set up permissions, policies, etc. just like any other Centrify app + +8. Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = Centrify + enabled = true + allow_sign_up = true + client_id = + client_secret = .my.centrify.com/OAuth2/Authorize/ + token_url = https://.my.centrify.com/OAuth2/Token/ + ``` +
    From b8a881646a3f8180b9336743e2c937d973caeb2d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 09:25:08 +0200 Subject: [PATCH 071/666] changelog: note about closing #11681 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14e187b48f..b3a9ffbec9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Minor * **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon) +* **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc) # 5.3.0 (unreleased) From 61112d93d8caa8a00b6ef2d1745d18706baf22f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 10:36:55 +0200 Subject: [PATCH 072/666] wip: folder to redux --- .../manage-dashboards/FolderSettingsPage.tsx | 60 +++++++++---------- .../manage-dashboards/state/reducers.ts | 8 ++- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx index 4ed6743a8dc..90528a8798d 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -120,48 +120,41 @@ export class FolderSettingsPage extends PureComponent { render() { const { navModel } = this.props; - // if (!folder.folder || !nav.main) { - // return

    Loading

    ; - // } - return (

    Folder Settings

    + +
    +
    +
    + + +
    +
    + + +
    + +
    ); } - - // asd() { - //
    - //
    - //
    - // - // - //
    - //
    - // - // - //
    - // - //
    - // - // } } const mapStateToProps = (state: StoreState) => { @@ -170,6 +163,7 @@ const mapStateToProps = (state: StoreState) => { return { navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), folderUid: uid, + folder: state.folder, }; }; diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 1eb873f5bd0..ee837acc9db 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -1,7 +1,13 @@ import { FolderState } from 'app/types'; import { Action, ActionTypes } from './actions'; -export const inititalState: FolderState = null; +export const inititalState: FolderState = { + uid: 'loading', + id: -1, + title: 'loading', + canSave: false, + hasChanged: false, +}; export const folderReducer = (state = inititalState, action: Action): FolderState => { switch (action.type) { From 2936e34d750ca8522c4dc3a4614fc015f1a111bd Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 10:53:21 +0200 Subject: [PATCH 073/666] removes protoc from makefile --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index c6915409ed7..c9e51d897f3 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,3 @@ test: test-go test-js run: ./bin/grafana-server - -protoc: - protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/. From 28250271ccafccd5547af0f418729a911ce846c5 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 11 Sep 2018 19:53:39 +0900 Subject: [PATCH 074/666] fix nil pointer dereference (#13221) --- pkg/tsdb/cloudwatch/metric_find_query.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index ef1b53eaf1b..e1e131d9f3a 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -466,6 +466,9 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, return nil, errors.New("invalid attribute path") } v = v.FieldByName(key) + if !v.IsValid() { + return nil, errors.New("invalid attribute path") + } } if attr, ok := v.Interface().(*string); ok { data = *attr From 1ce900114109f89a0ea1d5cfdee2f3670f64dc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 13:48:13 +0200 Subject: [PATCH 075/666] upgrade of typescript and tslint and jest (#13223) --- jest.config.js | 7 +- package.json | 11 +- .../__snapshots__/TeamPicker.test.tsx.snap | 8 - .../__snapshots__/UserPicker.test.tsx.snap | 8 - .../__snapshots__/SignIn.test.tsx.snap | 4 +- .../__snapshots__/ServerStats.test.tsx.snap | 1 - yarn.lock | 730 ++++++++---------- 7 files changed, 342 insertions(+), 427 deletions(-) diff --git a/jest.config.js b/jest.config.js index a5cd3416f75..cac634fbf10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,13 +1,8 @@ module.exports = { verbose: false, - "globals": { - "ts-jest": { - "tsConfigFile": "tsconfig.json" - } - }, "transform": { - "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" + "^.+\\.(ts|tsx)$": "ts-jest" }, "moduleDirectories": ["node_modules", "public"], "roots": [ diff --git a/package.json b/package.json index 7afe10c0772..29196ffdf01 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "expect.js": "~0.2.0", "expose-loader": "^0.7.3", "file-loader": "^1.1.11", - "fork-ts-checker-webpack-plugin": "^0.4.2", + "fork-ts-checker-webpack-plugin": "^0.4.9", "gaze": "^1.1.2", "glob": "~7.0.0", "grunt": "1.0.1", @@ -56,7 +56,7 @@ "html-webpack-harddisk-plugin": "^0.2.0", "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", - "jest": "^22.0.4", + "jest": "^23.6.0", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", @@ -80,12 +80,12 @@ "style-loader": "^0.21.0", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", - "ts-jest": "^22.4.6", - "ts-loader": "^4.3.0", + "ts-jest": "^23.1.4", + "ts-loader": "^5.1.0", "tslib": "^1.9.3", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", - "typescript": "^2.6.2", + "typescript": "^3.0.3", "uglifyjs-webpack-plugin": "^1.2.7", "webpack": "^4.8.0", "webpack-bundle-analyzer": "^2.9.0", @@ -133,6 +133,7 @@ "angular-native-dragdrop": "1.2.2", "angular-route": "1.6.6", "angular-sanitize": "1.6.6", + "babel-jest": "^23.6.0", "babel-polyfill": "^6.26.0", "baron": "^3.0.3", "brace": "^0.10.0", diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap index 67232d0ea5b..c63cc880900 100644 --- a/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap +++ b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap @@ -6,7 +6,6 @@ exports[`TeamPicker renders correctly 1`] = ` >
      =0.10.3 <1" +sisteransi@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -10834,9 +10812,9 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.0, source-map-support@^0.5.5: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" +source-map-support@^0.5.6: + version "0.5.9" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -11163,12 +11141,6 @@ style-loader@^0.21.0: loader-utils "^1.1.0" schema-utils "^0.4.5" -subarg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" - dependencies: - minimist "^1.1.0" - supports-color@4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" @@ -11492,25 +11464,18 @@ tryor@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" -ts-jest@^22.4.6: - version "22.4.6" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.4.6.tgz#a5d7f5e8b809626d1f4143209d301287472ec344" +ts-jest@^23.1.4: + version "23.1.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.1.4.tgz#66ac1d8d3fbf8f9a98432b11aa377aa850664b2b" dependencies: - babel-core "^6.26.3" - babel-plugin-istanbul "^4.1.6" - babel-plugin-transform-es2015-modules-commonjs "^6.26.2" - babel-preset-jest "^22.4.3" - cpx "^1.5.0" - fs-extra "6.0.0" - jest-config "^22.4.3" + closest-file-data "^0.1.4" + fs-extra "6.0.1" + json5 "^0.5.0" lodash "^4.17.10" - pkg-dir "^2.0.0" - source-map-support "^0.5.5" - yargs "^11.0.0" -ts-loader@^4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-4.3.1.tgz#345298df9a5019be7a3e86cd7b8b1aefef4bbd79" +ts-loader@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-5.1.0.tgz#ac13facb9360af4a4b072c851a120d17cbcaf1fa" dependencies: chalk "^2.3.0" enhanced-resolve "^4.0.0" @@ -11614,9 +11579,9 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@^2.6.2: - version "2.9.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961" +typescript@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" ua-parser-js@^0.7.9: version "0.7.18" @@ -11784,7 +11749,7 @@ unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -upath@^1.0.0: +upath@^1.0.0, upath@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" @@ -11981,12 +11946,6 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" -vue-parser@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/vue-parser/-/vue-parser-1.1.6.tgz#3063c8431795664ebe429c23b5506899706e6355" - dependencies: - parse5 "^3.0.3" - w3c-blob@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8" @@ -12387,12 +12346,6 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" -yargs-parser@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" - dependencies: - camelcase "^4.1.0" - yargs-parser@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" @@ -12416,23 +12369,6 @@ yargs@11.0.0, yargs@^11.0.0: y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@^10.0.3: - version "10.1.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5" - dependencies: - cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^8.1.0" - yargs@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" From 6ba5550f5f3115aa0cf23e958dd460677b81dc90 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 14:09:08 +0200 Subject: [PATCH 076/666] renames jest files to match new convention --- ...{datasource.jest.ts => datasource.test.ts} | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) rename public/app/plugins/datasource/grafana/specs/{datasource.jest.ts => datasource.test.ts} (72%) diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.test.ts similarity index 72% rename from public/app/plugins/datasource/grafana/specs/datasource.jest.ts rename to public/app/plugins/datasource/grafana/specs/datasource.test.ts index 544b04056ac..b3afe7207f2 100644 --- a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/grafana/specs/datasource.test.ts @@ -13,7 +13,11 @@ describe('grafana data source', () => { }; const templateSrvStub = { - replace: val => val.replace('$var', 'replaced') + replace: val => { + return val + .replace('$var2', 'replaced|replaced2') + .replace('$var', 'replaced'); + } }; const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); @@ -32,6 +36,21 @@ describe('grafana data source', () => { }); }); + describe('with tags that have multi value template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['$var2']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('replaced'); + expect(calledBackendSrvParams.tags[1]).toBe('replaced2'); + }); + }); + describe('with type dashboard', () => { const options = setupAnnotationQueryOptions( { From 9f73f13091d5ebaf4ba4e7ad40d7bdb9556e88d1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 11 Sep 2018 14:14:03 +0200 Subject: [PATCH 077/666] Teams page replace mobx (#13219) * creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * team settings * actions for group sync * tests for team groups * removed comment * remove old stores * fix: formating of datasource.go * fix: minor changes to imports * adding debounce and fixing issue in teamlist * refactoring: moving types to their own files --- public/app/containers/Teams/TeamPages.tsx | 77 ---- public/app/containers/Teams/TeamSettings.tsx | 69 ---- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 22 +- .../CustomScrollbar/CustomScrollbar.tsx | 1 - public/app/core/reducers/navModel.ts | 18 +- public/app/core/selectors/location.ts | 3 + public/app/features/alerting/state/actions.ts | 8 +- .../features/alerting/state/reducers.test.ts | 4 +- .../app/features/alerting/state/reducers.ts | 4 +- .../app/features/teams/TeamGroupSync.test.tsx | 63 ++++ .../teams}/TeamGroupSync.tsx | 95 +++-- public/app/features/teams/TeamList.test.tsx | 75 ++++ .../Teams => features/teams}/TeamList.tsx | 126 ++++--- .../app/features/teams/TeamMembers.test.tsx | 79 ++++ .../Teams => features/teams}/TeamMembers.tsx | 91 +++-- public/app/features/teams/TeamPages.test.tsx | 63 ++++ public/app/features/teams/TeamPages.tsx | 105 ++++++ .../app/features/teams/TeamSettings.test.tsx | 44 +++ public/app/features/teams/TeamSettings.tsx | 96 +++++ .../features/teams/__mocks__/navModelMock.ts | 59 +++ .../app/features/teams/__mocks__/teamMocks.ts | 65 ++++ .../__snapshots__/TeamGroupSync.test.tsx.snap | 281 ++++++++++++++ .../__snapshots__/TeamList.test.tsx.snap | 354 ++++++++++++++++++ .../__snapshots__/TeamMembers.test.tsx.snap | 317 ++++++++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 48 +++ .../__snapshots__/TeamSettings.test.tsx.snap | 57 +++ public/app/features/teams/state/actions.ts | 237 ++++++++++++ .../app/features/teams/state/reducers.test.ts | 72 ++++ public/app/features/teams/state/reducers.ts | 44 +++ .../features/teams/state/selectors.test.ts | 56 +++ public/app/features/teams/state/selectors.ts | 30 ++ public/app/routes/routes.ts | 4 +- public/app/stores/NavStore/NavStore.ts | 40 -- public/app/stores/RootStore/RootStore.ts | 4 - public/app/stores/TeamsStore/TeamsStore.ts | 156 -------- public/app/stores/configureStore.ts | 2 + public/app/types/alerting.ts | 35 ++ public/app/types/index.ts | 112 ++---- public/app/types/location.ts | 15 + public/app/types/navModel.ts | 22 ++ public/app/types/teams.ts | 32 ++ 42 files changed, 2493 insertions(+), 595 deletions(-) delete mode 100644 public/app/containers/Teams/TeamPages.tsx delete mode 100644 public/app/containers/Teams/TeamSettings.tsx create mode 100644 public/app/core/selectors/location.ts create mode 100644 public/app/features/teams/TeamGroupSync.test.tsx rename public/app/{containers/Teams => features/teams}/TeamGroupSync.tsx (73%) create mode 100644 public/app/features/teams/TeamList.test.tsx rename public/app/{containers/Teams => features/teams}/TeamList.tsx (63%) create mode 100644 public/app/features/teams/TeamMembers.test.tsx rename public/app/{containers/Teams => features/teams}/TeamMembers.tsx (66%) create mode 100644 public/app/features/teams/TeamPages.test.tsx create mode 100644 public/app/features/teams/TeamPages.tsx create mode 100644 public/app/features/teams/TeamSettings.test.tsx create mode 100644 public/app/features/teams/TeamSettings.tsx create mode 100644 public/app/features/teams/__mocks__/navModelMock.ts create mode 100644 public/app/features/teams/__mocks__/teamMocks.ts create mode 100644 public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamList.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap create mode 100644 public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap create mode 100644 public/app/features/teams/state/actions.ts create mode 100644 public/app/features/teams/state/reducers.test.ts create mode 100644 public/app/features/teams/state/reducers.ts create mode 100644 public/app/features/teams/state/selectors.test.ts create mode 100644 public/app/features/teams/state/selectors.ts delete mode 100644 public/app/stores/TeamsStore/TeamsStore.ts create mode 100644 public/app/types/alerting.ts create mode 100644 public/app/types/location.ts create mode 100644 public/app/types/navModel.ts create mode 100644 public/app/types/teams.ts diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/containers/Teams/TeamPages.tsx deleted file mode 100644 index 2abc9c51535..00000000000 --- a/public/app/containers/Teams/TeamPages.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import config from 'app/core/config'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { ViewStore } from 'app/stores/ViewStore/ViewStore'; -import TeamMembers from './TeamMembers'; -import TeamSettings from './TeamSettings'; -import TeamGroupSync from './TeamGroupSync'; - -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - view: typeof ViewStore.Type; -} - -@inject('nav', 'teams', 'view') -@observer -export class TeamPages extends React.Component { - isSyncEnabled: boolean; - currentPage: string; - - constructor(props) { - super(props); - - this.isSyncEnabled = config.buildInfo.isEnterprise; - this.currentPage = this.getCurrentPage(); - - this.loadTeam(); - } - - async loadTeam() { - const { teams, nav, view } = this.props; - - await teams.loadById(view.routeParams.get('id')); - - nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); - } - - getCurrentTeam(): Team { - const { teams, view } = this.props; - return teams.map.get(view.routeParams.get('id')); - } - - getCurrentPage() { - const pages = ['members', 'settings', 'groupsync']; - const currentPage = this.props.view.routeParams.get('page'); - return _.includes(pages, currentPage) ? currentPage : pages[0]; - } - - render() { - const { nav } = this.props; - const currentTeam = this.getCurrentTeam(); - - if (!nav.main) { - return null; - } - - return ( -
      - - {currentTeam && ( -
      - {this.currentPage === 'members' && } - {this.currentPage === 'settings' && } - {this.currentPage === 'groupsync' && this.isSyncEnabled && } -
      - )} -
      - ); - } -} - -export default hot(module)(TeamPages); diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/containers/Teams/TeamSettings.tsx deleted file mode 100644 index 0de60a0b16c..00000000000 --- a/public/app/containers/Teams/TeamSettings.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team } from 'app/stores/TeamsStore/TeamsStore'; -import { Label } from 'app/core/components/Forms/Forms'; - -interface Props { - team: Team; -} - -@observer -export class TeamSettings extends React.Component { - constructor(props) { - super(props); - } - - onChangeName = evt => { - this.props.team.setName(evt.target.value); - }; - - onChangeEmail = evt => { - this.props.team.setEmail(evt.target.value); - }; - - onUpdate = evt => { - evt.preventDefault(); - this.props.team.update(); - }; - - render() { - return ( -
      -

      Team Settings

      -
      -
      - - -
      -
      - - -
      - -
      - -
      -
      -
      - ); - } -} - -export default hot(module)(TeamSettings); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 74b61f845c0..451a13dae99 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { updateLocation } from './location'; +import { updateNavIndex, UpdateNavIndexAction } from './navModel'; -export { updateLocation }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 56d129fd263..a40a0e880ee 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,13 +1,17 @@ -export type Action = UpdateNavIndexAction; +import { NavModelItem } from '../../types'; -// this action is not used yet -// kind of just a placeholder, will be need for dynamic pages -// like datasource edit, teams edit page - -export interface UpdateNavIndexAction { - type: 'UPDATE_NAV_INDEX'; +export enum ActionTypes { + UpdateNavIndex = 'UPDATE_NAV_INDEX', } -export const updateNavIndex = (): UpdateNavIndexAction => ({ - type: 'UPDATE_NAV_INDEX', +export type Action = UpdateNavIndexAction; + +export interface UpdateNavIndexAction { + type: ActionTypes.UpdateNavIndex; + payload: NavModelItem; +} + +export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({ + type: ActionTypes.UpdateNavIndex, + payload: item, }); diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx index 8be65249808..9b9a9c4d02a 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx @@ -13,7 +13,6 @@ interface Props { * Wraps component into component from `react-custom-scrollbars` */ class CustomScrollbar extends PureComponent { - static defaultProps: Partial = { customClassName: 'custom-scrollbars', autoHide: true, diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 26acdb39a3d..ac0e51854e7 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,5 +1,5 @@ -import { Action } from 'app/core/actions/navModel'; -import { NavModelItem, NavIndex } from 'app/types'; +import { Action, ActionTypes } from 'app/core/actions/navModel'; +import { NavIndex, NavModelItem } from 'app/types'; import config from 'app/core/config'; export function buildInitialState(): NavIndex { @@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem? export const initialState: NavIndex = buildInitialState(); export const navIndexReducer = (state = initialState, action: Action): NavIndex => { + switch (action.type) { + case ActionTypes.UpdateNavIndex: + const newPages = {}; + const payload = action.payload; + + for (const node of payload.children) { + newPages[node.id] = { + ...node, + parentItem: payload, + }; + } + + return { ...state, ...newPages }; + } return state; }; diff --git a/public/app/core/selectors/location.ts b/public/app/core/selectors/location.ts new file mode 100644 index 00000000000..adc31f47e89 --- /dev/null +++ b/public/app/core/selectors/location.ts @@ -0,0 +1,3 @@ +export const getRouteParamsId = state => state.routeParams.id; + +export const getRouteParamsPage = state => state.routeParams.page; diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index ca50d9e1038..edd6fbb1da1 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,15 +1,15 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AlertRuleApi, StoreState } from 'app/types'; +import { AlertRuleDTO, StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; export enum ActionTypes { LoadAlertRules = 'LOAD_ALERT_RULES', - SetSearchQuery = 'SET_SEARCH_QUERY', + SetSearchQuery = 'SET_ALERT_SEARCH_QUERY', } export interface LoadAlertRulesAction { type: ActionTypes.LoadAlertRules; - payload: AlertRuleApi[]; + payload: AlertRuleDTO[]; } export interface SetSearchQueryAction { @@ -17,7 +17,7 @@ export interface SetSearchQueryAction { payload: string; } -export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({ +export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({ type: ActionTypes.LoadAlertRules, payload: rules, }); diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts index 96ca7bacf6c..4f079a090cf 100644 --- a/public/app/features/alerting/state/reducers.test.ts +++ b/public/app/features/alerting/state/reducers.test.ts @@ -1,9 +1,9 @@ import { ActionTypes, Action } from './actions'; import { alertRulesReducer, initialState } from './reducers'; -import { AlertRuleApi } from '../../../types'; +import { AlertRuleDTO } from 'app/types'; describe('Alert rules', () => { - const payload: AlertRuleApi[] = [ + const payload: AlertRuleDTO[] = [ { id: 2, dashboardId: 7, diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index 73feb3cb260..c525885bc9c 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types'; +import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types'; import { Action, ActionTypes } from './actions'; import alertDef from './alertDef'; @@ -29,7 +29,7 @@ function convertToAlertRule(rule, state): AlertRule { export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => { switch (action.type) { case ActionTypes.LoadAlertRules: { - const alertRules: AlertRuleApi[] = action.payload; + const alertRules: AlertRuleDTO[] = action.payload; const alertRulesViewModel: AlertRule[] = alertRules.map(rule => { return convertToAlertRule(rule, rule.state); diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx new file mode 100644 index 00000000000..f3deb62c77b --- /dev/null +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamGroupSync } from './TeamGroupSync'; +import { TeamGroup } from '../../types'; +import { getMockTeamGroups } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + groups: [] as TeamGroup[], + loadTeamGroups: jest.fn(), + addTeamGroup: jest.fn(), + removeTeamGroup: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamGroupSync; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render groups table', () => { + const { wrapper } = setup({ + groups: getMockTeamGroups(3), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should call add group', () => { + const { instance } = setup(); + + instance.setState({ newGroupId: 'some/group' }); + const mockEvent = { preventDefault: jest.fn() }; + + instance.onAddGroup(mockEvent); + + expect(instance.props.addTeamGroup).toHaveBeenCalledWith('some/group'); + }); + + it('should call remove group', () => { + const { instance } = setup(); + + const mockGroup: TeamGroup = { teamId: 1, groupId: 'some/group' }; + + instance.onRemoveGroup(mockGroup); + + expect(instance.props.removeTeamGroup).toHaveBeenCalledWith('some/group'); + }); +}); diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx similarity index 73% rename from public/app/containers/Teams/TeamGroupSync.tsx rename to public/app/features/teams/TeamGroupSync.tsx index a3b2e4aed14..939dfcc8e31 100644 --- a/public/app/containers/Teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,12 +1,16 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { TeamGroup } from '../../types'; +import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; +import { getTeamGroups } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + groups: TeamGroup[]; + loadTeamGroups: typeof loadTeamGroups; + addTeamGroup: typeof addTeamGroup; + removeTeamGroup: typeof removeTeamGroup; } interface State { @@ -16,15 +20,40 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -@observer -export class TeamGroupSync extends React.Component { +export class TeamGroupSync extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newGroupId: '' }; } componentDidMount() { - this.props.team.loadGroups(); + this.fetchTeamGroups(); + } + + async fetchTeamGroups() { + await this.props.loadTeamGroups(); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onNewGroupIdChanged = event => { + this.setState({ newGroupId: event.target.value }); + }; + + onAddGroup = event => { + event.preventDefault(); + this.props.addTeamGroup(this.state.newGroupId); + this.setState({ isAdding: false, newGroupId: '' }); + }; + + onRemoveGroup = (group: TeamGroup) => { + this.props.removeTeamGroup(group.groupId); + }; + + isNewGroupValid() { + return this.state.newGroupId.length > 1; } renderGroup(group: TeamGroup) { @@ -40,30 +69,9 @@ export class TeamGroupSync extends React.Component { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onNewGroupIdChanged = evt => { - this.setState({ newGroupId: evt.target.value }); - }; - - onAddGroup = () => { - this.props.team.addGroup(this.state.newGroupId); - this.setState({ isAdding: false, newGroupId: '' }); - }; - - onRemoveGroup = (group: TeamGroup) => { - this.props.team.removeGroup(group.groupId); - }; - - isNewGroupValid() { - return this.state.newGroupId.length > 1; - } - render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups.values(); + const groups = this.props.groups; return (
      @@ -86,7 +94,7 @@ export class TeamGroupSync extends React.Component {
      Add External Group
      -
      +
      {
      -
      -
      +
      @@ -146,4 +149,16 @@ export class TeamGroupSync extends React.Component { } } -export default hot(module)(TeamGroupSync); +function mapStateToProps(state) { + return { + groups: getTeamGroups(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamGroups, + addTeamGroup, + removeTeamGroup, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync); diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx new file mode 100644 index 00000000000..6f84ca920d0 --- /dev/null +++ b/public/app/features/teams/TeamList.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamList } from './TeamList'; +import { NavModel, Team } from '../../types'; +import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teams: [] as Team[], + loadTeams: jest.fn(), + deleteTeam: jest.fn(), + setSearchQuery: jest.fn(), + searchQuery: '', + teamsCount: 0, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamList; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render teams table', () => { + const { wrapper } = setup({ + teams: getMultipleMockTeams(5), + teamsCount: 5, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadTeams', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadTeams).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.deleteTeam(getMockTeam()); + + expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); + }); + }); + + describe('on search query change', () => { + it('should call setSearchQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'test' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test'); + }); + }); +}); diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx similarity index 63% rename from public/app/containers/Teams/TeamList.tsx rename to public/app/features/teams/TeamList.tsx index 2a5743bea96..985d73d9a52 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,42 +1,42 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { NavModel, Team } from '../../types'; +import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; +import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; +import { getNavModel } from 'app/core/selectors/navModel'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - backendSrv: BackendSrv; +export interface Props { + navModel: NavModel; + teams: Team[]; + searchQuery: string; + teamsCount: number; + loadTeams: typeof loadTeams; + deleteTeam: typeof deleteTeam; + setSearchQuery: typeof setSearchQuery; } -@inject('nav', 'teams') -@observer -export class TeamList extends React.Component { - constructor(props) { - super(props); - - this.props.nav.load('cfg', 'teams'); +export class TeamList extends PureComponent { + componentDidMount() { this.fetchTeams(); } - fetchTeams() { - this.props.teams.loadTeams(); + async fetchTeams() { + await this.props.loadTeams(); } - deleteTeam(team: Team) { - this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); - } - - onSearchQueryChange = evt => { - this.props.teams.setSearchQuery(evt.target.value); + deleteTeam = (team: Team) => { + this.props.deleteTeam(team.id); }; - renderTeamMember(team: Team): JSX.Element { + onSearchQueryChange = event => { + this.props.setSearchQuery(event.target.value); + }; + + renderTeam(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -62,7 +62,28 @@ export class TeamList extends React.Component { ); } - renderTeamList(teams) { + renderEmptyList() { + return ( +
      + +
      + ); + } + + renderTeamList() { + const { teams, searchQuery } = this.props; + return (
      @@ -72,7 +93,7 @@ export class TeamList extends React.Component { type="text" className="gf-form-input" placeholder="Search teams" - value={teams.search} + value={searchQuery} onChange={this.onSearchQueryChange} /> @@ -97,49 +118,38 @@ export class TeamList extends React.Component { - {teams.filteredTeams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeam(team))}
      ); } - renderEmptyList() { - return ( -
      - -
      - ); - } - render() { - const { nav, teams } = this.props; - let view; - - if (teams.filteredTeams.length > 0) { - view = this.renderTeamList(teams); - } else { - view = this.renderEmptyList(); - } + const { navModel, teamsCount } = this.props; return (
      - - {view} + + {teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
      ); } } -export default hot(module)(TeamList); +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'teams'), + teams: getTeams(state.teams), + searchQuery: getSearchQuery(state.teams), + teamsCount: getTeamsCount(state.teams), + }; +} + +const mapDispatchToProps = { + loadTeams, + deleteTeam, + setSearchQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx new file mode 100644 index 00000000000..cae37e184fb --- /dev/null +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamMembers, Props } from './TeamMembers'; +import { TeamMember } from '../../types'; +import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + members: [] as TeamMember[], + searchMemberQuery: '', + setSearchMemberQuery: jest.fn(), + loadTeamMembers: jest.fn(), + addTeamMember: jest.fn(), + removeTeamMember: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamMembers; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render team members', () => { + const { wrapper } = setup({ + members: getMockTeamMembers(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + describe('on search member query change', () => { + it('it should call setSearchMemberQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'member' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member'); + }); + }); + + 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(); + + wrapper.state().newTeamMember = { + id: 1, + label: '', + avatarUrl: '', + login: '', + }; + + instance.onAddUserToTeam(); + + expect(instance.props.addTeamMember).toHaveBeenCalledWith(1); + }); +}); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx similarity index 66% rename from public/app/containers/Teams/TeamMembers.tsx rename to public/app/features/teams/TeamMembers.tsx index b06a547063a..5ad688aabf8 100644 --- a/public/app/containers/Teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,13 +1,19 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { TeamMember } from '../../types'; +import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; +import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + members: TeamMember[]; + searchMemberQuery: string; + loadTeamMembers: typeof loadTeamMembers; + addTeamMember: typeof addTeamMember; + removeTeamMember: typeof removeTeamMember; + setSearchMemberQuery: typeof setSearchMemberQuery; } interface State { @@ -15,42 +21,22 @@ interface State { newTeamMember?: User; } -@observer -export class TeamMembers extends React.Component { +export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; } componentDidMount() { - this.props.team.loadMembers(); + this.props.loadTeamMembers(); } - onSearchQueryChange = evt => { - this.props.team.setSearchQuery(evt.target.value); + onSearchQueryChange = event => { + this.props.setSearchMemberQuery(event.target.value); }; - removeMember(member: TeamMember) { - this.props.team.removeMember(member); - } - - removeMemberConfirmed(member: TeamMember) { - this.props.team.removeMember(member); - } - - renderMember(member: TeamMember) { - return ( - - - - - {member.login} - {member.email} - - this.removeMember(member)} /> - - - ); + onRemoveMember(member: TeamMember) { + this.props.removeTeamMember(member.userId); } onToggleAdding = () => { @@ -62,16 +48,29 @@ export class TeamMembers extends React.Component { }; onAddUserToTeam = async () => { - await this.props.team.addMember(this.state.newTeamMember.id); - await this.props.team.loadMembers(); + this.props.addTeamMember(this.state.newTeamMember.id); this.setState({ newTeamMember: null }); }; + renderMember(member: TeamMember) { + return ( + + + + + {member.login} + {member.email} + + this.onRemoveMember(member)} /> + + + ); + } + render() { const { newTeamMember, isAdding } = this.state; - const members = this.props.team.filteredMembers; + const { searchMemberQuery, members } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); - const { team } = this.props; return (
      @@ -82,7 +81,7 @@ export class TeamMembers extends React.Component { type="text" className="gf-form-input" placeholder="Search members" - value={team.search} + value={searchMemberQuery} onChange={this.onSearchQueryChange} /> @@ -124,7 +123,7 @@ export class TeamMembers extends React.Component { - {members.map(member => this.renderMember(member))} + {members && members.map(member => this.renderMember(member))}
    @@ -132,4 +131,18 @@ export class TeamMembers extends React.Component { } } -export default hot(module)(TeamMembers); +function mapStateToProps(state) { + return { + members: getTeamMembers(state.team), + searchMemberQuery: getSearchMemberQuery(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamMembers, + addTeamMember, + removeTeamMember, + setSearchMemberQuery, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers); diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx new file mode 100644 index 00000000000..65084d0dc47 --- /dev/null +++ b/public/app/features/teams/TeamPages.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamPages, Props } from './TeamPages'; +import { NavModel, Team } from '../../types'; +import { getMockTeam } from './__mocks__/teamMocks'; + +jest.mock('app/core/config', () => ({ + buildInfo: { isEnterprise: true }, +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teamId: 1, + loadTeam: jest.fn(), + pageName: 'members', + team: {} as Team, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance(); + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render member page if team not empty', () => { + const { wrapper } = setup({ + team: getMockTeam(), + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render settings page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'settings', + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render group sync page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'groupsync', + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx new file mode 100644 index 00000000000..f28bde518d2 --- /dev/null +++ b/public/app/features/teams/TeamPages.tsx @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import _ from 'lodash'; +import { hot } from 'react-hot-loader'; +import config from 'app/core/config'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import TeamMembers from './TeamMembers'; +import TeamSettings from './TeamSettings'; +import TeamGroupSync from './TeamGroupSync'; +import { NavModel, Team } from '../../types'; +import { loadTeam } from './state/actions'; +import { getTeam } from './state/selectors'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; + +export interface Props { + team: Team; + loadTeam: typeof loadTeam; + teamId: number; + pageName: string; + navModel: NavModel; +} + +interface State { + isSyncEnabled: boolean; +} + +enum PageTypes { + Members = 'members', + Settings = 'settings', + GroupSync = 'groupsync', +} + +export class TeamPages extends PureComponent { + constructor(props) { + super(props); + + this.state = { + isSyncEnabled: config.buildInfo.isEnterprise, + }; + } + + componentDidMount() { + this.fetchTeam(); + } + + async fetchTeam() { + const { loadTeam, teamId } = this.props; + + await loadTeam(teamId); + } + + getCurrentPage() { + const pages = ['members', 'settings', 'groupsync']; + const currentPage = this.props.pageName; + return _.includes(pages, currentPage) ? currentPage : pages[0]; + } + + renderPage() { + const { isSyncEnabled } = this.state; + const currentPage = this.getCurrentPage(); + + switch (currentPage) { + case PageTypes.Members: + return ; + + case PageTypes.Settings: + return ; + + case PageTypes.GroupSync: + return isSyncEnabled && ; + } + + return null; + } + + render() { + const { team, navModel } = this.props; + + return ( +
    + + {team && Object.keys(team).length !== 0 &&
    {this.renderPage()}
    } +
    + ); + } +} + +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + const pageName = getRouteParamsPage(state.location) || 'members'; + + return { + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + teamId: teamId, + pageName: pageName, + team: getTeam(state.team, teamId), + }; +} + +const mapDispatchToProps = { + loadTeam, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages)); diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx new file mode 100644 index 00000000000..2e40a0e3c44 --- /dev/null +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamSettings } from './TeamSettings'; +import { getMockTeam } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + team: getMockTeam(), + updateTeam: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamSettings; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should update team', () => { + const { instance } = setup(); + const mockEvent = { preventDefault: jest.fn() }; + + instance.setState({ + name: 'test11', + }); + + instance.onUpdate(mockEvent); + + expect(instance.props.updateTeam).toHaveBeenCalledWith('test11', 'test@test.com'); + }); +}); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx new file mode 100644 index 00000000000..ef9a5ae0b70 --- /dev/null +++ b/public/app/features/teams/TeamSettings.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Label } from 'app/core/components/Forms/Forms'; +import { Team } from '../../types'; +import { updateTeam } from './state/actions'; +import { getRouteParamsId } from '../../core/selectors/location'; +import { getTeam } from './state/selectors'; + +export interface Props { + team: Team; + updateTeam: typeof updateTeam; +} + +interface State { + name: string; + email: string; +} + +export class TeamSettings extends React.Component { + constructor(props) { + super(props); + + this.state = { + name: props.team.name, + email: props.team.email, + }; + } + + onChangeName = event => { + this.setState({ name: event.target.value }); + }; + + onChangeEmail = event => { + this.setState({ email: event.target.value }); + }; + + onUpdate = event => { + const { name, email } = this.state; + event.preventDefault(); + this.props.updateTeam(name, email); + }; + + render() { + const { name, email } = this.state; + + return ( +
    +

    Team Settings

    +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    +
    + ); + } +} + +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + }; +} + +const mapDispatchToProps = { + updateTeam, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings); diff --git a/public/app/features/teams/__mocks__/navModelMock.ts b/public/app/features/teams/__mocks__/navModelMock.ts new file mode 100644 index 00000000000..7aa8515ee13 --- /dev/null +++ b/public/app/features/teams/__mocks__/navModelMock.ts @@ -0,0 +1,59 @@ +export const getMockNavModel = (pageName: string) => { + return { + node: { + active: false, + icon: 'gicon gicon-team', + id: `team-${pageName}-2`, + text: `${pageName}`, + url: 'org/teams/edit/2/members', + parentItem: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }, + main: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: true, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }; +}; diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts new file mode 100644 index 00000000000..c9e9a27bee0 --- /dev/null +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -0,0 +1,65 @@ +import { Team, TeamGroup, TeamMember } from '../../../types'; + +export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { + const teams: Team[] = []; + for (let i = 1; i <= numberOfTeams; i++) { + teams.push({ + id: i, + name: `test-${i}`, + avatarUrl: 'some/url/', + email: `test-${i}@test.com`, + memberCount: i, + }); + } + + return teams; +}; + +export const getMockTeam = (): Team => { + return { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + }; +}; + +export const getMockTeamMembers = (amount: number): TeamMember[] => { + const teamMembers: TeamMember[] = []; + + for (let i = 1; i <= amount; i++) { + teamMembers.push({ + userId: i, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: `testUser-${i}`, + }); + } + + return teamMembers; +}; + +export const getMockTeamMember = (): TeamMember => { + return { + userId: 1, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: 'testUser', + }; +}; + +export const getMockTeamGroups = (amount: number): TeamGroup[] => { + const groups: TeamGroup[] = []; + + for (let i = 1; i <= amount; i++) { + groups.push({ + groupId: `group-${i}`, + teamId: 1, + }); + } + + return groups; +}; diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap new file mode 100644 index 00000000000..5a143f19038 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -0,0 +1,281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +`; + +exports[`Render should render groups table 1`] = ` +
    +
    +

    + External group sync +

    + + + +
    + +
    + +
    + +
    + Add External Group +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + External Group ID + +
    + group-1 + + + + +
    + group-2 + + + + +
    + group-3 + + + + +
    +
    +
    +`; diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap new file mode 100644 index 00000000000..45d0f78126e --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -0,0 +1,354 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    + +
    +
    +`; + +exports[`Render should render teams table 1`] = ` +
    + +
    +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + Name + + Email + + Members + +
    + + + + + + test-1 + + + + test-1@test.com + + + + 1 + + + +
    + + + + + + test-2 + + + + test-2@test.com + + + + 2 + + + +
    + + + + + + test-3 + + + + test-3@test.com + + + + 3 + + + +
    + + + + + + test-4 + + + + test-4@test.com + + + + 4 + + + +
    + + + + + + test-5 + + + + test-5@test.com + + + + 5 + + + +
    +
    +
    +
    +`; diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap new file mode 100644 index 00000000000..2a42897e2b9 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    +
    + +
    +
    + +
    + +
    + +
    + Add Team Member +
    +
    + +
    +
    +
    +
    + + + + + + + + +
    + + Name + + Email + +
    +
    +
    +`; + +exports[`Render should render team members 1`] = ` +
    +
    +
    + +
    +
    + +
    + +
    + +
    + Add Team Member +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + Name + + Email + +
    + + + testUser-1 + + test@test.com + + +
    + + + testUser-2 + + test@test.com + + +
    + + + testUser-3 + + test@test.com + + +
    + + + testUser-4 + + test@test.com + + +
    + + + testUser-5 + + test@test.com + + +
    +
    +
    +`; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap new file mode 100644 index 00000000000..4ce4df4acb2 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    +`; + +exports[`Render should render group sync page 1`] = ` +
    + +
    + +
    +
    +`; + +exports[`Render should render member page if team not empty 1`] = ` +
    + +
    + +
    +
    +`; + +exports[`Render should render settings page 1`] = ` +
    + +
    + +
    +
    +`; diff --git a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap new file mode 100644 index 00000000000..0f6573ccf90 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +

    + Team Settings +

    +
    +
    + + Name + + +
    +
    + + Email + + +
    +
    + +
    +
    +
    +`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts new file mode 100644 index 00000000000..91aa899e171 --- /dev/null +++ b/public/app/features/teams/state/actions.ts @@ -0,0 +1,237 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; +import config from 'app/core/config'; + +export enum ActionTypes { + LoadTeams = 'LOAD_TEAMS', + LoadTeam = 'LOAD_TEAM', + SetSearchQuery = 'SET_TEAM_SEARCH_QUERY', + SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY', + LoadTeamMembers = 'TEAM_MEMBERS_LOADED', + LoadTeamGroups = 'TEAM_GROUPS_LOADED', +} + +export interface LoadTeamsAction { + type: ActionTypes.LoadTeams; + payload: Team[]; +} + +export interface LoadTeamAction { + type: ActionTypes.LoadTeam; + payload: Team; +} + +export interface LoadTeamMembersAction { + type: ActionTypes.LoadTeamMembers; + payload: TeamMember[]; +} + +export interface LoadTeamGroupsAction { + type: ActionTypes.LoadTeamGroups; + payload: TeamGroup[]; +} + +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + +export interface SetSearchMemberQueryAction { + type: ActionTypes.SetSearchMemberQuery; + payload: string; +} + +export type Action = + | LoadTeamsAction + | SetSearchQueryAction + | LoadTeamAction + | LoadTeamMembersAction + | SetSearchMemberQueryAction + | LoadTeamGroupsAction; + +type ThunkResult = ThunkAction; + +const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ + type: ActionTypes.LoadTeams, + payload: teams, +}); + +const teamLoaded = (team: Team): LoadTeamAction => ({ + type: ActionTypes.LoadTeam, + payload: team, +}); + +const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({ + type: ActionTypes.LoadTeamMembers, + payload: teamMembers, +}); + +const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({ + type: ActionTypes.LoadTeamGroups, + payload: teamGroups, +}); + +export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ + type: ActionTypes.SetSearchMemberQuery, + payload: searchQuery, +}); + +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: searchQuery, +}); + +export function loadTeams(): ThunkResult { + return async dispatch => { + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + dispatch(teamsLoaded(response.teams)); + }; +} + +function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function loadTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .get(`/api/teams/${id}`) + .then(response => { + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); + }); + }; +} + +export function loadTeamMembers(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/members`) + .then(response => { + dispatch(teamMembersLoaded(response)); + }); + }; +} + +export function addTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/members`, { userId: id }) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + +export function removeTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/members/${id}`) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + +export function updateTeam(name: string, email: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + await getBackendSrv() + .put(`/api/teams/${team.id}`, { + name, + email, + }) + .then(() => { + dispatch(loadTeam(team.id)); + }); + }; +} + +export function loadTeamGroups(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/groups`) + .then(response => { + dispatch(teamGroupsLoaded(response)); + }); + }; +} + +export function addTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + +export function removeTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/groups/${groupId}`) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + +export function deleteTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .delete(`/api/teams/${id}`) + .then(() => { + dispatch(loadTeams()); + }); + }; +} diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts new file mode 100644 index 00000000000..7f7a33d60ac --- /dev/null +++ b/public/app/features/teams/state/reducers.test.ts @@ -0,0 +1,72 @@ +import { Action, ActionTypes } from './actions'; +import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers'; +import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks'; + +describe('teams reducer', () => { + it('should set teams', () => { + const payload = [getMockTeam()]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialTeamsState, action); + + expect(result.teams).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test'; + + const action: Action = { + type: ActionTypes.SetSearchQuery, + payload, + }; + + const result = teamsReducer(initialTeamsState, action); + + expect(result.searchQuery).toEqual('test'); + }); +}); + +describe('team reducer', () => { + it('should set team', () => { + const payload = getMockTeam(); + + const action: Action = { + type: ActionTypes.LoadTeam, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.team).toEqual(payload); + }); + + it('should set team members', () => { + const mockTeamMember = getMockTeamMember(); + + const action: Action = { + type: ActionTypes.LoadTeamMembers, + payload: [mockTeamMember], + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.members).toEqual([mockTeamMember]); + }); + + it('should set member search query', () => { + const payload = 'member'; + + const action: Action = { + type: ActionTypes.SetSearchMemberQuery, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.searchMemberQuery).toEqual('member'); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts new file mode 100644 index 00000000000..8b76028b9cb --- /dev/null +++ b/public/app/features/teams/state/reducers.ts @@ -0,0 +1,44 @@ +import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamState: TeamState = { + team: {} as Team, + members: [] as TeamMember[], + groups: [] as TeamGroup[], + searchMemberQuery: '', +}; + +export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { + switch (action.type) { + case ActionTypes.LoadTeams: + return { ...state, teams: action.payload }; + + case ActionTypes.SetSearchQuery: + return { ...state, searchQuery: action.payload }; + } + return state; +}; + +export const teamReducer = (state = initialTeamState, action: Action): TeamState => { + switch (action.type) { + case ActionTypes.LoadTeam: + return { ...state, team: action.payload }; + + case ActionTypes.LoadTeamMembers: + return { ...state, members: action.payload }; + + case ActionTypes.SetSearchMemberQuery: + return { ...state, searchMemberQuery: action.payload }; + + case ActionTypes.LoadTeamGroups: + return { ...state, groups: action.payload }; + } + + return state; +}; + +export default { + teams: teamsReducer, + team: teamReducer, +}; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts new file mode 100644 index 00000000000..5f338069bbb --- /dev/null +++ b/public/app/features/teams/state/selectors.test.ts @@ -0,0 +1,56 @@ +import { getTeam, getTeamMembers, getTeams } from './selectors'; +import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { Team, TeamGroup, TeamsState, TeamState } from '../../../types'; + +describe('Teams selectors', () => { + describe('Get teams', () => { + const mockTeams = getMultipleMockTeams(5); + + it('should return teams if no search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '' }; + + const teams = getTeams(mockState); + + expect(teams).toEqual(mockTeams); + }); + + it('Should filter teams if search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' }; + + const teams = getTeams(mockState); + + expect(teams.length).toEqual(1); + }); + }); +}); + +describe('Team selectors', () => { + describe('Get team', () => { + const mockTeam = getMockTeam(); + + it('should return team if matching with location team', () => { + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] }; + + const team = getTeam(mockState, '1'); + + expect(team).toEqual(mockTeam); + }); + }); + + describe('Get members', () => { + const mockTeamMembers = getMockTeamMembers(5); + + it('should return team members', () => { + const mockState: TeamState = { + team: {} as Team, + searchMemberQuery: '', + members: mockTeamMembers, + groups: [] as TeamGroup[], + }; + + const members = getTeamMembers(mockState); + + expect(members).toEqual(mockTeamMembers); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts new file mode 100644 index 00000000000..9201993bf0d --- /dev/null +++ b/public/app/features/teams/state/selectors.ts @@ -0,0 +1,30 @@ +import { Team, TeamsState, TeamState } from 'app/types'; + +export const getSearchQuery = (state: TeamsState) => state.searchQuery; +export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery; +export const getTeamGroups = (state: TeamState) => state.groups; +export const getTeamsCount = (state: TeamsState) => state.teams.length; + +export const getTeam = (state: TeamState, currentTeamId): Team | null => { + if (state.team.id === parseInt(currentTeamId, 10)) { + return state.team; + } + + return null; +}; + +export const getTeams = (state: TeamsState) => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.teams.filter(team => { + return regex.test(team.name); + }); +}; + +export const getTeamMembers = (state: TeamState) => { + const regex = RegExp(state.searchMemberQuery, 'i'); + + return state.members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); +}; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index a0b070cbcb4..519008d70f5 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -4,9 +4,9 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; +import TeamPages from 'app/features/teams/TeamPages'; +import TeamList from 'app/features/teams/TeamList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; -import TeamPages from 'app/containers/Teams/TeamPages'; -import TeamList from 'app/containers/Teams/TeamList'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index d869b0f740d..f87cc486b41 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -1,7 +1,6 @@ import _ from 'lodash'; import { types, getEnv } from 'mobx-state-tree'; import { NavItem } from './NavItem'; -import { Team } from '../TeamsStore/TeamsStore'; export const NavStore = types .model('NavStore', { @@ -116,43 +115,4 @@ export const NavStore = types self.main = NavItem.create(main); }, - - initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) { - const main = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: tab === 'members', - icon: 'gicon gicon-team', - id: 'team-members', - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: tab === 'settings', - icon: 'fa fa-fw fa-sliders', - id: 'team-settings', - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (isSyncEnabled) { - main.children.splice(1, 0, { - active: tab === 'groupsync', - icon: 'fa fa-fw fa-refresh', - id: 'team-settings', - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - self.main = NavItem.create(main); - }, })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index fba25e5f015..37c13f48c61 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -3,7 +3,6 @@ import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; -import { TeamsStore } from './../TeamsStore/TeamsStore'; export const RootStore = types.model({ nav: types.optional(NavStore, {}), @@ -17,9 +16,6 @@ export const RootStore = types.model({ routeParams: {}, }), folder: types.optional(FolderStore, {}), - teams: types.optional(TeamsStore, { - map: {}, - }), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts deleted file mode 100644 index bc8af218def..00000000000 --- a/public/app/stores/TeamsStore/TeamsStore.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const TeamMemberModel = types.model('TeamMember', { - userId: types.identifier(types.number), - teamId: types.number, - avatarUrl: types.string, - email: types.string, - login: types.string, -}); - -type TeamMemberType = typeof TeamMemberModel.Type; -export interface TeamMember extends TeamMemberType {} - -export const TeamGroupModel = types.model('TeamGroup', { - groupId: types.identifier(types.string), - teamId: types.number, -}); - -type TeamGroupType = typeof TeamGroupModel.Type; -export interface TeamGroup extends TeamGroupType {} - -export const TeamModel = types - .model('Team', { - id: types.identifier(types.number), - name: types.string, - avatarUrl: types.string, - email: types.string, - memberCount: types.number, - search: types.optional(types.string, ''), - members: types.optional(types.map(TeamMemberModel), {}), - groups: types.optional(types.map(TeamGroupModel), {}), - }) - .views(self => ({ - get filteredMembers(this: Team) { - const members = this.members.values(); - const regex = new RegExp(self.search, 'i'); - return members.filter(member => { - return regex.test(member.login) || regex.test(member.email); - }); - }, - })) - .actions(self => ({ - setName(name: string) { - self.name = name; - }, - - setEmail(email: string) { - self.email = email; - }, - - setSearchQuery(query: string) { - self.search = query; - }, - - update: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - - yield backendSrv.put(`/api/teams/${self.id}`, { - name: self.name, - email: self.email, - }); - }), - - loadMembers: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`); - self.members.clear(); - - for (const member of rsp) { - self.members.set(member.userId.toString(), TeamMemberModel.create(member)); - } - }), - - removeMember: flow(function* load(member: TeamMember) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`); - // remove from store map - self.members.delete(member.userId.toString()); - }), - - addMember: flow(function* load(userId: number) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.post(`/api/teams/${self.id}/members`, { userId: userId }); - }), - - loadGroups: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`); - self.groups.clear(); - - for (const group of rsp) { - self.groups.set(group.groupId, TeamGroupModel.create(group)); - } - }), - - addGroup: flow(function* load(groupId: string) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId }); - self.groups.set( - groupId, - TeamGroupModel.create({ - teamId: self.id, - groupId: groupId, - }) - ); - }), - - removeGroup: flow(function* load(groupId: string) { - const backendSrv = getEnv(self).backendSrv; - yield backendSrv.delete(`/api/teams/${self.id}/groups/${groupId}`); - self.groups.delete(groupId); - }), - })); - -type TeamType = typeof TeamModel.Type; -export interface Team extends TeamType {} - -export const TeamsStore = types - .model('TeamsStore', { - map: types.map(TeamModel), - search: types.optional(types.string, ''), - }) - .views(self => ({ - get filteredTeams(this: any) { - const teams = this.map.values(); - const regex = new RegExp(self.search, 'i'); - return teams.filter(team => { - return regex.test(team.name); - }); - }, - })) - .actions(self => ({ - loadTeams: flow(function* load() { - const backendSrv = getEnv(self).backendSrv; - const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 }); - self.map.clear(); - - for (const team of rsp.teams) { - self.map.set(team.id.toString(), TeamModel.create(team)); - } - }), - - setSearchQuery(query: string) { - self.search = query; - }, - - loadById: flow(function* load(id: string) { - if (self.map.has(id)) { - return; - } - - const backendSrv = getEnv(self).backendSrv; - const team = yield backendSrv.get(`/api/teams/${id}`); - self.map.set(id, TeamModel.create(team)); - }), - })); diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index b101b31a2a2..0cdc07fd31a 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -3,10 +3,12 @@ import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; +import teamsReducers from 'app/features/teams/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, + ...teamsReducers, }); export let store; diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts new file mode 100644 index 00000000000..3a696d3a8d6 --- /dev/null +++ b/public/app/types/alerting.ts @@ -0,0 +1,35 @@ +export interface AlertRuleDTO { + id: number; + dashboardId: number; + dashboardUid: string; + dashboardSlug: string; + panelId: number; + name: string; + state: string; + newStateDate: string; + evalDate: string; + evalData?: object; + executionError: string; + url: string; +} + +export interface AlertRule { + id: number; + dashboardId: number; + panelId: number; + name: string; + state: string; + stateText: string; + stateIcon: string; + stateClass: string; + stateAge: string; + url: string; + info?: string; + executionError?: string; + evalData?: { noData: boolean }; +} + +export interface AlertRulesState { + items: AlertRule[]; + searchQuery: string; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..92bcdb32836 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,96 +1,30 @@ -// -// Location -// +import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; +import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; +import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; +import { NavModel, NavModelItem, NavIndex } from './navModel'; -export interface LocationUpdate { - path?: string; - query?: UrlQueryMap; - routeParams?: UrlQueryMap; -} - -export interface LocationState { - url: string; - path: string; - query: UrlQueryMap; - routeParams: UrlQueryMap; -} - -export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; -export type UrlQueryMap = { [s: string]: UrlQueryValue }; - -// -// Alerting -// - -export interface AlertRuleApi { - id: number; - dashboardId: number; - dashboardUid: string; - dashboardSlug: string; - panelId: number; - name: string; - state: string; - newStateDate: string; - evalDate: string; - evalData?: object; - executionError: string; - url: string; -} - -export interface AlertRule { - id: number; - dashboardId: number; - panelId: number; - name: string; - state: string; - stateText: string; - stateIcon: string; - stateClass: string; - stateAge: string; - url: string; - info?: string; - executionError?: string; - evalData?: { noData: boolean }; -} - -// -// NavModel -// - -export interface NavModelItem { - text: string; - url: string; - subTitle?: string; - icon?: string; - img?: string; - id: string; - active?: boolean; - hideFromTabs?: boolean; - divider?: boolean; - children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; - target?: string; - parentItem?: NavModelItem; -} - -export interface NavModel { - main: NavModelItem; - node: NavModelItem; -} - -export type NavIndex = { [s: string]: NavModelItem }; - -// -// Store -// - -export interface AlertRulesState { - items: AlertRule[]; - searchQuery: string; -} +export { + Team, + TeamsState, + TeamState, + TeamGroup, + TeamMember, + AlertRuleDTO, + AlertRule, + AlertRulesState, + LocationState, + LocationUpdate, + NavModel, + NavModelItem, + NavIndex, + UrlQueryMap, + UrlQueryValue, +}; export interface StoreState { navIndex: NavIndex; location: LocationState; alertRules: AlertRulesState; + teams: TeamsState; + team: TeamState; } diff --git a/public/app/types/location.ts b/public/app/types/location.ts new file mode 100644 index 00000000000..4a7f51523a7 --- /dev/null +++ b/public/app/types/location.ts @@ -0,0 +1,15 @@ +export interface LocationUpdate { + path?: string; + query?: UrlQueryMap; + routeParams?: UrlQueryMap; +} + +export interface LocationState { + url: string; + path: string; + query: UrlQueryMap; + routeParams: UrlQueryMap; +} + +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; +export type UrlQueryMap = { [s: string]: UrlQueryValue }; diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts new file mode 100644 index 00000000000..aae4a030cb4 --- /dev/null +++ b/public/app/types/navModel.ts @@ -0,0 +1,22 @@ +export interface NavModelItem { + text: string; + url: string; + subTitle?: string; + icon?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children?: NavModelItem[]; + breadcrumbs?: Array<{ title: string; url: string }>; + target?: string; + parentItem?: NavModelItem; +} + +export interface NavModel { + main: NavModelItem; + node: NavModelItem; +} + +export type NavIndex = { [s: string]: NavModelItem }; diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts new file mode 100644 index 00000000000..bcf752c86c0 --- /dev/null +++ b/public/app/types/teams.ts @@ -0,0 +1,32 @@ +export interface Team { + id: number; + name: string; + avatarUrl: string; + email: string; + memberCount: number; +} + +export interface TeamMember { + userId: number; + teamId: number; + avatarUrl: string; + email: string; + login: string; +} + +export interface TeamGroup { + groupId: string; + teamId: number; +} + +export interface TeamsState { + teams: Team[]; + searchQuery: string; +} + +export interface TeamState { + team: Team; + members: TeamMember[]; + groups: TeamGroup[]; + searchMemberQuery: string; +} From 19c7dd9834f88b2e8aa6623ad3d758c41fdeee69 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 14:25:25 +0200 Subject: [PATCH 078/666] support template variables with multiple values --- public/app/plugins/datasource/grafana/datasource.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 3d788378045..b3de9a9c85a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -57,8 +57,11 @@ class GrafanaDatasource { return this.$q.when([]); } const tags = []; - for (let t of params.tags) { - tags.push(this.templateSrv.replace(t)); + for (const t of params.tags) { + const renderedValues = this.templateSrv.replace(t, {}, 'pipe'); + for (const tt of renderedValues.split('|')) { + tags.push(tt); + } } params.tags = tags; } From 953bdc4dc063c85ac00e0e8536f1565e1c236144 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 11 Sep 2018 14:53:38 +0200 Subject: [PATCH 079/666] put folder name under dashboard name, tweaked aliginments in search results --- public/app/core/components/search/search_results.html | 3 ++- public/sass/components/_search.scss | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 9f266ed3a6b..45258ded652 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -33,7 +33,8 @@ -
    {{::item.title}} {{::item.folderTitle}}
    +
    {{::item.title}}
    + {{::item.folderTitle}}
    diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 1589cc1e52c..b1211bcbdee 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -210,18 +210,20 @@ .search-item__body-title { color: $list-item-link-color; + line-height: 14px; } .search-item__body-folder-title { color: $text-color-weak; - padding-left: 0.25rem; font-size: $font-size-xs; + line-height: 11px; } .search-item__icon { padding: 5px; flex: 0 0 auto; font-size: 19px; + line-height: 22px; padding: 5px 2px 5px 10px; } From 1638c6bea11f196b69611b28890eddebc91d933e Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 15:50:04 +0200 Subject: [PATCH 080/666] enable partial tag matches for annotations --- pkg/api/annotations.go | 21 +++++----- pkg/services/annotations/annotations.go | 1 + pkg/services/sqlstore/annotation.go | 7 +++- pkg/services/sqlstore/annotation_test.go | 41 ++++++++++++++++++- .../plugins/datasource/grafana/datasource.ts | 1 + .../grafana/partials/annotations.editor.html | 27 ++++++++---- 6 files changed, 76 insertions(+), 22 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 55c9c954940..eec07bb9f81 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -14,16 +14,17 @@ import ( func GetAnnotations(c *m.ReqContext) Response { query := &annotations.ItemQuery{ - From: c.QueryInt64("from"), - To: c.QueryInt64("to"), - OrgId: c.OrgId, - UserId: c.QueryInt64("userId"), - AlertId: c.QueryInt64("alertId"), - DashboardId: c.QueryInt64("dashboardId"), - PanelId: c.QueryInt64("panelId"), - Limit: c.QueryInt64("limit"), - Tags: c.QueryStrings("tags"), - Type: c.Query("type"), + From: c.QueryInt64("from"), + To: c.QueryInt64("to"), + OrgId: c.OrgId, + UserId: c.QueryInt64("userId"), + AlertId: c.QueryInt64("alertId"), + DashboardId: c.QueryInt64("dashboardId"), + PanelId: c.QueryInt64("panelId"), + Limit: c.QueryInt64("limit"), + Tags: c.QueryStrings("tags"), + Type: c.Query("type"), + PartialMatch: c.QueryBool("partialMatch"), } repo := annotations.GetRepository() diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index 9b490169d3b..daea43863f4 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -21,6 +21,7 @@ type ItemQuery struct { RegionId int64 `json:"regionId"` Tags []string `json:"tags"` Type string `json:"type"` + PartialMatch bool `json:"partialMatch"` Limit int64 `json:"limit"` } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index a65bc136554..6e25ce432f3 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -211,7 +211,12 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ) `, strings.Join(keyValueFilters, " OR ")) - sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) + if query.PartialMatch { + sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery)) + } else { + sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) + } + } } diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index c0d267f2578..4c31e0442c8 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -78,7 +78,31 @@ func TestAnnotations(t *testing.T) { So(err, ShouldBeNil) So(annotation2.Id, ShouldBeGreaterThan, 0) - Convey("Can query for annotation", func() { + globalAnnotation1 := &annotations.Item{ + OrgId: 1, + UserId: 1, + Text: "deploy", + Type: "", + Epoch: 15, + Tags: []string{"deploy"}, + } + err = repo.Save(globalAnnotation1) + So(err, ShouldBeNil) + So(globalAnnotation1.Id, ShouldBeGreaterThan, 0) + + globalAnnotation2 := &annotations.Item{ + OrgId: 1, + UserId: 1, + Text: "rollback", + Type: "", + Epoch: 17, + Tags: []string{"rollback"}, + } + err = repo.Save(globalAnnotation2) + So(err, ShouldBeNil) + So(globalAnnotation2.Id, ShouldBeGreaterThan, 0) + + Convey("Can query for annotation by dashboard id", func() { items, err := repo.Find(&annotations.ItemQuery{ OrgId: 1, DashboardId: 1, @@ -165,7 +189,7 @@ func TestAnnotations(t *testing.T) { OrgId: 1, DashboardId: 1, From: 1, - To: 15, + To: 15, //this will exclude the second test annotation Tags: []string{"outage", "error"}, }) @@ -173,6 +197,19 @@ func TestAnnotations(t *testing.T) { So(items, ShouldHaveLength, 1) }) + Convey("Should find two annotations using partial match", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + From: 1, + To: 25, + PartialMatch: true, + Tags: []string{"rollback", "deploy"}, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 2) + }) + Convey("Should find one when all key value tag filters does match", func() { items, err := repo.Find(&annotations.ItemQuery{ OrgId: 1, diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index b3de9a9c85a..4ddfa8df40d 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -40,6 +40,7 @@ class GrafanaDatasource { to: options.range.to.valueOf(), limit: options.annotation.limit, tags: options.annotation.tags, + partialMatch: options.annotation.partialMatch, }; if (options.annotation.type === 'dashboard') { diff --git a/public/app/plugins/datasource/grafana/partials/annotations.editor.html b/public/app/plugins/datasource/grafana/partials/annotations.editor.html index 4289a58e5cb..ba68a08cefd 100644 --- a/public/app/plugins/datasource/grafana/partials/annotations.editor.html +++ b/public/app/plugins/datasource/grafana/partials/annotations.editor.html @@ -2,7 +2,7 @@
    - + Filter by
      @@ -11,18 +11,11 @@
    -
    +
    - -
    - Tags - - -
    -
    Max limit
    @@ -31,6 +24,22 @@
    +
    +
    + +
    +
    + Tags + + +
    +
    From 0768a078ede818da6d1593f7d2fae747d77da742 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 11 Sep 2018 16:31:34 +0200 Subject: [PATCH 081/666] Upgrade react and enzyme (#13224) * upgrade to latest react and fixed failing test * upgrading libs * grunt exec update due to change filename * new yarn lock * updated snaps --- package.json | 22 +- .../__snapshots__/TeamPicker.test.tsx.snap | 4 +- .../__snapshots__/UserPicker.test.tsx.snap | 4 +- .../__snapshots__/SideMenu.test.tsx.snap | 21 +- scripts/grunt/options/exec.js | 2 +- yarn.lock | 2521 +++++++++-------- 6 files changed, 1354 insertions(+), 1220 deletions(-) diff --git a/package.json b/package.json index 29196ffdf01..071d32992af 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ }, "devDependencies": { "@types/d3": "^4.10.1", - "@types/enzyme": "^2.8.9", + "@types/enzyme": "^3.1.13", "@types/jest": "^21.1.4", "@types/node": "^8.0.31", - "@types/react": "^16.0.25", + "@types/react": "^16.4.14", "@types/react-custom-scrollbars": "^4.0.5", - "@types/react-dom": "^16.0.3", + "@types/react-dom": "^16.0.7", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", "axios": "^0.17.1", @@ -26,9 +26,9 @@ "babel-preset-es2015": "^6.24.1", "clean-webpack-plugin": "^0.1.19", "css-loader": "^0.28.7", - "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "^1.0.1", - "enzyme-to-json": "^3.3.0", + "enzyme": "^3.6.0", + "enzyme-adapter-react-16": "^1.5.0", + "enzyme-to-json": "^3.3.4", "es6-promise": "^3.0.2", "es6-shim": "^0.35.3", "expect.js": "~0.2.0", @@ -72,8 +72,8 @@ "postcss-loader": "^2.0.6", "postcss-reporter": "^5.0.0", "prettier": "1.9.2", - "react-hot-loader": "^4.2.0", - "react-test-renderer": "^16.0.0", + "react-hot-loader": "^4.3.6", + "react-test-renderer": "^16.5.0", "sass-lint": "^1.10.2", "sass-loader": "^7.0.1", "sinon": "1.17.6", @@ -153,11 +153,11 @@ "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", "prismjs": "^1.6.0", - "prop-types": "^15.6.0", + "prop-types": "^15.6.2", "rc-cascader": "^0.14.0", - "react": "^16.2.0", + "react": "^16.5.0", "react-custom-scrollbars": "^4.2.1", - "react-dom": "^16.2.0", + "react-dom": "^16.5.0", "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", "react-popper": "^0.7.5", diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap index c63cc880900..9c21da5fdc4 100644 --- a/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap +++ b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap @@ -15,7 +15,7 @@ exports[`TeamPicker renders correctly 1`] = ` onTouchMove={[Function]} onTouchStart={[Function]} > - @@ -66,7 +66,7 @@ exports[`TeamPicker renders correctly 1`] = `
    - +
    - +
    ,
    , + > + + + +  Close + +
    , , diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index e22d060ea04..087439f7ea9 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -7,7 +7,7 @@ module.exports = function(config, grunt) { } return { - tslint: 'node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json', + tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json', jest: 'node ./node_modules/jest-cli/bin/jest.js ' + coverage, webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js', }; diff --git a/yarn.lock b/yarn.lock index 03131261b22..fa079d15b72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,18 +3,18 @@ "@babel/code-frame@^7.0.0-beta.35": - version "7.0.0-beta.49" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz#becd805482734440c9d137e46d77340e64d7f51b" + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" dependencies: - "@babel/highlight" "7.0.0-beta.49" + "@babel/highlight" "^7.0.0" -"@babel/highlight@7.0.0-beta.49": - version "7.0.0-beta.49" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.49.tgz#96bdc6b43e13482012ba6691b1018492d39622cc" +"@babel/highlight@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" dependencies: chalk "^2.0.0" esutils "^2.0.2" - js-tokens "^3.0.0" + js-tokens "^4.0.0" "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -24,8 +24,8 @@ glob-to-regexp "^0.3.0" "@nodelib/fs.stat@^1.0.1": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a" + version "1.1.2" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.2.tgz#54c5a964462be3d4d78af631363c18d6fa91ac26" "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" @@ -38,8 +38,8 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" "@types/cheerio@*": - version "0.22.7" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce" + version "0.22.9" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b" "@types/d3-array@*": version "1.2.1" @@ -102,8 +102,8 @@ "@types/geojson" "*" "@types/d3-hierarchy@*": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.2.tgz#63a4e433f321ffc4dbd9aa05ff95962072b51993" + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.4.tgz#b04dfcb1f2074da789ada10fe4942d13f0bce421" "@types/d3-interpolate@*": version "1.2.0" @@ -120,20 +120,20 @@ resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.6.tgz#db25c630a2afb9191fe51ba61dd37baee9dd44c7" "@types/d3-quadtree@*": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.5.tgz#1ce1e659eae4530df0cb127f297f1741a367a82e" + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.6.tgz#45da9e603688ba90eedd3d40f6e504764e06e493" "@types/d3-queue@*": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-queue/-/d3-queue-3.0.6.tgz#2f5aa7eca3b153bb49687eaa570c10ab713439d3" + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-queue/-/d3-queue-3.0.7.tgz#94dc7af693281ab78ccdf381a8c1f71ef16659c1" "@types/d3-random@*": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.1.tgz#38647ce2ff4ce7d0d56974334c1c4092513c8b9f" "@types/d3-request@*": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-request/-/d3-request-1.0.2.tgz#db9db8154f47816584706c6e6f702be66f22f4be" + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-request/-/d3-request-1.0.3.tgz#f528d12efdc83dbc3df486746c939fc5519dc79c" dependencies: "@types/d3-dsv" "*" @@ -144,12 +144,12 @@ "@types/d3-time" "*" "@types/d3-selection@*": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.1.tgz#c6227f4e39d429cc429ce3882fd533facc7f014c" + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.2.tgz#dd5661a560ba9ce3aba823c424b8d4a1bc7e833f" "@types/d3-shape@*": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.3.tgz#cadc9f93a626db9190f306048a650df4ffa4e500" + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.4.tgz#e65585f2254d83ae42c47af2e730dd9b97952996" dependencies: "@types/d3-path" "*" @@ -166,14 +166,14 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.7.tgz#053e6369d9485c9dc80bc62fc0851123341d7816" "@types/d3-transition@*": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.1.tgz#c209fce6a966d6696356dd42b091a9c6cc79929f" + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.2.tgz#1106fc3129decc9ad5682a9f52b2dfa52f14e57c" dependencies: "@types/d3-selection" "*" "@types/d3-voronoi@*": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.7.tgz#c0a145cf04395927e01706ff6c4ff835c97a8ece" + version "1.1.8" + resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.8.tgz#a039cb8368bce4efc1a70aebe744d210851cf1a7" "@types/d3-zoom@*": version "1.7.1" @@ -217,28 +217,34 @@ "@types/d3-voronoi" "*" "@types/d3-zoom" "*" -"@types/enzyme@^2.8.9": - version "2.8.12" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-2.8.12.tgz#a669d79ce1760d7241bc4b6fb7535d68669d78ad" +"@types/enzyme@^3.1.13": + version "3.1.13" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea" dependencies: "@types/cheerio" "*" "@types/react" "*" "@types/geojson@*": - version "7946.0.3" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.3.tgz#e5791534ab0acfb2b3a39b713966cfcee85d469f" + version "7946.0.4" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.4.tgz#4e049756383c3f055dd8f3d24e63fb543e98eb07" "@types/jest@^21.1.4": version "21.1.10" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.10.tgz#dcacb5217ddf997a090cc822bba219b4b2fd7984" "@types/node@*": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.3.tgz#5c16980936c4e3c83ce64e8ed71fb37bd7aea135" + version "10.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897" "@types/node@^8.0.31": - version "8.10.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.17.tgz#d48cf10f0dc6dcf59f827f5a3fc7a4a6004318d3" + version "8.10.29" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.29.tgz#b3a13b58dd7b0682bf1b42022bef4a5a9718f687" + +"@types/prop-types@*": + version "15.5.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.5.tgz#17038dd322c2325f5da650a94d5f9974943625e3" + dependencies: + "@types/react" "*" "@types/react-custom-scrollbars@^4.0.5": version "4.0.5" @@ -246,17 +252,18 @@ dependencies: "@types/react" "*" -"@types/react-dom@^16.0.3": - version "16.0.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c" +"@types/react-dom@^16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.7.tgz#54d0f867a76b90597e8432030d297982f25c20ba" dependencies: "@types/node" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16.0.25": - version "16.3.16" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.16.tgz#78fc44a90b45701f50c8a7008f733680ba51fc86" +"@types/react@*", "@types/react@^16.4.14": + version "16.4.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.14.tgz#47c604c8e46ed674bbdf4aabf82b34b9041c6a04" dependencies: + "@types/prop-types" "*" csstype "^2.2.0" "@types/tapable@^0": @@ -264,165 +271,167 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-0.2.5.tgz#2443fc12da514c81346b1a665675559cee21fa75" "@types/uglify-js@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.2.tgz#f30c75458d18e8ee885c792c04adcb78a13bc286" + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.3.tgz#801a5ca1dc642861f47c46d14b700ed2d610840b" dependencies: source-map "^0.6.1" "@types/webpack@^3.0.5": - version "3.8.12" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-3.8.12.tgz#c5db4f273fb8f2a4929db6c486e19e68c350e7ac" + version "3.8.14" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-3.8.14.tgz#e2bfdf7f604b3f7dc776eaa17446d7f7538f3de7" dependencies: "@types/node" "*" "@types/tapable" "^0" "@types/uglify-js" "*" source-map "^0.6.0" -"@webassemblyjs/ast@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.10.tgz#7f1e81149ca4e103c9e7cc321ea0dcb83a392512" +"@webassemblyjs/ast@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.6.tgz#3ef8c45b3e5e943a153a05281317474fef63e21e" dependencies: - "@webassemblyjs/helper-module-context" "1.5.10" - "@webassemblyjs/helper-wasm-bytecode" "1.5.10" - "@webassemblyjs/wast-parser" "1.5.10" - debug "^3.1.0" + "@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/floating-point-hex-parser@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.10.tgz#ae48705fd58927df62023f114520b8215330ff86" +"@webassemblyjs/floating-point-hex-parser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.6.tgz#7cb37d51a05c3fe09b464ae7e711d1ab3837801f" -"@webassemblyjs/helper-api-error@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.10.tgz#0baf9453ce2fd8db58f0fdb4fb2852557c71d5a7" +"@webassemblyjs/helper-api-error@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.6.tgz#99b7e30e66f550a2638299a109dda84a622070ef" -"@webassemblyjs/helper-buffer@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.10.tgz#abee4284161e9cd6ba7619785ca277bfcb8052ce" +"@webassemblyjs/helper-buffer@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.6.tgz#ba0648be12bbe560c25c997e175c2018df39ca3e" + +"@webassemblyjs/helper-code-frame@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.6.tgz#5a94d21b0057b69a7403fca0c253c3aaca95b1a5" dependencies: - debug "^3.1.0" + "@webassemblyjs/wast-printer" "1.7.6" -"@webassemblyjs/helper-code-frame@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.10.tgz#4e23c05431665f16322104580af7c06253d4b4e0" - dependencies: - "@webassemblyjs/wast-printer" "1.5.10" +"@webassemblyjs/helper-fsm@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.6.tgz#ae1741c6f6121213c7a0b587fb964fac492d3e49" -"@webassemblyjs/helper-fsm@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.10.tgz#490bab613ea255a9272b764826d3cc9d15170676" - -"@webassemblyjs/helper-module-context@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.10.tgz#6fca93585228bf33e6da076d0a1373db1fdd6580" +"@webassemblyjs/helper-module-context@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.6.tgz#116d19a51a6cebc8900ad53ca34ff8269c668c23" dependencies: mamacro "^0.0.3" -"@webassemblyjs/helper-wasm-bytecode@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.10.tgz#90f6da93c7a186bfb2f587de442982ff533c4b44" +"@webassemblyjs/helper-wasm-bytecode@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.6.tgz#98e515eaee611aa6834eb5f6a7f8f5b29fefb6f1" -"@webassemblyjs/helper-wasm-section@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.10.tgz#d64292a19f7f357c49719461065efdf7ec975d66" +"@webassemblyjs/helper-wasm-section@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.6.tgz#783835867bdd686df7a95377ab64f51a275e8333" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/helper-buffer" "1.5.10" - "@webassemblyjs/helper-wasm-bytecode" "1.5.10" - "@webassemblyjs/wasm-gen" "1.5.10" - debug "^3.1.0" + "@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/ieee754@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.5.10.tgz#257cad440dd6c8a339402d31e035ba2e38e9c245" +"@webassemblyjs/ieee754@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.6.tgz#c34fc058f2f831fae0632a8bb9803cf2d3462eb1" dependencies: - ieee754 "^1.1.11" + "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.5.10.tgz#a8e4fe5f4b16daadb241fcc44d9735e9f27b05a3" +"@webassemblyjs/leb128@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.6.tgz#197f75376a29f6ed6ace15898a310d871d92f03b" dependencies: - leb "^0.3.0" + "@xtuc/long" "4.2.1" -"@webassemblyjs/utf8@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.5.10.tgz#0b3b6bc86b7619c5dc7b2789db6665aa35689983" +"@webassemblyjs/utf8@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.6.tgz#eb62c66f906af2be70de0302e29055d25188797d" -"@webassemblyjs/wasm-edit@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.10.tgz#0fe80f19e57f669eab1caa8c1faf9690b259d5b9" +"@webassemblyjs/wasm-edit@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.6.tgz#fa41929160cd7d676d4c28ecef420eed5b3733c5" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/helper-buffer" "1.5.10" - "@webassemblyjs/helper-wasm-bytecode" "1.5.10" - "@webassemblyjs/helper-wasm-section" "1.5.10" - "@webassemblyjs/wasm-gen" "1.5.10" - "@webassemblyjs/wasm-opt" "1.5.10" - "@webassemblyjs/wasm-parser" "1.5.10" - "@webassemblyjs/wast-printer" "1.5.10" - debug "^3.1.0" + "@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-gen@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.10.tgz#8b29ddd3651259408ae5d5c816a011fb3f3f3584" +"@webassemblyjs/wasm-gen@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.6.tgz#695ac38861ab3d72bf763c8c75e5f087ffabc322" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/helper-wasm-bytecode" "1.5.10" - "@webassemblyjs/ieee754" "1.5.10" - "@webassemblyjs/leb128" "1.5.10" - "@webassemblyjs/utf8" "1.5.10" + "@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-opt@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.10.tgz#569e45ab1b2bf0a7706cdf6d1b51d1188e9e4c7b" +"@webassemblyjs/wasm-opt@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.6.tgz#fbafa78e27e1a75ab759a4b658ff3d50b4636c21" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/helper-buffer" "1.5.10" - "@webassemblyjs/wasm-gen" "1.5.10" - "@webassemblyjs/wasm-parser" "1.5.10" - debug "^3.1.0" + "@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-parser@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.10.tgz#3e1017e49f833f46b840db7cf9d194d4f00037ff" +"@webassemblyjs/wasm-parser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.6.tgz#84eafeeff405ad6f4c4b5777d6a28ae54eed51fe" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/helper-api-error" "1.5.10" - "@webassemblyjs/helper-wasm-bytecode" "1.5.10" - "@webassemblyjs/ieee754" "1.5.10" - "@webassemblyjs/leb128" "1.5.10" - "@webassemblyjs/wasm-parser" "1.5.10" + "@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/wast-parser@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.5.10.tgz#1a3235926483c985a00ee8ebca856ffda9544934" +"@webassemblyjs/wast-parser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.6.tgz#ca4d20b1516e017c91981773bd7e819d6bd9c6a7" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/floating-point-hex-parser" "1.5.10" - "@webassemblyjs/helper-api-error" "1.5.10" - "@webassemblyjs/helper-code-frame" "1.5.10" - "@webassemblyjs/helper-fsm" "1.5.10" - long "^3.2.0" + "@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-printer@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.5.10.tgz#adb38831ba45efd0a5c7971b666e179b64f68bba" +"@webassemblyjs/wast-printer@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.6.tgz#a6002c526ac5fa230fe2c6d2f1bdbf4aead43a5e" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/wast-parser" "1.5.10" - long "^3.2.0" + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/wast-parser" "1.7.6" + "@xtuc/long" "4.2.1" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + +"@xtuc/long@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8" JSONStream@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.3.tgz#27b4b8fbbfeab4e71bcf551e7f27be8d952239bf" + version "1.3.4" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e" dependencies: jsonparse "^1.2.0" through ">=2.2.7 <3" -abab@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" +abab@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" abbrev@1, abbrev@~1.1.1: version "1.1.1" @@ -459,19 +468,15 @@ acorn-jsx@^3.0.0: acorn@^3.0.4: version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + resolved "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.0: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - -acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: - version "5.6.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7" +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.2: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" acorn@~2.6.4: version "2.6.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.6.4.tgz#eb1f45b4a43fa31d03701a5ec46f3b52673e90ee" + resolved "http://registry.npmjs.org/acorn/-/acorn-2.6.4.tgz#eb1f45b4a43fa31d03701a5ec46f3b52673e90ee" add-dom-event-listener@1.x: version "1.0.2" @@ -484,17 +489,21 @@ add-px-to-style@1.0.0: resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a" agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" dependencies: es6-promisify "^5.0.0" agentkeepalive@^3.3.0, agentkeepalive@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" + version "3.5.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.1.tgz#4eba75cf2ad258fc09efd506cdb8d8c2971d35a4" dependencies: humanize-ms "^1.2.1" +ajv-errors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" + ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -510,7 +519,7 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.0: +ajv@^5.1.0, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -520,13 +529,13 @@ ajv@^5.1.0: json-schema-traverse "^0.3.0" ajv@^6.1.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.0.tgz#4c8affdf80887d8f132c9c52ab8a2dc4d0b7b24c" + version "6.5.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9" dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - uri-js "^4.2.1" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" @@ -580,6 +589,10 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" +ansi-colors@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.0.5.tgz#cb9dc64993b64fd6945485f797fc3853137d9a7b" + ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -638,8 +651,8 @@ anymatch@^2.0.0: normalize-path "^2.1.1" app-root-path@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" + version "2.1.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a" append-transform@^0.4.0: version "0.4.0" @@ -651,6 +664,10 @@ aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" +"aproba@^1.1.2 || 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + archiver-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-1.3.0.tgz#e50b4c09c70bf3d680e32ff1b7994e9f9d895174" @@ -742,13 +759,6 @@ array-flatten@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" -array-includes@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" - dependencies: - define-properties "^1.1.2" - es-abstract "^1.7.0" - array-tree-filter@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-1.0.1.tgz#0a8ad1eefd38ce88858632f9cc0423d7634e4d5d" @@ -771,11 +781,19 @@ array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" +array.prototype.flat@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.10.0" + function-bind "^1.1.1" + arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" -asap@^2.0.0, asap@~2.0.3: +asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -788,17 +806,15 @@ asn1.js@^4.0.0: minimalistic-assert "^1.0.0" asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + dependencies: + safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - assert@^1.1.1: version "1.4.1" resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" @@ -837,11 +853,11 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@^1.4.0, async@^1.5.0, async@^1.5.2, async@~1.5.2: +async@^1.5.0, async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.4, async@^2.6.0: +async@^2.0.0, async@^2.1.4, async@^2.5.0, async@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: @@ -856,8 +872,8 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" atob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a" + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" autolinker@~0.15.0: version "0.15.3" @@ -874,17 +890,13 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0: postcss "^5.2.16" postcss-value-parser "^3.2.3" -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" -aws4@^1.2.1, aws4@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" +aws4@^1.6.0, aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" axios@^0.17.1: version "0.17.1" @@ -1064,8 +1076,8 @@ babel-jest@^23.6.0: babel-preset-jest "^23.2.0" babel-loader@^7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015" + version "7.1.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68" dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" @@ -1085,7 +1097,7 @@ babel-plugin-check-es2015-constants@^6.22.0: babel-plugin-istanbul@^4.1.6: version "4.1.6" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" + resolved "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" dependencies: babel-plugin-syntax-object-rest-spread "^6.13.0" find-up "^2.1.0" @@ -1098,43 +1110,43 @@ babel-plugin-jest-hoist@^23.2.0: babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + resolved "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" babel-plugin-syntax-async-generators@^6.5.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" + resolved "http://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" babel-plugin-syntax-class-constructor-call@^6.18.0: version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" + resolved "http://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" babel-plugin-syntax-class-properties@^6.8.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + resolved "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" babel-plugin-syntax-decorators@^6.13.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" + resolved "http://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" babel-plugin-syntax-dynamic-import@^6.18.0: version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + resolved "http://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" babel-plugin-syntax-exponentiation-operator@^6.8.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + resolved "http://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" babel-plugin-syntax-export-extensions@^6.8.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" + resolved "http://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" babel-plugin-syntax-flow@^6.18.0: version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + resolved "http://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + resolved "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" babel-plugin-syntax-trailing-function-commas@^6.22.0: version "6.22.0" @@ -1268,8 +1280,8 @@ babel-plugin-transform-es2015-modules-amd@^6.24.1: babel-template "^6.24.1" babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + version "6.26.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" dependencies: babel-plugin-transform-strict-mode "^6.24.1" babel-runtime "^6.26.0" @@ -1561,8 +1573,8 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" dependencies: tweetnacl "^0.14.3" @@ -1610,8 +1622,8 @@ block-stream@*: inherits "~2.0.0" bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + version "3.5.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.2.tgz#1be0908e054a751754549c270489c1505d4ab15a" bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1647,12 +1659,6 @@ boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - boxen@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -1739,12 +1745,13 @@ browserify-cipher@^1.0.0: evp_bytestokey "^1.0.0" browserify-des@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c" + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" dependencies: cipher-base "^1.0.1" des.js "^1.0.0" inherits "^2.0.1" + safe-buffer "^5.1.2" browserify-rsa@^4.0.0: version "4.0.1" @@ -1794,7 +1801,7 @@ buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" -buffer-alloc@^1.1.0: +buffer-alloc@^1.1.0, buffer-alloc@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" dependencies: @@ -1810,8 +1817,8 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" buffer-from@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" buffer-indexof@^1.0.0: version "1.1.1" @@ -1823,12 +1830,19 @@ buffer-xor@^1.0.3: buffer@^4.3.0: version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + resolved "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1872,15 +1886,15 @@ cacache@^10.0.0, cacache@^10.0.4: y18n "^4.0.0" cacache@^11.0.1, cacache@^11.0.2: - version "11.0.2" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.0.2.tgz#ff30541a05302200108a759e660e30786f788764" + version "11.2.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965" dependencies: bluebird "^3.5.1" chownr "^1.0.1" figgy-pudding "^3.1.0" glob "^7.1.2" graceful-fs "^4.1.11" - lru-cache "^4.1.2" + lru-cache "^4.1.3" mississippi "^3.0.0" mkdirp "^0.5.1" move-concurrently "^1.0.1" @@ -1992,12 +2006,8 @@ capture-exit@^1.2.0: rsvp "^3.3.3" capture-stack-trace@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" - -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + version "1.0.1" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" caseless@~0.12.0: version "0.12.0" @@ -2012,7 +2022,7 @@ center-align@^0.1.1: chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.1: version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + resolved "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: ansi-styles "^2.2.1" escape-string-regexp "^1.0.2" @@ -2030,7 +2040,7 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1: chalk@~0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" + resolved "http://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" dependencies: ansi-styles "~1.0.0" has-color "~0.1.0" @@ -2063,9 +2073,13 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + check-types@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" + version "7.4.0" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.4.0.tgz#0378ec1b9616ec71f774931a3c6516fad8c152f4" cheerio@^1.0.0-rc.2: version "1.0.0-rc.2" @@ -2078,25 +2092,7 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^2.0.0, chokidar@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176" - dependencies: - anymatch "^2.0.0" - async-each "^1.0.0" - braces "^2.3.0" - glob-parent "^3.1.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^2.1.1" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - upath "^1.0.0" - optionalDependencies: - fsevents "^1.1.2" - -chokidar@^2.0.4: +chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" dependencies: @@ -2119,13 +2115,15 @@ chownr@^1.0.1, chownr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" -chrome-trace-event@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.3.tgz#d395af2d31c87b90a716c831fe326f69768ec084" +chrome-trace-event@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" + dependencies: + tslib "^1.9.0" -ci-info@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2" +ci-info@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.5.1.tgz#17e8eb5de6f8b2b6038f0cbb714d410bfa9f3030" cidr-regex@1.0.6: version "1.0.6" @@ -2157,11 +2155,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.x, classnames@^2.2.4, classnames@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" - -classnames@^2.2.6: +classnames@2.x, classnames@^2.2.4, classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -2172,11 +2166,11 @@ clean-css@3.4.x, clean-css@~3.4.2: commander "2.8.x" source-map "0.4.x" -clean-css@4.1.x: - version "4.1.11" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a" +clean-css@4.2.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" dependencies: - source-map "0.5.x" + source-map "~0.6.0" clean-webpack-plugin@^0.1.19: version "0.1.19" @@ -2211,7 +2205,7 @@ cli-spinners@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" -cli-table2@^0.2.0, cli-table2@~0.2.0: +cli-table2@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" dependencies: @@ -2220,6 +2214,15 @@ cli-table2@^0.2.0, cli-table2@~0.2.0: optionalDependencies: colors "^1.1.2" +cli-table3@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" + dependencies: + object-assign "^4.1.0" + string-width "^2.1.1" + optionalDependencies: + colors "^1.1.2" + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -2309,8 +2312,8 @@ clone@^1.0.0, clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" clone@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" cloneable-readable@^1.0.0: version "1.1.2" @@ -2357,12 +2360,12 @@ collection-visit@^1.0.0: object-visit "^1.0.0" color-convert@^1.3.0, color-convert@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" dependencies: - color-name "^1.1.1" + color-name "1.1.3" -color-name@^1.0.0, color-name@^1.1.1: +color-name@1.1.3, color-name@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -2397,8 +2400,8 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" colors@^1.1.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e" + version "1.3.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" colors@~1.1.2: version "1.1.2" @@ -2411,29 +2414,33 @@ columnify@~1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" -combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: +combined-stream@1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" -commander@2, commander@2.15.x, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.8.1, commander@^2.9.0, commander@~2.15.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" +commander@2, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.8.1, commander@^2.9.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" commander@2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" +commander@2.17.x, commander@~2.17.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + commander@2.8.x: version "2.8.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + resolved "http://registry.npmjs.org/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" dependencies: graceful-readlink ">= 1.0.0" commander@2.9.x: version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + resolved "http://registry.npmjs.org/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: graceful-readlink ">= 1.0.0" @@ -2468,22 +2475,22 @@ compress-commons@^1.2.0: normalize-path "^2.0.0" readable-stream "^2.0.0" -compressible@~2.0.13: +compressible@~2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.14.tgz#326c5f507fbb055f54116782b969a81b67a29da7" dependencies: mime-db ">= 1.34.0 < 2" compression@^1.5.2: - version "1.7.2" - resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69" + version "1.7.3" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" dependencies: - accepts "~1.3.4" + accepts "~1.3.5" bytes "3.0.0" - compressible "~2.0.13" + compressible "~2.0.14" debug "2.6.9" on-headers "~1.0.1" - safe-buffer "5.1.1" + safe-buffer "5.1.2" vary "~1.1.2" concat-map@0.0.1: @@ -2551,12 +2558,14 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" convert-source-map@^1.4.0, convert-source-map@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + dependencies: + safe-buffer "~5.1.1" convert-source-map@~1.1.2: version "1.1.3" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + resolved "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" cookie-signature@1.0.6: version "1.0.6" @@ -2581,10 +2590,6 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - core-js@^2.0.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" @@ -2593,18 +2598,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" - dependencies: - is-directory "^0.3.1" - js-yaml "^3.4.3" - minimist "^1.2.0" - object-assign "^4.1.0" - os-homedir "^1.0.1" - parse-json "^2.2.0" - require-from-string "^1.1.0" - cosmiconfig@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc" @@ -2622,8 +2615,10 @@ crc32-stream@^2.0.0: readable-stream "^2.0.0" crc@^3.4.4: - version "3.5.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.5.0.tgz#98b8ba7d489665ba3979f59b21381374101a1964" + version "3.8.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + dependencies: + buffer "^5.1.0" create-ecdh@^4.0.0: version "4.0.3" @@ -2674,7 +2669,7 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^6.0.5: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" dependencies: @@ -2684,12 +2679,6 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2810,18 +2799,18 @@ csso@~2.3.1: source-map "^0.5.3" cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" + version "0.3.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797" -"cssstyle@>= 0.3.1 < 0.4.0": - version "0.3.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.3.1.tgz#6da9b4cff1bc5d716e6e5fe8e04fcb1b50a49adf" +cssstyle@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" dependencies: cssom "0.3.x" csstype@^2.2.0: - version "2.5.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7" + version "2.5.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.6.tgz#2ae1db2319642d8b80a668d2d025c6196071e788" currently-unhandled@^0.4.1: version "0.4.1" @@ -2833,7 +2822,11 @@ cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" -d3-array@1, d3-array@1.2.1, d3-array@^1.2.0: +d3-array@1, d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + +d3-array@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" @@ -2858,30 +2851,53 @@ d3-chord@1.0.4: d3-array "1" d3-path "1" -d3-collection@1, d3-collection@1.0.4: +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + +d3-collection@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2" d3-color@1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a" + version "1.2.3" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.3.tgz#6c67bb2af6df3cc8d79efcc4d3a3e83e28c8048f" d3-color@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" -d3-dispatch@1, d3-dispatch@1.0.3: +d3-dispatch@1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.5.tgz#e25c10a186517cd6c82dd19ea018f07e01e39015" + +d3-dispatch@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8" -d3-drag@1, d3-drag@1.2.1: +d3-drag@1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.3.tgz#46e206ad863ec465d88c588098a1df444cd33c64" + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-drag@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d" dependencies: d3-dispatch "1" d3-selection "1" -d3-dsv@1, d3-dsv@1.0.8: +d3-dsv@1: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.10.tgz#4371c489a2a654a297aca16fcaf605a6f31a6f51" + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-dsv@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae" dependencies: @@ -2889,7 +2905,11 @@ d3-dsv@1, d3-dsv@1.0.8: iconv-lite "0.4" rw "1" -d3-ease@1, d3-ease@1.0.3: +d3-ease@1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.5.tgz#8ce59276d81241b1b72042d6af2d40e76d936ffb" + +d3-ease@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" @@ -2903,8 +2923,8 @@ d3-force@1.1.0: d3-timer "1" d3-format@1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11" + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562" d3-format@1.2.2: version "1.2.2" @@ -2921,8 +2941,8 @@ d3-hierarchy@1.1.5: resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26" d3-interpolate@1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41" + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68" dependencies: d3-color "1" @@ -2932,7 +2952,11 @@ d3-interpolate@1.1.6: dependencies: d3-color "1" -d3-path@1, d3-path@1.0.5: +d3-path@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.7.tgz#8de7cd693a75ac0b5480d3abaccd94793e58aae8" + +d3-path@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764" @@ -2940,7 +2964,11 @@ d3-polygon@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62" -d3-quadtree@1, d3-quadtree@1.0.3: +d3-quadtree@1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.5.tgz#305394840b01f51a341a0da5008585e837fe7e9b" + +d3-quadtree@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438" @@ -2962,8 +2990,8 @@ d3-request@1.0.6: xmlhttprequest "1" d3-scale-chromatic@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.0.tgz#7ee38ffcaa7ad55cfed83a6a668aac5570c653c4" + version "1.3.3" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0" dependencies: d3-color "1" d3-interpolate "1" @@ -2980,7 +3008,11 @@ d3-scale@1.0.7: d3-time "1" d3-time-format "2" -d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0: +d3-selection@1, d3-selection@^1.1.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.2.tgz#6e70a9df60801c8af28ac24d10072d82cbfdf652" + +d3-selection@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" @@ -2990,21 +3022,46 @@ d3-shape@1.2.0: dependencies: d3-path "1" -d3-time-format@2, d3-time-format@2.1.1: +d3-time-format@2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b" + dependencies: + d3-time "1" + +d3-time-format@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31" dependencies: d3-time "1" -d3-time@1, d3-time@1.0.8: +d3-time@1: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.10.tgz#8259dd71288d72eeacfd8de281c4bf5c7393053c" + +d3-time@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84" -d3-timer@1, d3-timer@1.0.7: +d3-timer@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.9.tgz#f7bb8c0d597d792ff7131e1c24a36dd471a471ba" + +d3-timer@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531" -d3-transition@1, d3-transition@1.1.1: +d3-transition@1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.3.tgz#3a435b05ce9cef9524fe0d38121cfb6905331ca6" + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-transition@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039" dependencies: @@ -3081,12 +3138,12 @@ dashdash@^1.12.0: assert-plus "^1.0.0" data-urls@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.0.tgz#24802de4e81c298ea8a9388bb0d8e461c774684f" + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.1.tgz#d416ac3896918f29ca84d81085bc3705834da579" dependencies: - abab "^1.0.4" - whatwg-mimetype "^2.0.0" - whatwg-url "^6.4.0" + abab "^2.0.0" + whatwg-mimetype "^2.1.0" + whatwg-url "^7.0.0" date-fns@^1.27.2: version "1.29.0" @@ -3113,13 +3170,19 @@ 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" dependencies: ms "2.0.0" -debuglog@^1.0.1: +debug@^3.1.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.4.tgz#82123737c51afbe9609a2b5dfe9664e7487171f0" + dependencies: + ms "^2.1.1" + +debuglog@*, debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -3127,6 +3190,12 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decamelize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + dependencies: + xregexp "4.0.0" + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -3149,10 +3218,6 @@ deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" -deep-extend@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f" - deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -3167,6 +3232,13 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +default-gateway@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" + dependencies: + execa "^0.10.0" + ip-regex "^2.1.0" + default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" @@ -3180,11 +3252,10 @@ defaults@^1.0.3: clone "^1.0.2" define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" + object-keys "^1.0.12" define-property@^0.2.5: version "0.2.5" @@ -3290,8 +3361,8 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" detect-node@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" dezalgo@^1.0.0, dezalgo@~1.0.3: version "1.0.3" @@ -3301,8 +3372,8 @@ dezalgo@^1.0.0, dezalgo@~1.0.3: wrappy "1" diff-match-patch@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.1.tgz#d5f880213d82fbc124d2b95111fb3c033dbad7fa" + version "1.0.4" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" diff@3.3.1: version "3.3.1" @@ -3312,7 +3383,7 @@ diff@^2.0.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" -diff@^3.2.0, diff@^3.3.1, diff@^3.5.0: +diff@^3.2.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3369,7 +3440,7 @@ dom-align@^1.7.0: dom-converter@~0.1: version "0.1.4" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b" + resolved "http://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b" dependencies: utila "~0.3" @@ -3408,7 +3479,7 @@ domelementtype@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" -domexception@^1.0.0: +domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" dependencies: @@ -3486,15 +3557,16 @@ each-async@^1.0.0: onetime "^1.0.0" set-immediate-shim "^1.0.0" -eastasianwidth@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.1.1.tgz#44d656de9da415694467335365fb3147b8572b7c" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" dependencies: jsbn "~0.1.0" + safer-buffer "^2.1.0" editions@^1.3.3: version "1.3.4" @@ -3513,8 +3585,8 @@ ejs@^2.5.7, ejs@^2.5.9: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" electron-to-chromium@^1.2.7: - version "1.3.48" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900" + version "1.3.65" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.65.tgz#0655c238e45fea7e0e0e81fd0cac62b8186129c2" elegant-spinner@^1.0.1: version "1.0.1" @@ -3527,8 +3599,8 @@ element-resize-detector@^1.1.12: batch-processor "^1.0.0" elliptic@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + version "6.4.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -3542,19 +3614,19 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" -empower-core@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-0.6.2.tgz#5adef566088e31fba80ba0a36df47d7094169144" +empower-core@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-1.2.0.tgz#ce3fb2484d5187fa29c23fba8344b0b2fdf5601c" dependencies: call-signature "0.0.2" core-js "^2.0.0" -empower@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/empower/-/empower-1.2.3.tgz#6f0da73447f4edd838fec5c60313a88ba5cb852b" +empower@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/empower/-/empower-1.3.1.tgz#768979cbbb36d71d8f5edaab663deacb9dab916c" dependencies: core-js "^2.0.0" - empower-core "^0.6.2" + empower-core "^1.2.0" encodeurl@~1.0.2: version "1.0.2" @@ -3572,9 +3644,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a" +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" dependencies: graceful-fs "^4.1.2" memory-fs "^0.4.0" @@ -3588,52 +3660,55 @@ envinfo@^5.7.0: version "5.10.0" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-5.10.0.tgz#503a9774ae15b93ea68bdfae2ccd6306624ea6df" -enzyme-adapter-react-16@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4" +enzyme-adapter-react-16@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.5.0.tgz#50af8d76a45fe0915de932bd95d34cdca75c0be3" dependencies: - enzyme-adapter-utils "^1.3.0" - lodash "^4.17.4" - object.assign "^4.0.4" + enzyme-adapter-utils "^1.8.0" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" object.values "^1.0.4" - prop-types "^15.6.0" - react-reconciler "^0.7.0" + prop-types "^15.6.2" + react-is "^16.4.2" react-test-renderer "^16.0.0-0" -enzyme-adapter-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7" +enzyme-adapter-utils@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.8.0.tgz#ee9f07250663a985f1f2caaf297720787da559f1" dependencies: - lodash "^4.17.4" - object.assign "^4.0.4" - prop-types "^15.6.0" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" + prop-types "^15.6.2" -enzyme-to-json@^3.3.0: +enzyme-to-json@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.4.tgz#67c6040e931182f183418af2eb9f4323258aa77f" dependencies: lodash "^4.17.4" -enzyme@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479" +enzyme@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.6.0.tgz#d213f280a258f61e901bc663d4cc2d6fd9a9dec8" dependencies: + array.prototype.flat "^1.2.1" cheerio "^1.0.0-rc.2" - function.prototype.name "^1.0.3" - has "^1.0.1" + function.prototype.name "^1.1.0" + has "^1.0.3" is-boolean-object "^1.0.0" - is-callable "^1.1.3" + is-callable "^1.1.4" is-number-object "^1.0.3" is-string "^1.0.4" is-subset "^0.1.1" - lodash "^4.17.4" - object-inspect "^1.5.0" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.6.0" object-is "^1.0.1" object.assign "^4.1.0" object.entries "^1.0.4" object.values "^1.0.4" raf "^3.4.0" rst-selector-parser "^2.2.3" + string.prototype.trim "^1.1.2" err-code@^1.0.0: version "1.1.2" @@ -3646,8 +3721,8 @@ errno@^0.1.3, errno@~0.1.7: prr "~1.0.1" error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" dependencies: is-arrayish "^0.2.1" @@ -3658,7 +3733,7 @@ error@^7.0.2: string-template "~0.2.1" xtend "~4.0.0" -es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0: +es-abstract@^1.10.0, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.6.1: version "1.12.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" dependencies: @@ -3677,8 +3752,8 @@ es-to-primitive@^1.1.1: is-symbol "^1.0.1" es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.45" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.45.tgz#0bfdf7b473da5919d5adf3bd25ceb754fccc3653" + version "0.10.46" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" dependencies: es6-iterator "~2.0.3" es6-symbol "~3.1.1" @@ -3705,11 +3780,11 @@ es6-map@^0.1.3: es6-promise@^3.0.2: version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + resolved "http://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" es6-promise@^4.0.3: - version "4.2.4" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" es6-promisify@^5.0.0: version "5.0.0" @@ -3762,9 +3837,9 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escodegen@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2" +escodegen@^1.9.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -3782,16 +3857,16 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" +eslint-scope@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" eslint@^2.7.0: version "2.13.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11" + resolved "http://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11" dependencies: chalk "^1.1.3" concat-stream "^1.4.6" @@ -3843,12 +3918,12 @@ esprima@^3.1.3, esprima@~3.1.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" esprima@^4.0.0, esprima@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" espurify@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.8.0.tgz#270d8046e4e47e923d75bc8a87357c7112ca8485" + version "1.8.1" + resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.8.1.tgz#5746c6c1ab42d302de10bd1d5bf7f0e8c0515056" dependencies: core-js "^2.0.0" @@ -3883,7 +3958,7 @@ event-emitter@~0.3.5: eventemitter2@~0.4.13: version "0.4.14" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" + resolved "http://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" eventemitter3@^2.0.3: version "2.0.3" @@ -3911,10 +3986,22 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: safe-buffer "^5.1.1" exec-sh@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" + version "0.2.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" dependencies: - merge "^1.1.3" + merge "^1.2.0" + +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + dependencies: + cross-spawn "^6.0.0" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" execa@^0.7.0: version "0.7.0" @@ -4003,7 +4090,7 @@ expose-loader@^0.7.3: express@^4.16.2: version "4.16.3" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" + resolved "http://registry.npmjs.org/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" dependencies: accepts "~1.3.5" array-flatten "1.1.1" @@ -4049,18 +4136,26 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.0, extend@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" +extend@~3.0.1, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" external-editor@^2.1.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + resolved "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" dependencies: chardet "^0.4.0" iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -4146,27 +4241,15 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.16: - version "0.8.16" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.9" - fd-slicer@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" dependencies: pend "~1.2.0" -figgy-pudding@^3.0.0, figgy-pudding@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.1.0.tgz#a77ed2284175976c424b390b298569e9df86dd1e" +figgy-pudding@^3.0.0, figgy-pudding@^3.1.0, figgy-pudding@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" figures@^1.0.1, figures@^1.3.5, figures@^1.7.0: version "1.7.0" @@ -4278,6 +4361,12 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + dependencies: + locate-path "^3.0.0" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -4304,8 +4393,8 @@ flatten@^1.0.2: resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" flow-parser@^0.*: - version "0.74.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.74.0.tgz#4acc8f55bdce5fa4da43c72c28ef8a9600ace87c" + version "0.80.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.80.0.tgz#90704d27eca33eb8c8454c61df76f08f498aaae6" flush-write-stream@^1.0.0: version "1.0.3" @@ -4315,8 +4404,8 @@ flush-write-stream@^1.0.0: readable-stream "^2.0.4" follow-redirects@^1.0.0, follow-redirects@^1.2.5: - version "1.5.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77" + version "1.5.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.7.tgz#a39e4804dacb90202bca76a9e2ac10433ca6a69a" dependencies: debug "^3.1.0" @@ -4340,10 +4429,6 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -4363,15 +4448,7 @@ fork-ts-checker-webpack-plugin@^0.4.9: resolve "^1.5.0" tapable "^1.0.0" -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.3.1: +form-data@~2.3.1, form-data@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" dependencies: @@ -4474,7 +4551,7 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.1.2, fsevents@^1.2.2, fsevents@^1.2.3: +fsevents@^1.2.2, fsevents@^1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" dependencies: @@ -4490,11 +4567,11 @@ fstream@^1.0.0, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.1.0, function-bind@^1.1.1: +function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" -function.prototype.name@^1.0.3: +function.prototype.name@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327" dependencies: @@ -4522,8 +4599,10 @@ gaze@^1.0.0, gaze@^1.1.2: globule "^1.0.0" generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + dependencies: + is-property "^1.0.2" generate-object-property@^1.1.0: version "1.2.0" @@ -4549,8 +4628,8 @@ gentle-fs@^2.0.0, gentle-fs@^2.0.1: slide "^1.1.6" get-caller-file@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" get-document@1: version "1.0.0" @@ -4636,7 +4715,7 @@ glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" -glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: +glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -4647,13 +4726,14 @@ glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glo once "^1.3.0" path-is-absolute "^1.0.0" -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" dependencies: + fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "2 || 3" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -4734,7 +4814,18 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^8.0.0, globby@^8.0.1: +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globby@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" dependencies: @@ -4768,7 +4859,7 @@ good-listener@^1.2.2: got@^6.7.1: version "6.7.1" - resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + resolved "http://registry.npmjs.org/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" dependencies: create-error-class "^3.0.0" duplexer3 "^0.1.4" @@ -4802,8 +4893,8 @@ got@^7.0.0: url-to-options "^1.0.1" got@^8.3.1: - version "8.3.1" - resolved "https://registry.yarnpkg.com/got/-/got-8.3.1.tgz#093324403d4d955f5a16a7a8d39955d055ae10ed" + version "8.3.2" + resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" dependencies: "@sindresorhus/is" "^0.7.0" cacheable-request "^2.1.1" @@ -4862,7 +4953,7 @@ grunt-cli@~1.2.0: grunt-contrib-clean@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/grunt-contrib-clean/-/grunt-contrib-clean-1.0.0.tgz#6b2ed94117e2c7ffe32ee04578c96fe4625a9b6d" + resolved "http://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.0.0.tgz#6b2ed94117e2c7ffe32ee04578c96fe4625a9b6d" dependencies: async "^1.5.2" rimraf "^2.5.1" @@ -4895,7 +4986,7 @@ grunt-contrib-copy@~1.0.0: grunt-contrib-cssmin@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/grunt-contrib-cssmin/-/grunt-contrib-cssmin-1.0.2.tgz#1734cbd3d84ca7364758b7e58ff18e52aa60bb76" + resolved "http://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-1.0.2.tgz#1734cbd3d84ca7364758b7e58ff18e52aa60bb76" dependencies: chalk "^1.0.0" clean-css "~3.4.2" @@ -4906,8 +4997,8 @@ grunt-exec@^1.0.1: resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4" grunt-known-options@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.0.tgz#a4274eeb32fa765da5a7a3b1712617ce3b144149" + version "1.1.1" + resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.1.tgz#6cc088107bd0219dc5d3e57d91923f469059804d" grunt-legacy-log-utils@~1.0.0: version "1.0.0" @@ -4977,8 +5068,8 @@ grunt-usemin@3.1.1: path-exists "^1.0.0" grunt-webpack@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/grunt-webpack/-/grunt-webpack-3.1.2.tgz#5649a2c9884cb99e706309268067125833a1ec5c" + version "3.1.3" + resolved "https://registry.yarnpkg.com/grunt-webpack/-/grunt-webpack-3.1.3.tgz#7e0a016773b105bb87718c19f308100b498ce39a" dependencies: deep-for-each "^2.0.2" lodash "^4.7.0" @@ -5023,28 +5114,19 @@ handle-thing@^1.2.5: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" handlebars@^4.0.3: - version "4.0.11" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" + version "4.0.12" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" dependencies: - async "^1.4.0" + async "^2.5.0" optimist "^0.6.1" - source-map "^0.4.4" + source-map "^0.6.1" optionalDependencies: - uglify-js "^2.6" + uglify-js "^3.1.4" har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - har-validator@~5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" @@ -5052,6 +5134,13 @@ har-validator@~5.0.3: ajv "^5.1.0" har-schema "^2.0.0" +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -5119,7 +5208,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.1: +has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" dependencies: @@ -5133,11 +5222,11 @@ hash-base@^3.0.0: safe-buffer "^5.0.1" hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + version "1.1.5" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812" dependencies: inherits "^2.0.3" - minimalistic-assert "^1.0.0" + minimalistic-assert "^1.0.1" hasha@^2.2.0: version "2.2.0" @@ -5146,15 +5235,6 @@ hasha@^2.2.0: is-stream "^1.0.1" pinkie-promise "^2.0.0" -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - he@1.1.1, he@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -5178,13 +5258,9 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40" + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" home-or-tmp@^2.0.0: version "2.0.0" @@ -5204,8 +5280,8 @@ hooker@~0.2.3: resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" + version "2.7.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" hpack.js@^2.1.6: version "2.1.6" @@ -5241,16 +5317,16 @@ html-loader@^0.5.1: object-assign "^4.1.1" html-minifier@^3.2.3, html-minifier@^3.5.8: - version "3.5.16" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.16.tgz#39f5aabaf78bdfc057fe67334226efd7f3851175" + version "3.5.20" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.20.tgz#7b19fd3caa0cb79f7cde5ee5c3abdf8ecaa6bb14" dependencies: camel-case "3.0.x" - clean-css "4.1.x" - commander "2.15.x" + clean-css "4.2.x" + commander "2.17.x" he "1.1.x" param-case "2.1.x" relateurl "0.2.x" - uglify-js "3.3.x" + uglify-js "3.4.x" html-minifier@~2.1.2: version "2.1.7" @@ -5273,7 +5349,7 @@ html-webpack-harddisk-plugin@^0.2.0: html-webpack-plugin@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" + resolved "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" dependencies: html-minifier "^3.2.3" loader-utils "^0.2.16" @@ -5322,7 +5398,7 @@ http-errors@1.6.2: http-errors@~1.6.2: version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + resolved "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" dependencies: depd "~1.1.2" inherits "2.0.3" @@ -5342,7 +5418,7 @@ http-proxy-agent@^2.0.0, http-proxy-agent@^2.1.0: http-proxy-middleware@~0.18.0: version "0.18.0" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" + resolved "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" dependencies: http-proxy "^1.16.2" is-glob "^4.0.0" @@ -5357,14 +5433,6 @@ http-proxy@^1.16.2: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -5398,9 +5466,9 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" -iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" +iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" dependencies: safer-buffer ">= 2.1.2 < 3" @@ -5408,6 +5476,12 @@ iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -5418,9 +5492,9 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" -ieee754@^1.1.11, ieee754@^1.1.4: - version "1.1.11" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" +ieee754@^1.1.4: + version "1.1.12" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" iferr@^0.1.5, iferr@~0.1.5: version "0.1.5" @@ -5433,8 +5507,8 @@ ignore-walk@^3.0.1: minimatch "^3.0.4" ignore@^3.1.2, ignore@^3.3.5: - version "3.3.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" iltorb@^1.0.13: version "1.3.10" @@ -5449,6 +5523,18 @@ immutable@^3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + dependencies: + import-from "^2.1.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + dependencies: + resolve-from "^3.0.0" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -5460,7 +5546,14 @@ import-local@^1.0.0: pkg-dir "^2.0.0" resolve-cwd "^2.0.0" -imurmurhash@^0.1.4: +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@*, imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -5554,11 +5647,30 @@ inquirer@^5.2.0: strip-ansi "^4.0.0" through "^2.3.6" -internal-ip@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" +inquirer@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" dependencies: - meow "^3.3.0" + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.17.10" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +internal-ip@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" + dependencies: + default-gateway "^2.6.0" + ipaddr.js "^1.5.2" interpret@^1.0.0, interpret@^1.1.0: version "1.1.0" @@ -5581,13 +5693,25 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + ip@^1.1.0, ip@^1.1.4, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" -ipaddr.js@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" +ipaddr.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + +ipaddr.js@^1.5.2: + version "1.8.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.1.tgz#fa4b79fa47fd3def5e3b159825161c0a519c9427" is-absolute-url@^2.0.0: version "2.1.0" @@ -5629,15 +5753,15 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-callable@^1.1.1, is-callable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" +is-callable@^1.1.1, is-callable@^1.1.3, is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" is-ci@^1.0.10: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" dependencies: - ci-info "^1.0.0" + ci-info "^1.5.0" is-cidr@~1.0.0: version "1.0.0" @@ -5752,8 +5876,8 @@ is-glob@^4.0.0: is-extglob "^2.1.1" is-hotkey@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.2.tgz#aeda5e4f542284700ae18b46980fb0637c021198" + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.3.tgz#8a129eec16f3941bd4f37191e02b9c3e91950549" is-in-browser@^1.1.3: version "1.1.3" @@ -5776,9 +5900,9 @@ is-my-ip-valid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" -is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: - version "2.17.2" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c" +is-my-json-valid@^2.10.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175" dependencies: generate-function "^2.0.0" generate-object-property "^1.1.0" @@ -5830,12 +5954,6 @@ is-observable@^1.1.0: dependencies: symbol-observable "^1.1.0" -is-odd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" - dependencies: - is-number "^4.0.0" - is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" @@ -5874,7 +5992,7 @@ is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" -is-property@^1.0.0: +is-property@^1.0.0, is-property@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -5963,8 +6081,10 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" isbinaryfile@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" + version "3.0.3" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80" + dependencies: + buffer-alloc "^1.2.0" isexe@^2.0.0: version "2.0.0" @@ -5984,13 +6104,6 @@ isomorphic-base64@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803" -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -6011,11 +6124,7 @@ istanbul-api@^1.3.1: mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341" - -istanbul-lib-coverage@^1.2.1: +istanbul-lib-coverage@^1.2.0, istanbul-lib-coverage@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" @@ -6025,19 +6134,7 @@ istanbul-lib-hook@^1.2.2: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b" - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.18.0" - istanbul-lib-coverage "^1.2.0" - semver "^5.3.0" - -istanbul-lib-instrument@^1.10.2: +istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" dependencies: @@ -6058,17 +6155,7 @@ istanbul-lib-report@^1.1.5: path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.2.4: - version "1.2.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz#ffe6be4e7ab86d3603e4290d54990b14506fc9b1" - dependencies: - debug "^3.1.0" - istanbul-lib-coverage "^1.2.0" - mkdirp "^0.5.1" - rimraf "^2.6.1" - source-map "^0.5.3" - -istanbul-lib-source-maps@^1.2.6: +istanbul-lib-source-maps@^1.2.4, istanbul-lib-source-maps@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" dependencies: @@ -6407,14 +6494,18 @@ jquery@^3.2.1: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" js-base64@^2.1.8, js-base64@^2.1.9: - version "2.4.5" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92" + version "2.4.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" -js-tokens@^3.0.0, js-tokens@^3.0.2: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + +js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: +js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: @@ -6480,34 +6571,34 @@ jscodeshift@^0.5.0: write-file-atomic "^1.2.0" jsdom@^11.5.1: - version "11.11.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.11.0.tgz#df486efad41aee96c59ad7a190e2449c7eb1110e" + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" dependencies: - abab "^1.0.4" - acorn "^5.3.0" + abab "^2.0.0" + acorn "^5.5.3" acorn-globals "^4.1.0" array-equal "^1.0.0" cssom ">= 0.3.2 < 0.4.0" - cssstyle ">= 0.3.1 < 0.4.0" + cssstyle "^1.0.0" data-urls "^1.0.0" - domexception "^1.0.0" - escodegen "^1.9.0" + domexception "^1.0.1" + escodegen "^1.9.1" html-encoding-sniffer "^1.0.2" - left-pad "^1.2.0" - nwsapi "^2.0.0" + left-pad "^1.3.0" + nwsapi "^2.0.7" parse5 "4.0.0" pn "^1.1.0" - request "^2.83.0" + request "^2.87.0" request-promise-native "^1.0.5" sax "^1.2.4" symbol-tree "^3.2.2" - tough-cookie "^2.3.3" + tough-cookie "^2.3.4" w3c-hr-time "^1.0.1" webidl-conversions "^4.0.2" whatwg-encoding "^1.0.3" whatwg-mimetype "^2.1.0" whatwg-url "^6.4.1" - ws "^4.0.0" + ws "^5.2.0" xml-name-validator "^3.0.0" jsesc@^0.5.0, jsesc@~0.5.0: @@ -6530,6 +6621,10 @@ json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -6554,7 +6649,7 @@ json5@^0.5.0, json5@^0.5.1: jsonfile@^2.1.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + resolved "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" optionalDependencies: graceful-fs "^4.1.6" @@ -6606,8 +6701,8 @@ keyv@3.0.0: json-buffer "3.0.0" killable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" @@ -6676,11 +6771,13 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -leb@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/leb/-/leb-0.3.0.tgz#32bee9fad168328d6aea8522d833f4180eed1da3" +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + dependencies: + invert-kv "^2.0.0" -left-pad@^1.2.0: +left-pad@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" @@ -6728,7 +6825,7 @@ libnpx@^10.2.0: lint-staged@^6.0.0: version "6.1.1" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.1.1.tgz#cd08c4d9b8ccc2d37198d1c47ce77d22be6cf324" + resolved "http://registry.npmjs.org/lint-staged/-/lint-staged-6.1.1.tgz#cd08c4d9b8ccc2d37198d1c47ce77d22be6cf324" dependencies: app-root-path "^2.0.0" chalk "^2.1.0" @@ -6800,25 +6897,18 @@ listr@^0.13.0: strip-ansi "^3.0.1" listr@^0.14.1: - version "0.14.1" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.1.tgz#8a7afa4a7135cee4c921d128e0b7dfc6e522d43d" + version "0.14.2" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.2.tgz#cbe44b021100a15376addfc2d79349ee430bfe14" dependencies: "@samverschueren/stream-to-observable" "^0.3.0" - cli-truncate "^0.2.1" - figures "^1.7.0" - indent-string "^2.1.0" is-observable "^1.1.0" is-promise "^2.1.0" is-stream "^1.1.0" listr-silent-renderer "^1.1.1" listr-update-renderer "^0.4.0" listr-verbose-renderer "^0.4.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - ora "^0.2.3" p-map "^1.1.1" rxjs "^6.1.0" - strip-ansi "^3.0.1" load-grunt-tasks@3.5.2: version "3.5.2" @@ -6876,6 +6966,13 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + dependencies: + p-locate "^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" @@ -6893,6 +6990,10 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" +lodash._baseindexof@*: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -6900,11 +7001,25 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" +lodash._bindcallback@*: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._cacheindexof@*: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" + +lodash._createcache@*: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" + dependencies: + lodash._getnative "^3.0.0" + lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@^3.0.0: +lodash._getnative@*, lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -6936,6 +7051,10 @@ lodash.endswith@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09" +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -6948,7 +7067,7 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" -lodash.isequal@^4.0.0: +lodash.isequal@^4.0.0, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -6984,6 +7103,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.restparam@*: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -7014,7 +7137,7 @@ lodash.without@~4.4.0: lodash@^3.10.1, lodash@^3.6.0: version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + resolved "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" lodash@^4.0.0, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5: version "4.17.10" @@ -7022,7 +7145,7 @@ lodash@^4.0.0, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, l lodash@~4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4" + resolved "http://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4" log-symbols@^1.0.2: version "1.0.2" @@ -7030,7 +7153,7 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" -log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: +log-symbols@^2.0.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" dependencies: @@ -7047,30 +7170,19 @@ loglevel@^1.4.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" -loglevelnext@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.5.tgz#36fc4f5996d6640f539ff203ba819641680d75a2" - dependencies: - es6-symbol "^3.1.1" - object.assign "^4.1.0" - lolex@1.3.2: version "1.3.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" - -long@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + resolved "http://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" dependencies: - js-tokens "^3.0.0" + js-tokens "^3.0.0 || ^4.0.0" loud-rejection@^1.0.0, loud-rejection@^1.6.0: version "1.6.0" @@ -7110,7 +7222,23 @@ make-dir@^1.0.0, make-dir@^1.1.0: dependencies: pify "^3.0.0" -make-fetch-happen@^2.5.0, make-fetch-happen@^2.6.0: +"make-fetch-happen@^2.5.0 || 3 || 4", make-fetch-happen@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz#141497cb878f243ba93136c83d8aba12c216c083" + dependencies: + agentkeepalive "^3.4.1" + cacache "^11.0.1" + http-cache-semantics "^3.8.1" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + lru-cache "^4.1.2" + mississippi "^3.0.0" + node-fetch-npm "^2.0.2" + promise-retry "^1.1.1" + socks-proxy-agent "^4.0.0" + ssri "^6.0.0" + +make-fetch-happen@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38" dependencies: @@ -7142,22 +7270,6 @@ make-fetch-happen@^3.0.0: socks-proxy-agent "^3.0.1" ssri "^5.2.4" -make-fetch-happen@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz#141497cb878f243ba93136c83d8aba12c216c083" - dependencies: - agentkeepalive "^3.4.1" - cacache "^11.0.1" - http-cache-semantics "^3.8.1" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - lru-cache "^4.1.2" - mississippi "^3.0.0" - node-fetch-npm "^2.0.2" - promise-retry "^1.1.1" - socks-proxy-agent "^4.0.0" - ssri "^6.0.0" - makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -7168,6 +7280,12 @@ mamacro@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" +map-age-cleaner@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74" + dependencies: + p-defer "^1.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -7215,14 +7333,14 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" mem-fs-editor@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.2.tgz#55a79b1e824da631254c4c95ba6366602c77af90" + version "4.0.3" + resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.3.tgz#d282a0c4e0d796e9eff9d75661f25f68f389af53" dependencies: commondir "^1.0.1" - deep-extend "^0.5.1" + deep-extend "^0.6.0" ejs "^2.5.9" glob "^7.0.3" - globby "^8.0.0" + globby "^7.1.1" isbinaryfile "^3.0.2" mkdirp "^0.5.0" multimatch "^2.0.0" @@ -7244,6 +7362,14 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +mem@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf" + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^1.0.0" + p-is-promise "^1.1.0" + memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -7280,7 +7406,7 @@ merge2@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34" -merge@^1.1.3, merge@^1.2.0: +merge@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" @@ -7331,25 +7457,21 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.34.0 < 2": - version "1.34.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.34.0.tgz#452d0ecff5c30346a6dc1e64b1eaee0d3719ff9a" +"mime-db@>= 1.34.0 < 2", mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: + version "2.1.20" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" dependencies: - mime-db "~1.33.0" + mime-db "~1.36.0" mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" -mime@^2.1.0: +mime@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" @@ -7358,8 +7480,8 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" mimic-response@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" min-document@^2.19.0: version "2.19.0" @@ -7368,13 +7490,14 @@ min-document@^2.19.0: dom-walk "^0.1.0" mini-css-extract-plugin@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.0.tgz#ff3bf08bee96e618e177c16ca6131bfecef707f9" + version "0.4.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.2.tgz#b3ecc0d6b1bbe5ff14add42b946a7b200cf78651" dependencies: loader-utils "^1.1.0" + schema-utils "^1.0.0" webpack-sources "^1.1.0" -minimalistic-assert@^1.0.0: +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -7396,27 +7519,27 @@ minimatch@3.0.3: minimist@0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" minimist@1.1.x: version "1.1.3" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + resolved "http://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" minimist@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" minimist@~0.0.1: version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" minipass@^2.2.1, minipass@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" + version "2.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" @@ -7488,7 +7611,7 @@ mixin-object@^2.0.1: mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" @@ -7498,13 +7621,13 @@ mobx-react-devtools@^4.2.15: mobx-react@^4.3.5: version "4.4.3" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.4.3.tgz#baa9ec41165ee35ae7b9df19bca10190f36f117e" + resolved "http://registry.npmjs.org/mobx-react/-/mobx-react-4.4.3.tgz#baa9ec41165ee35ae7b9df19bca10190f36f117e" dependencies: hoist-non-react-statics "^2.3.1" mobx-state-tree@^1.3.1: version "1.4.0" - resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-1.4.0.tgz#c914c855d5ec5c1c16e4ba6d6925679df42c8110" + resolved "http://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-1.4.0.tgz#c914c855d5ec5c1c16e4ba6d6925679df42c8110" mobx@^3.4.1: version "3.6.2" @@ -7529,6 +7652,10 @@ moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" +moo@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" + mousetrap-global-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz#cd7de9222bd0646fa2e010d54c84a74c26a88edd" @@ -7552,7 +7679,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" @@ -7585,19 +7712,18 @@ mute-stream@0.0.7, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" nan@^2.10.0, nan@^2.6.2, nan@^2.9.2: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + version "2.11.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" nanomatch@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" define-property "^2.0.2" extend-shallow "^3.0.2" fragment-cache "^0.2.1" - is-odd "^2.0.0" is-windows "^1.0.2" kind-of "^6.0.2" object.pick "^1.3.0" @@ -7616,17 +7742,18 @@ ncname@1.0.x: xml-char-classes "^1.0.0" nearley@^2.7.10: - version "2.13.0" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439" + version "2.15.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.15.1.tgz#965e4e6ec9ed6b80fc81453e161efbcebb36d247" dependencies: + moo "^0.4.3" nomnom "~1.6.2" railroad-diagrams "^1.0.0" randexp "0.4.6" semver "^5.4.1" -needle@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" +needle@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.3.tgz#c1b04da378cd634d8befe2de965dc2cfb0fd65ca" dependencies: debug "^2.1.2" iconv-lite "^0.4.4" @@ -7637,8 +7764,8 @@ negotiator@0.6.1: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" neo-async@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee" + version "2.5.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.2.tgz#489105ce7bc54e709d736b195f82135048c50fcc" next-tick@1: version "1.0.0" @@ -7703,8 +7830,8 @@ ngtemplate-loader@^2.0.1: loader-utils "^1.0.2" nice-try@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" no-case@^2.2.0, no-case@^2.3.2: version "2.3.2" @@ -7713,8 +7840,8 @@ no-case@^2.2.0, no-case@^2.3.2: lower-case "^1.1.1" node-abi@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.1.tgz#7628c4d4ec4e9cd3764ceb3652f36b2e7f8d4923" + version "2.4.3" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.3.tgz#43666b7b17e57863e572409edbb82115ac7af28b" dependencies: semver "^5.4.1" @@ -7730,30 +7857,22 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - node-forge@0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" -node-gyp@^3.3.1, node-gyp@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" +node-gyp@^3.6.2, node-gyp@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" dependencies: fstream "^1.0.0" glob "^7.0.3" graceful-fs "^4.1.2" - minimatch "^3.0.2" mkdirp "^0.5.0" nopt "2 || 3" npmlog "0 || 1 || 2 || 3 || 4" osenv "0" - request "2" + request "^2.87.0" rimraf "2" semver "~5.3.0" tar "^2.0.0" @@ -7801,23 +7920,23 @@ node-notifier@^5.2.1: which "^1.3.0" node-pre-gyp@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46" + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" - needle "^2.2.0" + needle "^2.2.1" nopt "^4.0.1" npm-packlist "^1.1.6" npmlog "^4.0.2" - rc "^1.1.7" + rc "^1.2.7" rimraf "^2.6.1" semver "^5.3.0" tar "^4" node-sass@^4.7.2: - version "4.9.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.0.tgz#d1b8aa855d98ed684d6848db929a20771cc2ae52" + version "4.9.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7832,9 +7951,9 @@ node-sass@^4.7.2: meow "^3.7.0" mkdirp "^0.5.1" nan "^2.10.0" - node-gyp "^3.3.1" + node-gyp "^3.8.0" npmlog "^4.0.0" - request "~2.79.0" + request "2.87.0" sass-graph "^2.2.4" stdout-stream "^1.4.0" "true-case-path" "^1.0.2" @@ -7915,15 +8034,15 @@ normalize-url@^1.4.0: sort-keys "^1.0.0" npm-audit-report@^1.0.9: - version "1.2.1" - resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.2.1.tgz#14813e9551f0f33088e7acc442e83ea6d627ef13" + version "1.3.1" + resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.3.1.tgz#e79ea1fcb5ffaf3031102b389d5222c2b0459632" dependencies: - cli-table2 "^0.2.0" + cli-table3 "^0.5.0" console-control-strings "^1.1.0" npm-bundled@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" + version "1.0.5" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" npm-cache-filename@~1.0.2: version "1.0.2" @@ -7936,17 +8055,17 @@ npm-install-checks@~3.0.0: semver "^2.3.0 || 3.x || 4 || 5" npm-lifecycle@^2.0.1, npm-lifecycle@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.0.3.tgz#696bedf1143371163e9cc16fe872357e25d8d90e" + version "2.1.0" + resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.1.0.tgz#1eda2eedb82db929e3a0c50341ab0aad140ed569" dependencies: byline "^5.0.0" graceful-fs "^4.1.11" - node-gyp "^3.6.2" + node-gyp "^3.8.0" resolve-from "^4.0.0" slide "^1.1.6" uid-number "0.0.6" umask "^1.1.0" - which "^1.3.0" + which "^1.3.1" npm-logical-tree@^1.2.1: version "1.2.1" @@ -7962,8 +8081,8 @@ npm-logical-tree@^1.2.1: validate-npm-package-name "^3.0.0" npm-packlist@^1.1.10, npm-packlist@^1.1.6, npm-packlist@~1.1.10: - version "1.1.10" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" + version "1.1.11" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -7982,15 +8101,15 @@ npm-pick-manifest@^2.1.0: semver "^5.4.1" npm-profile@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.1.tgz#65a1018340f14399a086b5d0a9bd0d13145d8e57" + version "3.0.2" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.2.tgz#58d568f1b56ef769602fd0aed8c43fa0e0de0f57" dependencies: - aproba "^1.1.2" - make-fetch-happen "^2.5.0" + aproba "^1.1.2 || 2" + make-fetch-happen "^2.5.0 || 3 || 4" npm-registry-client@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.5.1.tgz#8115809c0a4b40938b8a109b8ea74d26c6f5d7f1" + version "8.6.0" + resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.6.0.tgz#7f1529f91450732e89f8518e0f21459deea3e4c4" dependencies: concat-stream "^1.5.2" graceful-fs "^4.1.6" @@ -8170,14 +8289,18 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -nwsapi@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.2.tgz#33a0aab27c678d4dfdbba6a7f84b1c627fc4966f" +nwsapi@^2.0.7: + version "2.0.9" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016" -oauth-sign@~0.8.1, oauth-sign@~0.8.2: +oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -8190,7 +8313,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.5.0: +object-inspect@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" @@ -8198,9 +8321,9 @@ object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" -object-keys@^1.0.0, object-keys@^1.0.11, object-keys@^1.0.8: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object-keys@^1.0.0, object-keys@^1.0.11, object-keys@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" object-visit@^1.0.0: version "1.0.1" @@ -8208,7 +8331,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.0.4, object.assign@^4.1.0: +object.assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" dependencies: @@ -8285,7 +8408,11 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -opener@^1.4.3, opener@~1.4.3: +opener@^1.4.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" + +opener@~1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" @@ -8303,8 +8430,8 @@ optimist@^0.6.1, optimist@~0.6.1: wordwrap "~0.0.2" optimize-css-assets-webpack-plugin@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-4.0.2.tgz#813d511d20fe5d9a605458441ed97074d79c1122" + version "4.0.3" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-4.0.3.tgz#4f714e276b279700892c4a6202b7e22812d6f683" dependencies: cssnano "^3.10.0" last-call-webpack-plugin "^3.0.0" @@ -8322,7 +8449,7 @@ optionator@^0.8.1: ora@^0.2.3: version "0.2.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + resolved "http://registry.npmjs.org/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" dependencies: chalk "^1.1.1" cli-cursor "^1.0.2" @@ -8340,10 +8467,10 @@ ordered-esprima-props@~1.1.0: resolved "https://registry.yarnpkg.com/ordered-esprima-props/-/ordered-esprima-props-1.1.0.tgz#a9827086df5f010aa60e9bd02b6e0335cea2ffcb" original@>=0.0.5: - version "1.0.1" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.1.tgz#b0a53ff42ba997a8c9cd1fb5daaeb42b9d693190" + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" dependencies: - url-parse "~1.4.0" + url-parse "^1.4.3" os-browserify@^0.3.0: version "0.3.0" @@ -8355,7 +8482,7 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: os-locale@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + resolved "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" dependencies: lcid "^1.0.0" @@ -8367,6 +8494,14 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" +os-locale@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" + dependencies: + execa "^0.10.0" + lcid "^2.0.0" + mem "^4.0.0" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -8384,7 +8519,11 @@ p-cancelable@^0.3.0: p-cancelable@^0.4.0: version "0.4.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + resolved "http://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" p-each-series@^1.0.0: version "1.0.0" @@ -8410,12 +8549,24 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" +p-limit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" dependencies: p-limit "^1.1.0" +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + dependencies: + p-limit "^2.0.0" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -8440,6 +8591,10 @@ p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" +p-try@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -8632,8 +8787,8 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" path-to-regexp@0.1.7: version "0.1.7" @@ -8709,6 +8864,12 @@ pkg-dir@^2.0.0: dependencies: find-up "^2.1.0" +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + dependencies: + find-up "^3.0.0" + pkg-up@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26" @@ -8724,12 +8885,12 @@ pn@^1.1.0: resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" popper.js@^1.12.5: - version "1.14.3" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" + version "1.14.4" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6" portfinder@^1.0.9: - version "1.0.13" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" + version "1.0.17" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a" dependencies: async "^1.5.2" debug "^2.2.0" @@ -8805,36 +8966,20 @@ postcss-filter-plugins@^2.0.0: dependencies: postcss "^5.0.4" -postcss-load-config@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" +postcss-load-config@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.0.0.tgz#f1312ddbf5912cd747177083c5ef7a19d62ee484" dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - postcss-load-options "^1.2.0" - postcss-load-plugins "^2.3.0" - -postcss-load-options@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - -postcss-load-plugins@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" - dependencies: - cosmiconfig "^2.1.1" - object-assign "^4.1.0" + cosmiconfig "^4.0.0" + import-cwd "^2.0.0" postcss-loader@^2.0.6: - version "2.1.5" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.5.tgz#3c6336ee641c8f95138172533ae461a83595e788" + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.6.tgz#1d7dd7b17c6ba234b9bed5af13e0bea40a42d740" dependencies: loader-utils "^1.1.0" postcss "^6.0.0" - postcss-load-config "^1.2.0" + postcss-load-config "^2.0.0" schema-utils "^0.4.0" postcss-merge-idents@^2.1.5: @@ -9024,38 +9169,38 @@ postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0. supports-color "^3.2.3" postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.8: - version "6.0.22" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3" + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" dependencies: chalk "^2.4.1" source-map "^0.6.1" supports-color "^5.4.0" power-assert-context-formatter@^1.0.7: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz#edba352d3ed8a603114d667265acce60d689ccdf" + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.2.0.tgz#8fbe72692288ec5a7203cdf215c8b838a6061d2a" dependencies: core-js "^2.0.0" - power-assert-context-traversal "^1.1.1" + power-assert-context-traversal "^1.2.0" power-assert-context-reducer-ast@^1.0.7: - version "1.1.2" - resolved "https://registry.yarnpkg.com/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.1.2.tgz#484a99e26f4973ff8832e5c5cc756702e6094174" + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.2.0.tgz#c7ca1c9e39a6fb717f7ac5fe9e76e192bf525df3" dependencies: - acorn "^4.0.0" + acorn "^5.0.0" acorn-es7-plugin "^1.0.12" core-js "^2.0.0" espurify "^1.6.0" estraverse "^4.2.0" -power-assert-context-traversal@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz#88cabca0d13b6359f07d3d3e8afa699264577ed9" +power-assert-context-traversal@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-context-traversal/-/power-assert-context-traversal-1.2.0.tgz#f6e71454baf640de5c1c9c270349f5c9ab0b2e94" dependencies: core-js "^2.0.0" estraverse "^4.1.0" -power-assert-formatter@^1.3.1: +power-assert-formatter@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz#5dc125ed50a3dfb1dda26c19347f3bf58ec2884a" dependencies: @@ -9068,19 +9213,19 @@ power-assert-formatter@^1.3.1: power-assert-renderer-file "^1.0.7" power-assert-renderer-assertion@^1.0.7: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz#cbfc0e77e0086a8f96af3f1d8e67b9ee7e28ce98" + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.2.0.tgz#3db6ffcda106b37bc1e06432ad0d748a682b147a" dependencies: power-assert-renderer-base "^1.1.1" - power-assert-util-string-width "^1.1.1" + power-assert-util-string-width "^1.2.0" power-assert-renderer-base@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz#96a650c6fd05ee1bc1f66b54ad61442c8b3f63eb" power-assert-renderer-comparison@^1.0.7: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.1.1.tgz#d7439d97d85156be4e30a00f2fb5a72514ce3c08" + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.2.0.tgz#e4f88113225a69be8aa586ead05aef99462c0495" dependencies: core-js "^2.0.0" diff-match-patch "^1.0.0" @@ -9089,33 +9234,33 @@ power-assert-renderer-comparison@^1.0.7: type-name "^2.0.1" power-assert-renderer-diagram@^1.0.7: - version "1.1.2" - resolved "https://registry.yarnpkg.com/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz#655f8f711935a9b6d541b86327654717c637a986" + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.2.0.tgz#37f66e8542e5677c5b58e6d72b01c0d9a30e2219" dependencies: core-js "^2.0.0" power-assert-renderer-base "^1.1.1" - power-assert-util-string-width "^1.1.1" + power-assert-util-string-width "^1.2.0" stringifier "^1.3.0" power-assert-renderer-file@^1.0.7: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-renderer-file/-/power-assert-renderer-file-1.1.1.tgz#a37e2bbd178ccacd04e78dbb79c92fe34933c5e7" + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-renderer-file/-/power-assert-renderer-file-1.2.0.tgz#3f4bebd9e1455d75cf2ac541e7bb515a87d4ce4b" dependencies: power-assert-renderer-base "^1.1.1" -power-assert-util-string-width@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz#be659eb7937fdd2e6c9a77268daaf64bd5b7c592" +power-assert-util-string-width@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/power-assert-util-string-width/-/power-assert-util-string-width-1.2.0.tgz#6e06d5e3581bb876c5d377c53109fffa95bd91a0" dependencies: - eastasianwidth "^0.1.1" + eastasianwidth "^0.2.0" power-assert@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.5.0.tgz#624caa76a5dc228c00f36704bb1762657c174fee" + version "1.6.1" + resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.6.1.tgz#b28cbc02ae808afd1431d0cd5093a39ac5a5b1fe" dependencies: define-properties "^1.1.2" - empower "^1.2.3" - power-assert-formatter "^1.3.1" + empower "^1.3.1" + power-assert-formatter "^1.4.1" universal-deep-strict-equal "^1.2.1" xtend "^4.0.0" @@ -9164,8 +9309,8 @@ prettier@1.9.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.9.2.tgz#96bc2132f7a32338e6078aeb29727178c6335827" prettier@^1.12.1: - version "1.13.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.4.tgz#31bbae6990f13b1093187c731766a14036fa72e6" + version "1.14.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.2.tgz#0ac1c6e1a90baa22a62925f41963c841983282f9" pretty-bytes@^1.0.0: version "1.0.4" @@ -9200,8 +9345,8 @@ pretty-format@^23.6.0: ansi-styles "^3.2.0" prismjs@^1.6.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.14.0.tgz#bbccfdb8be5d850d26453933cb50122ca0362ae0" + version "1.15.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9" optionalDependencies: clipboard "^2.0.0" @@ -9236,12 +9381,6 @@ promise-retry@^1.1.1: err-code "^1.0.0" retry "^0.10.0" -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - dependencies: - asap "~2.0.3" - prompts@^0.1.9: version "0.1.14" resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2" @@ -9255,11 +9394,10 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: - fbjs "^0.8.16" loose-envify "^1.3.1" object-assign "^4.1.1" @@ -9274,11 +9412,11 @@ protoduck@^5.0.0: genfun "^4.0.1" proxy-addr@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" + version "2.0.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" dependencies: forwarded "~0.1.2" - ipaddr.js "1.6.0" + ipaddr.js "1.8.0" prr@~1.0.1: version "1.0.1" @@ -9289,8 +9427,8 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" psl@^1.1.24: - version "1.1.27" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.27.tgz#2b2c77019db86855170d903532400bf71ee085b6" + version "1.1.29" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" public-encrypt@^4.0.0: version "4.0.2" @@ -9355,11 +9493,7 @@ qs@6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" -qs@~6.3.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" - -qs@~6.5.1: +qs@~6.5.1, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -9419,8 +9553,8 @@ randexp@0.4.6: ret "~0.1.10" randomatic@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923" + version "3.1.0" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.0.tgz#36f2ca708e9e567f5ed2ec01949026d50aa10116" dependencies: is-number "^4.0.0" kind-of "^6.0.0" @@ -9500,7 +9634,7 @@ rc-util@^4.0.4, rc-util@^4.4.0: prop-types "^15.5.10" shallowequal "^0.2.2" -rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" dependencies: @@ -9517,14 +9651,14 @@ react-custom-scrollbars@^4.2.1: prop-types "^15.5.10" raf "^3.1.0" -react-dom@^16.2.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" +react-dom@^16.5.0: + version "16.5.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.0.tgz#57704e5718669374b182a17ea79a6d24922cb27d" dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + schedule "^0.3.0" react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3": version "3.0.5" @@ -9550,9 +9684,9 @@ react-highlight-words@^0.10.0: highlight-words-core "^1.1.0" prop-types "^15.5.8" -react-hot-loader@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.3.0.tgz#3d417797acd6f78bd0291ee225828f5dd78a3829" +react-hot-loader@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.3.6.tgz#26e1491f08daf2bad99d141b1927c9faadef2fb4" dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" @@ -9571,9 +9705,9 @@ react-input-autosize@^2.1.2: dependencies: prop-types "^15.5.8" -react-is@^16.4.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf" +react-is@^16.4.2, react-is@^16.5.0: + version "16.5.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.5.0.tgz#2ec7c192709698591efe13722fab3ef56144ba55" react-lifecycles-compat@^3.0.4: version "3.0.4" @@ -9592,15 +9726,6 @@ react-portal@^3.1.0: dependencies: prop-types "^15.5.8" -react-reconciler@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d" - dependencies: - fbjs "^0.8.16" - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.0" - react-redux@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" @@ -9620,47 +9745,49 @@ react-resizable@1.x: react-draggable "^2.2.6 || ^3.0.3" react-select@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.1.tgz#a2fe58a569eb14dcaa6543816260b97e538120d1" + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.3.0.tgz#1828ad5bf7f3e42a835c7e2d8cb13b5c20714876" dependencies: classnames "^2.2.4" prop-types "^15.5.8" react-input-autosize "^2.1.2" react-sizeme@^2.3.6: - version "2.4.4" - resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.4.4.tgz#437c2ae82da744dbe40dc589f595e6f70039961d" + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.5.2.tgz#e7041390cfb895ed15d896aa91d76e147e3b70b5" dependencies: element-resize-detector "^1.1.12" invariant "^2.2.2" lodash.debounce "^4.0.8" lodash.throttle "^4.1.1" + shallowequal "^1.0.2" -react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.0.tgz#0dbe0e24263e94e1830c7afb1f403707fad313a3" +react-test-renderer@^16.0.0-0, react-test-renderer@^16.5.0: + version "16.5.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.0.tgz#1aeca0edc4f27f63265dcaed80ba82e11e762f56" dependencies: - fbjs "^0.8.16" object-assign "^4.1.1" - prop-types "^15.6.0" - react-is "^16.4.0" + prop-types "^15.6.2" + react-is "^16.5.0" + schedule "^0.3.0" react-transition-group@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.1.tgz#31d611b33e143a5e0f2d94c348e026a0f3b474b6" + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a" dependencies: dom-helpers "^3.3.1" loose-envify "^1.3.1" - prop-types "^15.6.1" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" -react@^16.2.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585" +react@^16.5.0: + version "16.5.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.5.0.tgz#f2c1e754bf9751a549d9c6d9aca41905beb56575" dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + schedule "^0.3.0" read-chunk@^2.1.0: version "2.1.0" @@ -9747,7 +9874,7 @@ read@1, read@~1.0.1, read@~1.0.7: "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -9759,7 +9886,7 @@ read@1, read@~1.0.1, read@~1.0.7: readable-stream@1.0: version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" dependencies: core-util-is "~1.0.0" inherits "~2.0.1" @@ -9768,14 +9895,14 @@ readable-stream@1.0: readable-stream@~1.1.10: version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" dependencies: core-util-is "~1.0.0" inherits "~2.0.1" isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: @@ -9802,8 +9929,8 @@ readline2@^1.0.1: mute-stream "0.0.5" realpath-native@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.0.tgz#7885721a83b43bd5327609f0ddecb2482305fdf0" + version "1.0.2" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" dependencies: util.promisify "^1.0.0" @@ -9818,8 +9945,8 @@ recast@^0.12.5: source-map "~0.6.1" recast@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.15.0.tgz#b8c8bfdda245e1580c0a4d9fc25d4e820bf57208" + version "0.15.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.15.5.tgz#6871177ee26720be80d7624e4283d5c855a5cb0b" dependencies: ast-types "0.11.5" esprima "~4.0.0" @@ -9981,8 +10108,8 @@ renderkid@^2.0.1: utila "~0.3" repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" @@ -10022,7 +10149,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" -request@2, request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.85.0: +request@2.87.0: version "2.87.0" resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" dependencies: @@ -10047,39 +10174,35 @@ request@2, request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.85.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@~2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" +request@^2.74.0, request@^2.81.0, request@^2.85.0, request@^2.87.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" -require-from-string@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" - require-from-string@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -10143,8 +10266,8 @@ resolve@1.1.7, resolve@~1.1.0: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolve@^1.1.6, resolve@^1.3.2, resolve@^1.5.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" dependencies: path-parse "^1.0.5" @@ -10203,7 +10326,7 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2. rimraf@~2.2.6, rimraf@~2.2.8: version "2.2.8" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + resolved "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" @@ -10256,14 +10379,14 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" rxjs@^5.4.2, rxjs@^5.4.3, rxjs@^5.5.2: - version "5.5.11" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87" + version "5.5.12" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" dependencies: symbol-observable "1.0.1" rxjs@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.0.tgz#e024d0e180b72756a83c2aaea8f25423751ba978" + version "6.3.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.2.tgz#6a688b16c4e6e980e62ea805ec30648e1c60907f" dependencies: tslib "^1.9.0" @@ -10271,7 +10394,7 @@ safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -10281,7 +10404,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -10337,26 +10460,41 @@ sass-lint@^1.10.2, sass-lint@^1.12.0: util "^0.10.3" sass-loader@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.0.3.tgz#6ca10871a1cc7549f8143db5a9958242c4e4ca2a" + version "7.1.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" dependencies: clone-deep "^2.0.1" loader-utils "^1.0.1" lodash.tail "^4.1.1" neo-async "^2.5.0" pify "^3.0.0" + semver "^5.5.0" sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" +schedule@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schedule/-/schedule-0.3.0.tgz#1be2ab2fc2e768536269ce7326efb478d6c045e8" + dependencies: + object-assign "^4.1.1" + schema-utils@^0.4.0, schema-utils@^0.4.4, schema-utils@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" dependencies: ajv "^6.1.0" ajv-keywords "^3.1.0" +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + scoped-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" @@ -10393,8 +10531,8 @@ semver-diff@^2.0.0: semver "^5.0.3" "semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" semver@~5.3.0: version "5.3.0" @@ -10476,7 +10614,7 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@^1.0.5: +setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -10521,8 +10659,8 @@ shallowequal@^0.2.2: lodash.keys "^3.1.2" shallowequal@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.0.2.tgz#1561dbdefb8c01408100319085764da3fcf83f8f" + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" shebang-command@^1.2.0: version "1.2.0" @@ -10576,7 +10714,7 @@ simple-is@~0.2.0: sinon@1.17.6: version "1.17.6" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.6.tgz#a43116db59577c8296356afee13fafc2332e58e1" + resolved "http://registry.npmjs.org/sinon/-/sinon-1.17.6.tgz#a43116db59577c8296356afee13fafc2332e58e1" dependencies: formatio "1.1.1" lolex "1.3.2" @@ -10591,44 +10729,42 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" -slate-base64-serializer@^0.2.31: - version "0.2.31" - resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.31.tgz#802effe887b429d4072dbb373bf5755b83d3f4f1" +slate-base64-serializer@^0.2.36: + version "0.2.63" + resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.63.tgz#b086dfce5145c29b8465dc54ff5493b726ddca07" dependencies: isomorphic-base64 "^1.0.2" -slate-dev-environment@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.1.2.tgz#743a8bd7f427dc272425b0439a29e83ca5521688" +slate-dev-environment@^0.1.2, slate-dev-environment@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.1.6.tgz#ff22b40ef4cc890ff7706b6b657abc276782424f" dependencies: is-in-browser "^1.1.3" -slate-dev-logger@^0.1.39: - version "0.1.39" - resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.39.tgz#744a69b85034244713e6de51483af5713c345af4" +slate-dev-logger@^0.1.39, slate-dev-logger@^0.1.43: + version "0.1.43" + resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.43.tgz#77f6ca7207fcbf453a5516f3aa8b19794d1d26dc" slate-hotkeys@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.1.2.tgz#2e35a08a42eaaa113b64d438d537e77a582c8d4a" + version "0.1.4" + resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.1.4.tgz#5b10b2a178affc60827f9284d4c0a5d7e5041ffe" dependencies: is-hotkey "^0.1.1" - slate-dev-environment "^0.1.2" + slate-dev-environment "^0.1.4" -slate-plain-serializer@^0.5.10, slate-plain-serializer@^0.5.12: - version "0.5.12" - resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.12.tgz#db782f855f6dc8ae52ab4cc76c68f02a238736a0" +slate-plain-serializer@^0.5.10, slate-plain-serializer@^0.5.17: + version "0.5.41" + resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.41.tgz#dc2d219602c2cb8dc710ac660e108f3b3cc4dc80" dependencies: - slate-dev-logger "^0.1.39" + slate-dev-logger "^0.1.43" -slate-prop-types@^0.4.29: - version "0.4.29" - resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.29.tgz#4564c2d978968296f37d7a6b9edf8a0a2e51593c" - dependencies: - slate-dev-logger "^0.1.39" +slate-prop-types@^0.4.34: + version "0.4.61" + resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.61.tgz#141c109bed81b130dd03ab86dd7541b28d6d962a" slate-react@^0.12.4: - version "0.12.6" - resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.12.6.tgz#486bf9b42cbd6d4ff7299ab0fdff6c40eed035df" + version "0.12.11" + resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.12.11.tgz#6d83e604634704757690a57dbd6aab282a964ad3" dependencies: debug "^3.1.0" get-window "^1.1.1" @@ -10639,20 +10775,20 @@ slate-react@^0.12.4: react-immutable-proptypes "^2.1.0" react-portal "^3.1.0" selection-is-backward "^1.0.0" - slate-base64-serializer "^0.2.31" + slate-base64-serializer "^0.2.36" slate-dev-environment "^0.1.2" slate-dev-logger "^0.1.39" slate-hotkeys "^0.1.2" - slate-plain-serializer "^0.5.12" - slate-prop-types "^0.4.29" + slate-plain-serializer "^0.5.17" + slate-prop-types "^0.4.34" -slate-schema-violations@^0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.10.tgz#165227c230ea6c1027e523b7171a73e860e73646" +slate-schema-violations@^0.1.12: + version "0.1.39" + resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.39.tgz#854ab5624136419cef4c803b1823acabe11f1c15" slate@^0.33.4: - version "0.33.6" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.33.6.tgz#0c7cb193cc5adeecec5c81e2ec0c86ab23dd6755" + version "0.33.8" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.33.8.tgz#c2cd9906c446d010b15e9e28f6d1a01792c7a113" dependencies: debug "^3.1.0" direction "^0.1.5" @@ -10661,7 +10797,7 @@ slate@^0.33.4: is-plain-object "^2.0.4" lodash "^4.17.4" slate-dev-logger "^0.1.39" - slate-schema-violations "^0.1.10" + slate-schema-violations "^0.1.12" type-of "^2.0.1" slice-ansi@0.0.4: @@ -10713,15 +10849,9 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -sockjs-client@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" +sockjs-client@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.5.tgz#1bb7c0f7222c40f42adf14f4442cbd1269771a83" dependencies: debug "^2.6.6" eventsource "0.1.6" @@ -10759,8 +10889,8 @@ socks@^1.1.10: smart-buffer "^1.0.13" socks@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.0.tgz#144985b3331ced3ab5ccbee640ab7cb7d43fdd1f" + version "2.2.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.1.tgz#68ad678b3642fbc5d99c64c165bc561eab0215f9" dependencies: ip "^1.1.5" smart-buffer "^4.0.1" @@ -10823,7 +10953,7 @@ source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" -source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4, source-map@~0.4.1: +source-map@0.4.x, source-map@^0.4.2, source-map@~0.4.1: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: @@ -10833,11 +10963,11 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -10860,8 +10990,8 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz#e2a303236cac54b04031fa7a5a79c7e701df852f" spdy-transport@^2.0.18: version "2.1.0" @@ -10918,8 +11048,10 @@ ssri@^5.0.0, ssri@^5.2.4, ssri@^5.3.0: safe-buffer "^5.1.1" ssri@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.0.tgz#fc21bfc90e03275ac3e23d5a42e38b8a1cbc130d" + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + dependencies: + figgy-pudding "^3.5.1" stable@~0.1.3, stable@~0.1.5: version "0.1.8" @@ -10953,8 +11085,8 @@ statuses@~1.4.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" stdout-stream@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + version "1.4.1" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" dependencies: readable-stream "^2.0.1" @@ -10974,8 +11106,8 @@ stream-buffers@^2.1.0: resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" stream-each@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" dependencies: end-of-stream "^1.1.0" stream-shift "^1.0.0" @@ -11041,6 +11173,14 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string.prototype.trim@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + string_decoder@^1.0.0, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -11052,8 +11192,8 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" stringifier@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.3.0.tgz#def18342f6933db0f2dbfc9aa02175b448c17959" + version "1.4.0" + resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.4.0.tgz#d704581567f4526265d00ed8ecb354a02c3fec28" dependencies: core-js "^2.0.0" traverse "^0.6.6" @@ -11075,10 +11215,6 @@ stringset@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/stringset/-/stringset-0.2.1.tgz#ef259c4e349344377fcd1c913dd2e848c9c042b5" -stringstream@~0.0.4: - version "0.0.6" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" - strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -11158,8 +11294,8 @@ supports-color@^3.1.2, supports-color@^3.2.3: has-flag "^1.0.0" supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" dependencies: has-flag "^3.0.0" @@ -11208,7 +11344,7 @@ systemjs@0.20.19: table@^3.7.8: version "3.8.3" - resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + resolved "http://registry.npmjs.org/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" dependencies: ajv "^4.7.0" ajv-keywords "^1.0.0" @@ -11222,8 +11358,8 @@ tapable@^1.0.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" tar-fs@^1.13.0: - version "1.16.2" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.2.tgz#17e5239747e399f7e77344f5f53365f04af53577" + version "1.16.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" dependencies: chownr "^1.0.1" mkdirp "^0.5.1" @@ -11251,8 +11387,8 @@ tar@^2.0.0: inherits "2" tar@^4, tar@^4.4.0, tar@^4.4.2, tar@^4.4.3: - version "4.4.4" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd" + version "4.4.6" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" dependencies: chownr "^1.0.1" fs-minipass "^1.2.5" @@ -11276,11 +11412,11 @@ term-size@^1.2.0: execa "^0.7.0" test-exclude@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa" + version "4.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" dependencies: arrify "^1.0.1" - micromatch "^3.1.8" + micromatch "^2.3.11" object-assign "^4.1.0" read-pkg-up "^1.0.1" require-main-filename "^1.0.1" @@ -11419,14 +11555,14 @@ toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" -tough-cookie@>=2.3.3, tough-cookie@^2.3.3: - version "2.4.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.2.tgz#aa9133154518b494efab98a58247bfc38818c00c" +tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" dependencies: psl "^1.1.24" punycode "^1.4.1" -tough-cookie@~2.3.0, tough-cookie@~2.3.3: +tough-cookie@~2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" dependencies: @@ -11451,14 +11587,14 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" "true-case-path@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" + version "1.0.3" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" dependencies: - glob "^6.0.4" + glob "^7.1.2" tryer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7" + version "1.0.1" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" tryor@~0.1.2: version "0.1.2" @@ -11483,11 +11619,7 @@ ts-loader@^5.1.0: micromatch "^3.1.4" semver "^5.0.1" -tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: - version "1.9.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e" - -tslib@^1.9.3: +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -11508,8 +11640,8 @@ tslint-react@^3.6.0: tsutils "^2.13.1" tslint@^5.8.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3" + version "5.11.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" dependencies: babel-code-frame "^6.22.0" builtin-modules "^1.1.1" @@ -11522,15 +11654,9 @@ tslint@^5.8.0: resolve "^1.3.2" semver "^5.3.0" tslib "^1.8.0" - tsutils "^2.12.1" + tsutils "^2.27.2" -tsutils@^2.12.1: - version "2.27.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.1.tgz#ab0276ac23664f36ce8fd4414daec4aebf4373ee" - dependencies: - tslib "^1.8.1" - -tsutils@^2.13.1: +tsutils@^2.13.1, tsutils@^2.27.2: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" dependencies: @@ -11546,10 +11672,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -11583,10 +11705,6 @@ typescript@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" -ua-parser-js@^0.7.9: - version "0.7.18" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" - uglify-es@^3.3.4: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" @@ -11596,49 +11714,27 @@ uglify-es@^3.3.4: uglify-js@2.6.x: version "2.6.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.6.4.tgz#65ea2fb3059c9394692f15fed87c2b36c16b9adf" + resolved "http://registry.npmjs.org/uglify-js/-/uglify-js-2.6.4.tgz#65ea2fb3059c9394692f15fed87c2b36c16b9adf" dependencies: async "~0.2.6" source-map "~0.5.1" uglify-to-browserify "~1.0.0" yargs "~3.10.0" -uglify-js@3.3.x: - version "3.3.28" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.28.tgz#0efb9a13850e11303361c1051f64d2ec68d9be06" +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" dependencies: - commander "~2.15.0" + commander "~2.17.1" source-map "~0.6.1" -uglify-js@^2.6: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" -uglifyjs-webpack-plugin@^1.2.4: - version "1.2.5" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641" - dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" - schema-utils "^0.4.5" - serialize-javascript "^1.4.0" - source-map "^0.6.1" - uglify-es "^3.3.4" - webpack-sources "^1.1.0" - worker-farm "^1.5.2" - -uglifyjs-webpack-plugin@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz#57638dd99c853a1ebfe9d97b42160a8a507f9d00" +uglifyjs-webpack-plugin@^1.2.4, uglifyjs-webpack-plugin@^1.2.7: + version "1.3.0" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz#75f548160858163a08643e086d5fefe18a5d67de" dependencies: cacache "^10.0.4" find-cache-dir "^1.0.0" @@ -11727,8 +11823,8 @@ universal-deep-strict-equal@^1.2.1: object-keys "^1.0.0" universalify@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" @@ -11741,7 +11837,7 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -untildify@^3.0.2: +untildify@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" @@ -11749,7 +11845,7 @@ unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -upath@^1.0.0, upath@^1.0.5: +upath@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" @@ -11778,7 +11874,7 @@ upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" -uri-js@^4.2.1: +uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" dependencies: @@ -11804,9 +11900,9 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.1.8, url-parse@~1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.1.tgz#4dec9dad3dc8585f862fed461d2e19bbf623df30" +url-parse@^1.1.8, url-parse@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.3.tgz#bfaee455c889023219d757e045fa6a684ec36c15" dependencies: querystringify "^2.0.0" requires-port "^1.0.0" @@ -11823,10 +11919,8 @@ url@^0.11.0: querystring "0.2.0" use@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" - dependencies: - kind-of "^6.0.2" + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" user-home@^2.0.0: version "2.0.0" @@ -11855,7 +11949,13 @@ util@0.10.3: dependencies: inherits "2.0.1" -"util@>=0.10.3 <1", util@^0.10.3: +"util@>=0.10.3 <1": + version "0.11.0" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.0.tgz#c5f391beb244103d799b21077a926fef8769e1fb" + dependencies: + inherits "2.0.3" + +util@^0.10.3: version "0.10.4" resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" dependencies: @@ -11873,17 +11973,17 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" -uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" +uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" v8-compile-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz#526492e35fc616864284700b7043e01baee09f0a" + version "2.0.2" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" dependencies: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" @@ -11930,8 +12030,8 @@ vinyl@^1.1.0: replace-ext "0.0.1" vinyl@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c" + version "2.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" dependencies: clone "^2.1.1" clone-buffer "^1.0.0" @@ -11967,8 +12067,8 @@ walker@~1.0.5: makeerror "1.0.x" warning@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.1.tgz#66ce376b7fbfe8a887c22bdf0e7349d73d397745" + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" dependencies: loose-envify "^1.0.0" @@ -12072,24 +12172,23 @@ webpack-core@^0.6.5: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.1.3.tgz#8b32aa43da9ae79368c1bf1183f2b6cf5e1f39ed" +webpack-dev-middleware@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.2.0.tgz#a20ceef194873710052da678f3c6ee0aeed92552" dependencies: loud-rejection "^1.6.0" memory-fs "~0.4.1" - mime "^2.1.0" + mime "^2.3.1" path-is-absolute "^1.0.0" range-parser "^1.0.3" url-join "^4.0.0" - webpack-log "^1.0.1" + webpack-log "^2.0.0" webpack-dev-server@^3.1.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz#9a08d13c4addd1e3b6d8ace116e86715094ad5b4" + version "3.1.8" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.8.tgz#eb7a95945d1108170f902604fb3b939533d9daeb" dependencies: ansi-html "0.0.7" - array-includes "^3.0.3" bonjour "^3.5.0" chokidar "^2.0.0" compression "^1.5.2" @@ -12099,62 +12198,60 @@ webpack-dev-server@^3.1.0: express "^4.16.2" html-entities "^1.2.0" http-proxy-middleware "~0.18.0" - import-local "^1.0.0" - internal-ip "1.2.0" + import-local "^2.0.0" + internal-ip "^3.0.1" ip "^1.1.5" killable "^1.0.0" loglevel "^1.4.1" opn "^5.1.0" portfinder "^1.0.9" + schema-utils "^1.0.0" selfsigned "^1.9.1" serve-index "^1.7.2" sockjs "0.3.19" - sockjs-client "1.1.4" + sockjs-client "1.1.5" spdy "^3.4.1" strip-ansi "^3.0.0" supports-color "^5.1.0" - webpack-dev-middleware "3.1.3" - webpack-log "^1.1.2" - yargs "11.0.0" + webpack-dev-middleware "3.2.0" + webpack-log "^2.0.0" + yargs "12.0.2" -webpack-log@^1.0.1, webpack-log@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.2.0.tgz#a4b34cda6b22b518dbb0ab32e567962d5c72a43d" +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" dependencies: - chalk "^2.1.0" - log-symbols "^2.1.0" - loglevelnext "^1.0.1" - uuid "^3.1.0" + ansi-colors "^3.0.0" + uuid "^3.3.2" webpack-merge@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.2.tgz#5d372dddd3e1e5f8874f5bf5a8e929db09feb216" + version "4.1.4" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.4.tgz#0fde38eabf2d5fd85251c24a5a8c48f8a3f4eb7b" dependencies: lodash "^4.17.5" -webpack-sources@^1.0.1, webpack-sources@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" +webpack-sources@^1.1.0, webpack-sources@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.2.0.tgz#18181e0d013fce096faf6f8e6d41eeffffdceac2" dependencies: source-list-map "^2.0.0" source-map "~0.6.1" webpack@^4.8.0: - version "4.11.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.11.1.tgz#1aa0b936f7ae93a52cf38d2ad0d0f46dcf3c2723" + version "4.18.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.18.0.tgz#7dafaaf309c12e63080d3960fba7ed94afdcbe84" dependencies: - "@webassemblyjs/ast" "1.5.10" - "@webassemblyjs/helper-module-context" "1.5.10" - "@webassemblyjs/wasm-edit" "1.5.10" - "@webassemblyjs/wasm-opt" "1.5.10" - "@webassemblyjs/wasm-parser" "1.5.10" - acorn "^5.0.0" + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-module-context" "1.7.6" + "@webassemblyjs/wasm-edit" "1.7.6" + "@webassemblyjs/wasm-parser" "1.7.6" + acorn "^5.6.2" acorn-dynamic-import "^3.0.0" ajv "^6.1.0" ajv-keywords "^3.1.0" - chrome-trace-event "^0.1.1" - enhanced-resolve "^4.0.0" - eslint-scope "^3.7.1" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" json-parse-better-errors "^1.0.2" loader-runner "^2.3.0" loader-utils "^1.1.0" @@ -12167,7 +12264,7 @@ webpack@^4.8.0: tapable "^1.0.0" uglifyjs-webpack-plugin "^1.2.4" watchpack "^1.5.0" - webpack-sources "^1.0.1" + webpack-sources "^1.2.0" websocket-driver@>=0.5.1: version "0.7.0" @@ -12181,22 +12278,26 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3" + version "1.0.4" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.4.tgz#63fb016b7435b795d9025632c086a5209dbd2621" dependencies: - iconv-lite "0.4.19" + iconv-lite "0.4.23" -whatwg-fetch@>=0.10.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - -whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0: +whatwg-mimetype@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4" -whatwg-url@^6.4.0, whatwg-url@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.1.tgz#fdb94b440fd4ad836202c16e9737d511f012fd67" +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" @@ -12218,7 +12319,7 @@ which-pm-runs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" -which@1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0: +which@1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@^1.3.1, which@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: @@ -12266,7 +12367,7 @@ worker-farm@^1.5.2, worker-farm@^1.6.0: wrap-ansi@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + resolved "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" dependencies: string-width "^1.0.1" strip-ansi "^3.0.1" @@ -12304,6 +12405,12 @@ ws@^4.0.0: async-limiter "~1.0.0" safe-buffer "~5.1.0" +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + dependencies: + async-limiter "~1.0.0" + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" @@ -12320,6 +12427,10 @@ xmlhttprequest@1: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -12328,7 +12439,7 @@ y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" -y18n@^4.0.0: +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -12340,6 +12451,12 @@ yallist@^3.0.0, yallist@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" +yargs-parser@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + dependencies: + camelcase "^4.1.0" + yargs-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" @@ -12352,26 +12469,26 @@ yargs-parser@^9.0.2: dependencies: camelcase "^4.1.0" -yargs@11.0.0, yargs@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" +yargs@12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" dependencies: cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" + decamelize "^2.0.0" + find-up "^3.0.0" get-caller-file "^1.0.1" - os-locale "^2.0.0" + os-locale "^3.0.0" require-directory "^2.1.1" require-main-filename "^1.0.1" set-blocking "^2.0.0" string-width "^2.0.0" which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^9.0.2" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" -yargs@^11.1.0: +yargs@^11.0.0, yargs@^11.1.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" + resolved "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" dependencies: cliui "^4.0.0" decamelize "^1.1.1" @@ -12406,13 +12523,13 @@ yargs@^7.0.0: yargs@~1.2.6: version "1.2.6" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b" + resolved "http://registry.npmjs.org/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b" dependencies: minimist "^0.1.0" yargs@~3.10.0: version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + resolved "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" dependencies: camelcase "^1.0.2" cliui "^2.1.0" @@ -12426,24 +12543,24 @@ yauzl@2.4.1: fd-slicer "~1.0.1" yeoman-environment@^2.0.5, yeoman-environment@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.2.0.tgz#6c0ee93a8d962a9f6dbc5ad4e90ae7ab34875393" + version "2.3.3" + resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.3.3.tgz#1bd9720714cc49036e901503a789d809df8f51bf" dependencies: - chalk "^2.1.0" + chalk "^2.4.1" cross-spawn "^6.0.5" debug "^3.1.0" - diff "^3.3.1" + diff "^3.5.0" escape-string-regexp "^1.0.2" globby "^8.0.1" grouped-queue "^0.3.3" - inquirer "^5.2.0" + inquirer "^6.0.0" is-scoped "^1.0.0" lodash "^4.17.10" - log-symbols "^2.1.0" + log-symbols "^2.2.0" mem-fs "^1.1.0" strip-ansi "^4.0.0" text-table "^0.2.0" - untildify "^3.0.2" + untildify "^3.0.3" yeoman-generator@^2.0.5: version "2.0.5" From d8a702cba385cd477764b9f339ec089b5bc60d2f Mon Sep 17 00:00:00 2001 From: David Date: Tue, 11 Sep 2018 16:57:43 +0200 Subject: [PATCH 082/666] Fix prometheus label filtering for comparison queries (#13213) - Now supports click filtering for queries like `metric > 0.1` --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- .../app/plugins/datasource/prometheus/specs/datasource.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2d7b81b313a..624e5694294 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -39,7 +39,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri // Add empty selector to bare metric name let previousWord; - query = query.replace(/(\w+)\b(?![\(\]{=",])/g, (match, word, offset) => { + query = query.replace(/([A-Za-z]\w*)\b(?![\(\]{=",])/g, (match, word, offset) => { // Check if inside a selector const nextSelectorStart = query.slice(offset).indexOf('{'); const nextSelectorEnd = query.slice(offset).indexOf('}'); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 064f1bc1818..f659c89c3ea 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -376,6 +376,7 @@ describe('PrometheusDatasource', () => { 'foo{bar="baz",instance="my-host.com:9100"}' ); expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])'); + expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001'); }); }); From 19cbff658bb53bc33ccfcaa84cc5d01fd7d76705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 17:36:23 +0200 Subject: [PATCH 083/666] wip: folder settings page to redux progress --- public/app/core/reducers/location.ts | 4 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ----------- .../manage-dashboards/FolderSettingsPage.tsx | 152 +++++------------- .../manage-dashboards/state/actions.ts | 24 ++- .../manage-dashboards/state/reducers.ts | 7 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/types/dashboard.ts | 1 + public/app/types/index.ts | 1 + 10 files changed, 72 insertions(+), 283 deletions(-) delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx index 90528a8798d..a23e495fd3c 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -4,121 +4,53 @@ import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState } from 'app/types'; -import { getFolderByUid } from './state/actions'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; export interface Props { navModel: NavModel; folderUid: string; + folder: FolderState; getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; } export class FolderSettingsPage extends PureComponent { - // formSnapshot: any; - // componentDidMount() { this.props.getFolderByUid(this.props.folderUid); } - // - // loadStore() { - // const { nav, folder, view } = this.props; - // - // return folder.load(view.routeParams.get('uid') as string).then(res => { - // this.formSnapshot = getSnapshot(folder); - // view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - // - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }); - // } - // onTitleChange(evt) { - // this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - // } - // - // getFormSnapshot() { - // if (!this.formSnapshot) { - // this.formSnapshot = getSnapshot(this.props.folder); - // } - // - // return this.formSnapshot; - // } - // - // save(evt) { - // if (evt) { - // evt.stopPropagation(); - // evt.preventDefault(); - // } - // - // const { nav, folder, view } = this.props; - // - // folder - // .saveFolder({ overwrite: false }) - // .then(newUrl => { - // view.updatePathAndQuery(newUrl, {}, {}); - // - // appEvents.emit('dashboard-saved'); - // appEvents.emit('alert-success', ['Folder saved']); - // }) - // .then(() => { - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }) - // .catch(this.handleSaveFolderError.bind(this)); - // } - // - // delete(evt) { - // if (evt) { - // evt.stopPropagation(); - // evt.preventDefault(); - // } - // - // const { folder, view } = this.props; - // const title = folder.folder.title; - // - // appEvents.emit('confirm-modal', { - // title: 'Delete', - // text: `Do you want to delete this folder and all its dashboards?`, - // icon: 'fa-trash', - // yesText: 'Delete', - // onConfirm: () => { - // return folder.deleteFolder().then(() => { - // appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - // view.updatePathAndQuery('dashboards', '', ''); - // }); - // }, - // }); - // } - // - // handleSaveFolderError(err) { - // if (err.data && err.data.status === 'version-mismatch') { - // err.isHandled = true; - // - // const { nav, folder, view } = this.props; - // - // appEvents.emit('confirm-modal', { - // title: 'Conflict', - // text: 'Someone else has updated this folder.', - // text2: 'Would you still like to save this folder?', - // yesText: 'Save & Overwrite', - // icon: 'fa-warning', - // onConfirm: () => { - // folder - // .saveFolder({ overwrite: true }) - // .then(newUrl => { - // view.updatePathAndQuery(newUrl, {}, {}); - // - // appEvents.emit('dashboard-saved'); - // appEvents.emit('alert-success', ['Folder saved']); - // }) - // .then(() => { - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }); - // }, - // }); - // } - // } + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + appEvents.emit('alert-success', ['Folder saved']); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; render() { - const { navModel } = this.props; + const { navModel, folder } = this.props; return (
    @@ -127,25 +59,21 @@ export class FolderSettingsPage extends PureComponent {

    Folder Settings

    -
    +
    - -
    @@ -159,7 +87,6 @@ export class FolderSettingsPage extends PureComponent { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; - return { navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), folderUid: uid, @@ -169,6 +96,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index acd5571aa95..84fe97f22ab 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,8 +1,8 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, NavModelItem } from 'app/types'; -import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; +import { FolderDTO, FolderState, NavModelItem } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -32,7 +32,7 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ export type Action = LoadFolderAction | SetFolderTitleAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; function buildNavModel(folder: FolderDTO): NavModelItem { return { @@ -67,6 +67,7 @@ function buildNavModel(folder: FolderDTO): NavModelItem { ], }; } + export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { const folder = await getBackendSrv().getFolderByUid(uid); @@ -74,3 +75,20 @@ export function getFolderByUid(uid: string): ThunkResult { dispatch(updateNavIndex(buildNavModel(folder))); }; } + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 4844b465dfb..ada5b1812ad 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -16,9 +16,14 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat case ActionTypes.LoadFolder: return { ...action.payload, - canSave: false, hasChanged: false, }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: true, + }; } return state; }; diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 576432d413e..6fbe79cce8c 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -4,6 +4,7 @@ export interface FolderDTO { title: string; url: string; version: number; + canSave: boolean; } export interface FolderState { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 0ddb8f7cd0f..b1096c4827c 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -30,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } From 888ac27e256eb4b8383f6c893ea62b121f41ca49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 18:24:07 +0200 Subject: [PATCH 084/666] commented out metalinter as gopkg is having issues --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb8724bed3c..02a6625a44c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -81,7 +81,7 @@ jobs: working_directory: /go/src/github.com/grafana/grafana steps: - checkout - - run: 'go get -u gopkg.in/alecthomas/gometalinter.v2' + #- run: 'go get -u gopkg.in/alecthomas/gometalinter.v2' - run: 'go get -u github.com/tsenart/deadcode' - run: 'go get -u github.com/gordonklaus/ineffassign' - run: 'go get -u github.com/opennota/check/cmd/structcheck' From 207ce0cde1e6e85b48311f1b01f0b7b11c5a4317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 18:29:50 +0200 Subject: [PATCH 085/666] changed gometalinter to use github master --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 02a6625a44c..e5ac39da530 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -81,7 +81,7 @@ jobs: working_directory: /go/src/github.com/grafana/grafana steps: - checkout - #- run: 'go get -u gopkg.in/alecthomas/gometalinter.v2' + - run: 'go get -u https://github.com/alecthomas/gometalinter' - run: 'go get -u github.com/tsenart/deadcode' - run: 'go get -u github.com/gordonklaus/ineffassign' - run: 'go get -u github.com/opennota/check/cmd/structcheck' From 48d1ebacbba0e225a20f2889ce79b57c68ade171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 18:32:19 +0200 Subject: [PATCH 086/666] Another circleci fix --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e5ac39da530..30db9dac53b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -81,7 +81,7 @@ jobs: working_directory: /go/src/github.com/grafana/grafana steps: - checkout - - run: 'go get -u https://github.com/alecthomas/gometalinter' + - run: 'go get -u github.com/alecthomas/gometalinter' - run: 'go get -u github.com/tsenart/deadcode' - run: 'go get -u github.com/gordonklaus/ineffassign' - run: 'go get -u github.com/opennota/check/cmd/structcheck' From dd01abc54413bc6d209876604b39cf3a3b00dcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 18:37:02 +0200 Subject: [PATCH 087/666] another circleci fix --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 30db9dac53b..186997d0045 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -89,7 +89,7 @@ jobs: - run: 'go get -u github.com/opennota/check/cmd/varcheck' - run: name: run linters - command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' + command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' - run: name: run go vet command: 'go vet ./pkg/...' From ec41d7608089ab65f78954a5218d7eef4bc578ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 19:00:37 +0200 Subject: [PATCH 088/666] mobx -> redux: major progress on folder migration --- public/app/containers/ContainerProps.ts | 2 - .../manage-dashboards}/FolderPermissions.tsx | 49 ++++--- .../FolderSettingsPage.test.tsx | 118 ++++++---------- .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++++++ .../manage-dashboards/state/reducers.ts | 4 +- public/app/routes/routes.ts | 2 +- public/app/stores/RootStore/RootStore.ts | 2 - public/app/types/{dashboard.ts => folder.ts} | 0 public/app/types/index.ts | 2 +- 9 files changed, 211 insertions(+), 99 deletions(-) rename public/app/{containers/ManageDashboards => features/manage-dashboards}/FolderPermissions.tsx (65%) create mode 100644 public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap rename public/app/types/{dashboard.ts => folder.ts} (100%) diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts index ce09b992f80..84c395413b6 100644 --- a/public/app/containers/ContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -1,13 +1,11 @@ import { NavStore } from './../stores/NavStore/NavStore'; import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; interface ContainerProps { nav: typeof NavStore.Type; permissions: typeof PermissionsStore.Type; view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; backendSrv: any; } diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/manage-dashboards/FolderPermissions.tsx similarity index 65% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/manage-dashboards/FolderPermissions.tsx index 072908d2b8e..00b229801f3 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/manage-dashboards/FolderPermissions.tsx @@ -2,24 +2,34 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +37,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { + if (folder.id === 0) { return

    Loading

    ; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
    - +

    Folder Permissions

    @@ -77,4 +79,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx index bed3d569bcc..defec5e6a57 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx @@ -1,84 +1,54 @@ import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel, FolderState } from '../../types'; import { shallow } from 'enzyme'; -describe('FolderSettings', () => { - let wrapper; - let page; +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', + uid: '1234', + title: 'loading', canSave: true, + hasChanged: true, version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); + }); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
    + +
    +

    + Folder Settings +

    +
    + +
    + + +
    +
    + + +
    + +
    +
    +
    +`; + +exports[`Render should render component 1`] = ` +
    + +
    +

    + Folder Settings +

    +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +`; diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index ada5b1812ad..41ae10d19e5 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -2,8 +2,8 @@ import { FolderState } from 'app/types'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { + id: 0, uid: 'loading', - id: -1, title: 'loading', url: '', canSave: false, @@ -22,7 +22,7 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat return { ...state, title: action.payload, - hasChanged: true, + hasChanged: action.payload.trim().length > 0, }; } return state; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 93d83a3b7db..45e72e68c38 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; -import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; +import FolderPermissions from 'app/features/manage-dashboards/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/types/dashboard.ts b/public/app/types/folder.ts similarity index 100% rename from public/app/types/dashboard.ts rename to public/app/types/folder.ts diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b1096c4827c..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,7 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; -import { FolderDTO, FolderState } from './dashboard'; +import { FolderDTO, FolderState } from './folder'; export { Team, From 98daceade0ce40aa8100b9d3b8091dc1049cdb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 19:08:32 +0200 Subject: [PATCH 089/666] fix: fixed typescript test error --- public/app/features/teams/TeamMembers.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx index cae37e184fb..8584edd86c8 100644 --- a/public/app/features/teams/TeamMembers.test.tsx +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TeamMembers, Props } from './TeamMembers'; +import { TeamMembers, Props, State } from './TeamMembers'; import { TeamMember } from '../../types'; import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks'; @@ -64,8 +64,9 @@ describe('Functions', () => { describe('on add user to team', () => { const { wrapper, instance } = setup(); + const state = wrapper.state() as State; - wrapper.state().newTeamMember = { + state.newTeamMember = { id: 1, label: '', avatarUrl: '', From f5ee91f85a1423e6e9ab1f5ded34b76cf0dd451e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 19:30:38 +0200 Subject: [PATCH 090/666] fix: added type export to fix failing test --- public/app/features/teams/TeamMembers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 5ad688aabf8..38e8c0a9aa0 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -16,7 +16,7 @@ export interface Props { setSearchMemberQuery: typeof setSearchMemberQuery; } -interface State { +export interface State { isAdding: boolean; newTeamMember?: User; } From 17861985585c9b291c40c4c5b1626ff4f4aa6f52 Mon Sep 17 00:00:00 2001 From: qhyou11 Date: Wed, 12 Sep 2018 12:52:38 +0800 Subject: [PATCH 091/666] fix theme parameter not working problem while prefer theme set to light (#13232) --- pkg/api/index.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/api/index.go b/pkg/api/index.go index ea10940d3ba..b8101a01fc8 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -91,6 +91,9 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { if themeURLParam == "light" { data.User.LightTheme = true data.Theme = "light" + } else if themeURLParam == "dark" { + data.User.LightTheme = false + data.Theme = "dark" } if hasEditPermissionInFoldersQuery.Result { From a83beac565e55f14089b26ed025ed5a7b66e2cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:15:18 +0200 Subject: [PATCH 092/666] redux: moved folders to it's own features folder --- public/app/containers/ContainerProps.ts | 12 --- .../FolderPermissions.tsx | 6 +- .../FolderSettingsPage.test.tsx | 3 +- .../FolderSettingsPage.tsx | 1 - .../FolderSettingsPage.test.tsx.snap | 0 .../state/actions.ts | 51 +++---------- public/app/features/folders/state/navModel.ts | 35 +++++++++ .../state/reducers.ts | 0 public/app/features/teams/state/actions.ts | 74 +++++-------------- public/app/routes/routes.ts | 4 +- public/app/stores/configureStore.ts | 4 +- yarn.lock | 30 +------- 12 files changed, 80 insertions(+), 140 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts rename public/app/features/{manage-dashboards => folders}/FolderPermissions.tsx (93%) rename public/app/features/{manage-dashboards => folders}/FolderSettingsPage.test.tsx (95%) rename public/app/features/{manage-dashboards => folders}/FolderSettingsPage.tsx (98%) rename public/app/features/{manage-dashboards => folders}/__snapshots__/FolderSettingsPage.test.tsx.snap (100%) rename public/app/features/{manage-dashboards => folders}/state/actions.ts (64%) create mode 100644 public/app/features/folders/state/navModel.ts rename public/app/features/{manage-dashboards => folders}/state/reducers.ts (100%) diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index 84c395413b6..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/features/manage-dashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 93% rename from public/app/features/manage-dashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 00b229801f3..1dc34aaba1e 100644 --- a/public/app/features/manage-dashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; @@ -11,13 +10,16 @@ import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; -import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; export interface Props { navModel: NavModel; getFolderByUid: typeof getFolderByUid; folderUid: string; folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; } @inject('permissions') diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx similarity index 95% rename from public/app/features/manage-dashboards/FolderSettingsPage.test.tsx rename to public/app/features/folders/FolderSettingsPage.test.tsx index defec5e6a57..3680fa9a197 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { FolderSettingsPage, Props } from './FolderSettingsPage'; -import { NavModel, FolderState } from '../../types'; +import { NavModel } from 'app/types'; import { shallow } from 'enzyme'; const setup = (propOverrides?: object) => { @@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => { uid: '1234', title: 'loading', canSave: true, + url: 'url', hasChanged: false, version: 1, }, diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx similarity index 98% rename from public/app/features/manage-dashboards/FolderSettingsPage.tsx rename to public/app/features/folders/FolderSettingsPage.tsx index a23e495fd3c..2aff0e3e1c4 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -31,7 +31,6 @@ export class FolderSettingsPage extends PureComponent { evt.stopPropagation(); await this.props.saveFolder(this.props.folder); - appEvents.emit('alert-success', ['Folder saved']); }; onDelete = evt => { diff --git a/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap similarity index 100% rename from public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap rename to public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/folders/state/actions.ts similarity index 64% rename from public/app/features/manage-dashboards/state/actions.ts rename to public/app/features/folders/state/actions.ts index 84fe97f22ab..5d153b2fb8a 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,8 +1,10 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, FolderState, NavModelItem } from 'app/types'; +import { FolderDTO, FolderState } from 'app/types'; import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -15,16 +17,16 @@ export interface LoadFolderAction { payload: FolderDTO; } -export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ - type: ActionTypes.LoadFolder, - payload: folder, -}); - export interface SetFolderTitleAction { type: ActionTypes.SetFolderTitle; payload: string; } +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ type: ActionTypes.SetFolderTitle, payload: newTitle, @@ -34,39 +36,6 @@ export type Action = LoadFolderAction | SetFolderTitleAction; type ThunkResult = ThunkAction; -function buildNavModel(folder: FolderDTO): NavModelItem { - return { - icon: 'fa fa-folder-open', - id: 'manage-folder', - subTitle: 'Manage folder dashboards & permissions', - url: '', - text: folder.title, - breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], - children: [ - { - active: false, - icon: 'fa fa-fw fa-th-large', - id: `folder-dashboards-${folder.uid}`, - text: 'Dashboards', - url: folder.url, - }, - { - active: false, - icon: 'fa fa-fw fa-lock', - id: `folder-permissions-${folder.uid}`, - text: 'Permissions', - url: `${folder.url}/permissions`, - }, - { - active: false, - icon: 'fa fa-fw fa-cog', - id: `folder-settings-${folder.uid}`, - text: 'Settings', - url: `${folder.url}/settings`, - }, - ], - }; -} export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -82,6 +51,10 @@ export function saveFolder(folder: FolderState): ThunkResult { title: folder.title, version: folder.version, }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + dispatch(updateLocation({ path: `${res.url}/settings` })); }; } diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..614bb30f2d8 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,35 @@ +import { FolderDTO, NavModelItem } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/folders/state/reducers.ts similarity index 100% rename from public/app/features/manage-dashboards/state/reducers.ts rename to public/app/features/folders/state/reducers.ts diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..63bea743607 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -131,107 +131,71 @@ function buildNavModel(team: Team): NavModelItem { export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); }; } export function loadTeamMembers(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/members`) - .then(response => { - dispatch(teamMembersLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + dispatch(teamMembersLoaded(response)); }; } export function addTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/members`, { userId: id }) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + dispatch(loadTeamMembers()); }; } export function removeTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/members/${id}`) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + dispatch(loadTeamMembers()); }; } export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv() - .put(`/api/teams/${team.id}`, { - name, - email, - }) - .then(() => { - dispatch(loadTeam(team.id)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + dispatch(loadTeam(team.id)); }; } export function loadTeamGroups(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/groups`) - .then(response => { - dispatch(teamGroupsLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + dispatch(teamGroupsLoaded(response)); }; } export function addTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups()); }; } export function removeTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/groups/${groupId}`) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 45e72e68c38..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; -import FolderPermissions from 'app/features/manage-dashboards/FolderPermissions'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 5aa5ccc5f41..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,13 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; -import manageDashboardsReducers from 'app/features/manage-dashboards/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, - ...manageDashboardsReducers, + ...foldersReducers, }); export let store; diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: From 0705bf570d403ed4233d4152b3e14a97b71cffc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:47:23 +0200 Subject: [PATCH 093/666] fix: added loading nav states --- public/app/core/selectors/navModel.ts | 10 ++- .../features/folders/FolderPermissions.tsx | 5 +- .../features/folders/FolderSettingsPage.tsx | 4 +- public/app/features/folders/state/navModel.ts | 20 +++++- public/app/features/teams/TeamPages.tsx | 8 ++- public/app/features/teams/state/actions.ts | 43 +----------- public/app/features/teams/state/navModel.ts | 67 +++++++++++++++++++ 7 files changed, 106 insertions(+), 51 deletions(-) create mode 100644 public/app/features/teams/state/navModel.ts diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 1dc34aaba1e..512927c24e6 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -12,6 +12,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { getFolderByUid } from './state/actions'; import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; export interface Props { navModel: NavModel; @@ -48,7 +49,7 @@ export class FolderPermissions extends Component { const { navModel, permissions, backendSrv, folder } = this.props; if (folder.id === 0) { - return

    Loading

    ; + return ; } const dashboardId = folder.id; @@ -84,7 +85,7 @@ export class FolderPermissions extends Component { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; return { - navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`), + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), folderUid: uid, folder: state.folder, }; diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx index 2aff0e3e1c4..1eb7ccafc65 100644 --- a/public/app/features/folders/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -6,6 +6,7 @@ import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; export interface Props { navModel: NavModel; @@ -86,8 +87,9 @@ export class FolderSettingsPage extends PureComponent { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; + return { - navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), folderUid: uid, folder: state.folder, }; diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 614bb30f2d8..e6ef763d019 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -1,4 +1,4 @@ -import { FolderDTO, NavModelItem } from 'app/types'; +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; export function buildNavModel(folder: FolderDTO): NavModelItem { return { @@ -33,3 +33,21 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { ], }; } + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 63bea743607..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,45 +90,6 @@ export function loadTeams(): ThunkResult { }; } -function buildNavModel(team: Team): NavModelItem { - const navModel = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: false, - icon: 'gicon gicon-team', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `team-settings-${team.id}`, - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (config.buildInfo.isEnterprise) { - navModel.children.push({ - active: false, - icon: 'fa fa-fw fa-refresh', - id: `team-groupsync-${team.id}`, - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - return navModel; -} - export function loadTeam(id: number): ThunkResult { return async dispatch => { const response = await getBackendSrv().get(`/api/teams/${id}`); diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} From 78d36f784f3e17a0da13ba0ab007e287eb6f3034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:55:22 +0200 Subject: [PATCH 094/666] fix: gofmt issues --- pkg/models/datasource.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b7e3e3eaa17..cbdd0136f4d 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -59,22 +59,22 @@ type DataSource struct { } var knownDatasourcePlugins = map[string]bool{ - DS_ES: true, - DS_GRAPHITE: true, - DS_INFLUXDB: true, - DS_INFLUXDB_08: true, - DS_KAIROSDB: true, - DS_CLOUDWATCH: true, - DS_PROMETHEUS: true, - DS_OPENTSDB: true, - DS_POSTGRES: true, - DS_MYSQL: true, - DS_MSSQL: true, - "opennms": true, - "abhisant-druid-datasource": true, - "dalmatinerdb-datasource": true, - "gnocci": true, - "zabbix": true, + DS_ES: true, + DS_GRAPHITE: true, + DS_INFLUXDB: true, + DS_INFLUXDB_08: true, + DS_KAIROSDB: true, + DS_CLOUDWATCH: true, + DS_PROMETHEUS: true, + DS_OPENTSDB: true, + DS_POSTGRES: true, + DS_MYSQL: true, + DS_MSSQL: true, + "opennms": true, + "abhisant-druid-datasource": true, + "dalmatinerdb-datasource": true, + "gnocci": true, + "zabbix": true, "alexanderzobnin-zabbix-datasource": true, "newrelic-app": true, "grafana-datadog-datasource": true, From a317158b72c7841dcef935452ecc7a316eb7c8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 12:18:24 +0200 Subject: [PATCH 095/666] wip: working on reducer test --- .../app/features/folders/state/reducers.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 public/app/features/folders/state/reducers.test.ts diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..1a7f4310f76 --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,17 @@ +import { Action, ActionTypes } from './actions'; +import { inititalState, folderReducer } from './reducers'; + +describe('folder reducer', () => { + it('should set teams', () => { + const payload = [getMockTeam()]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialTeamsState, action); + + expect(result.teams).toEqual(payload); + }); +}); From c7bb44b34a73a8a5f82d8cc365ace7a0dd8f450f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 13:13:47 +0200 Subject: [PATCH 096/666] fix: url update loop fix (#13243) --- public/app/core/specs/url.test.ts | 16 ++++++++++++++++ public/app/core/utils/url.ts | 4 +--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 public/app/core/specs/url.test.ts diff --git a/public/app/core/specs/url.test.ts b/public/app/core/specs/url.test.ts new file mode 100644 index 00000000000..b5994488128 --- /dev/null +++ b/public/app/core/specs/url.test.ts @@ -0,0 +1,16 @@ +import { toUrlParams } from '../utils/url'; + +describe('toUrlParams', () => { + it('should encode object properties as url parameters', () => { + const url = toUrlParams({ + server: 'backend-01', + hasSpace: 'has space', + many: ['1', '2', '3'], + true: true, + number: 20, + isNull: null, + isUndefined: undefined, + }); + expect(url).toBe('server=backend-01&hasSpace=has%20space&many=1&many=2&many=3&true&number=20&isNull=&isUndefined='); + }); +}); diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts index 04c3e9a4308..198029b0e9f 100644 --- a/public/app/core/utils/url.ts +++ b/public/app/core/utils/url.ts @@ -50,7 +50,5 @@ export function toUrlParams(a) { return s; }; - return buildParams('', a) - .join('&') - .replace(/%20/g, '+'); + return buildParams('', a).join('&'); } From f0e905f3c9993135603b74bb6de3b9b41e31e404 Mon Sep 17 00:00:00 2001 From: Dan Doyle Date: Wed, 12 Sep 2018 13:17:15 +0000 Subject: [PATCH 097/666] First pass at a text based template var, getting feedback from devs --- .../features/dashboard/submenu/submenu.html | 3 +- public/app/features/templating/all.ts | 2 + .../features/templating/partials/editor.html | 8 +++ .../app/features/templating/text_variable.ts | 58 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 public/app/features/templating/text_variable.ts diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index f240a86efba..9d3332b06e2 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -4,7 +4,8 @@ - + +
    diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index 16465740642..424494f66b2 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -9,6 +9,7 @@ import { DatasourceVariable } from './datasource_variable'; import { CustomVariable } from './custom_variable'; import { ConstantVariable } from './constant_variable'; import { AdhocVariable } from './adhoc_variable'; +import { TextVariable } from './text_variable'; coreModule.factory('templateSrv', () => { return templateSrv; @@ -22,4 +23,5 @@ export { CustomVariable, ConstantVariable, AdhocVariable, + TextVariable }; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 0d8b0ace327..ed8398738da 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -155,6 +155,14 @@
    +
    +
    Text options
    +
    + Value + +
    +
    +
    Query Options
    diff --git a/public/app/features/templating/text_variable.ts b/public/app/features/templating/text_variable.ts new file mode 100644 index 00000000000..3459b99f602 --- /dev/null +++ b/public/app/features/templating/text_variable.ts @@ -0,0 +1,58 @@ +import { Variable, assignModelProperties, variableTypes } from './variable'; + +export class TextVariable implements Variable { + query: string; + current: any; + options: any[]; + skipUrlSync: boolean; + + defaults = { + type: 'text', + name: '', + hide: 2, + label: '', + query: '', + current: {}, + options: [], + skipUrlSync: false, + }; + + /** @ngInject */ + constructor(private model, private variableSrv) { + assignModelProperties(this, model, this.defaults); + } + + getSaveModel() { + assignModelProperties(this.model, this, this.defaults); + return this.model; + } + + setValue(option) { + this.variableSrv.setOptionAsCurrent(this, option); + } + + updateOptions() { + this.options = [{ text: this.query.trim(), value: this.query.trim() }]; + this.current = this.options[0]; + return Promise.resolve(); + } + + dependsOn(variable) { + return false; + } + + setValueFromUrl(urlValue) { + this.query = urlValue; + return this.variableSrv.setOptionFromUrl(this, urlValue); + } + + getValueForUrl() { + return this.current.value; + } +} + +variableTypes['text'] = { + name: 'Text', + ctor: TextVariable, + description: 'Define a textbox variable, where users can enter any arbitrary string', +}; From c56ca57df55a5ff9f6519115735de04016d5800b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 12 Sep 2018 17:54:47 +0200 Subject: [PATCH 098/666] docs: include active directory ldap example and restructure --- docs/sources/auth/ldap.md | 200 ++++++++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 52 deletions(-) diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index f63a44e1750..8e95e24a0b0 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -1,7 +1,7 @@ +++ title = "LDAP Authentication" description = "Grafana LDAP Authentication Guide " -keywords = ["grafana", "configuration", "documentation", "ldap"] +keywords = ["grafana", "configuration", "documentation", "ldap", "active directory"] type = "docs" [menu.docs] name = "LDAP" @@ -10,35 +10,42 @@ parent = "authentication" weight = 2 +++ -# LDAP +# LDAP Authentication The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP -group memberships and Grafana Organization user roles. Below we detail grafana.ini config file -settings and ldap.toml config file options. +group memberships and Grafana Organization user roles. + +## Supported LDAP Servers + +Grafana uses a [third-party LDAP library](https://github.com/go-ldap/ldap) under the hood that supports basic LDAP v3 functionality. +This means that you should be able to configure LDAP integration using any compliant LDAPv3 server, for example [OpenLDAP](#openldap) or +[Active Directory](#active-directory) among [others](https://en.wikipedia.org/wiki/Directory_service#LDAP_implementations). ## Enable LDAP -You turn on LDAP in the [main config file]({{< relref "installation/configuration.md" >}}) as well as specify the path to the LDAP +In order to use LDAP integration you'll first need to enable LDAP in the [main config file]({{< relref "installation/configuration.md" >}}) as well as specify the path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`). ```bash [auth.ldap] # Set to `true` to enable LDAP integration (default: `false`) enabled = true + # Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) -config_file = /etc/grafana/ldap.toml` +config_file = /etc/grafana/ldap.toml + # Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to # false only pre-existing Grafana users will be able to login (if ldap authentication is ok). allow_sign_up = true ``` -## LDAP Configuration +## Grafana LDAP Configuration +Depending on which LDAP server you're using and how that's configured your Grafana LDAP configuration may vary. +See [configuration examples](#configuration-examples) for more information. + +**LDAP specific configuration file (ldap.toml) example:** ```bash -# To troubleshoot and get more log info enable ldap debug logging in grafana.ini -# [log] -# filters = ldap:debug - [[servers]] # Ldap server host (specify multiple hosts space separated) host = "127.0.0.1" @@ -69,13 +76,8 @@ search_filter = "(cn=%s)" # An array of base dns to search through search_base_dns = ["dc=grafana,dc=org"] -# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups. -# This is done by enabling group_search_filter below. You must also set member_of= "cn" -# in [servers.attributes] below. - -## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" -## An array of the base DNs to search through for groups. Typically uses ou=groups +# group_search_filter_user_attribute = "distinguishedName" # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] # Specify names of the ldap attributes your ldap uses @@ -85,28 +87,11 @@ surname = "sn" username = "cn" member_of = "memberOf" email = "email" - -# Map ldap groups to grafana org roles -[[servers.group_mappings]] -group_dn = "cn=admins,dc=grafana,dc=org" -org_role = "Admin" -# To make user an instance admin (Grafana Admin) uncomment line below -# grafana_admin = true -# The Grafana organization database id, optional, if left out the default org (id 1) will be used. Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs -# org_id = 1 - -[[servers.group_mappings]] -group_dn = "cn=users,dc=grafana,dc=org" -org_role = "Editor" - -[[servers.group_mappings]] -# If you want to match all (or no ldap groups) then you can use wildcard -group_dn = "*" -org_role = "Viewer" - ``` -## Bind & Bind Password +### Bind + +#### Bind & Bind Password By default the configuration expects you to specify a bind DN and bind password. This should be a read only user that can perform LDAP searches. When the user DN is found a second bind is performed with the user provided username & password (in the normal Grafana login form). @@ -116,7 +101,7 @@ bind_dn = "cn=admin,dc=grafana,dc=org" bind_password = "grafana" ``` -### Single Bind Example +#### Single Bind Example If you can provide a single bind expression that matches all possible users, you can skip the second bind and bind against the user DN directly. This allows you to not specify a bind_password in the configuration file. @@ -128,7 +113,7 @@ bind_dn = "cn=%s,o=users,dc=grafana,dc=org" In this case you skip providing a `bind_password` and instead provide a `bind_dn` value with a `%s` somewhere. This will be replaced with the username entered in on the Grafana login page. The search filter and search bases settings are still needed to perform the LDAP search to retrieve the other LDAP information (like LDAP groups and email). -## POSIX schema (no memberOf attribute) +### POSIX schema If your ldap server does not support the memberOf attribute add these options: ```bash @@ -140,23 +125,134 @@ group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] Also change set `member_of = "cn"` in the `[servers.attributes]` section. +### Group Mappings -## LDAP to Grafana Org Role Sync +In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization and role. These will be synced every time the user logs in, with LDAP being +the authoritative source. So, if you change a user's role in the Grafana Org. Users page, this change will be reset the next time the user logs in. If you +change the LDAP groups of a user, the change will take effect the next time the user logs in. -### Mappings -In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization -and role. These will be synced every time the user logs in, with LDAP being -the authoritative source. So, if you change a user's role in the Grafana Org. -Users page, this change will be reset the next time the user logs in. If you -change the LDAP groups of a user, the change will take effect the next -time the user logs in. +The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the +TOML config will be used. -### Grafana Admin -with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs & -users. +**LDAP specific configuration file (ldap.toml) example:** +```bash +[[servers]] +# other settings omitted for clarity -### Priority -The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used. +[[servers.group_mappings]] +group_dn = "cn=superadmins,dc=grafana,dc=org" +org_role = "Admin" +grafana_admin = true # Available in Grafana v5.3 and above + +[[servers.group_mappings]] +group_dn = "cn=admins,dc=grafana,dc=org" +org_role = "Admin" + +[[servers.group_mappings]] +group_dn = "cn=users,dc=grafana,dc=org" +org_role = "Editor" + +[[servers.group_mappings]] +group_dn = "*" +org_role = "Viewer" +``` + +Setting | Required | Description | Default +------------ | ------------ | ------------- | ------------- +`group_dn` | Yes | LDAP distinguished name (DN) of LDAP group. If you want to match all (or no LDAP groups) then you can use wildcard (`"*"`) | +`org_role` | Yes | Assign users of `group_dn` the organisation role `"Admin"`, `"Editor"` or `"Viewer"` | +`org_id` | No | The Grafana organization database id. Setting this allows for multiple group_dn's to be assigned to the same `org_role` provided the `org_id` differs | `1` (default org id) +`grafana_admin` | No | When `true` makes user of `group_dn` Grafana server admin. A Grafana server admin has admin access over all organisations and users. Available in Grafana v5.3 and above | `false` + +### Nested/recursive group membership + +Users with nested/recursive group membership must have an LDAP server that supports `LDAP_MATCHING_RULE_IN_CHAIN` +and configure `group_search_filter` in a way that it returns the groups the submitted username is a member of. + +**Active Directory example:** + +Active Directory groups store the Distinguished Names (DNs) of members, so your filter will need to know the DN for the user based only on the submitted username. +Multiple DN templates can be searched by combining filters with the LDAP OR-operator. Examples: + +```bash +group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])" +group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))" +``` + +For troubleshooting, by changing `member_of` in `[servers.attributes]` to "distinguishedName" it will show you more accurate group memberships when [debug is enabled](#troubleshooting). +## Configuration examples +### OpenLDAP + +[OpenLDAP](http://www.openldap.org/) is an open source directory service. + +**LDAP specific configuration file (ldap.toml):** +```bash +[[servers]] +host = "127.0.0.1" +port = 389 +use_ssl = false +start_tls = false +ssl_skip_verify = false +bind_dn = "cn=admin,dc=grafana,dc=org" +bind_password = 'grafana' +search_filter = "(cn=%s)" +search_base_dns = ["dc=grafana,dc=org"] + +[servers.attributes] +name = "givenName" +surname = "sn" +username = "cn" +member_of = "memberOf" +email = "email" + +# [[servers.group_mappings]] omitted for clarity +``` + +### Active Directory + +[Active Directory](https://technet.microsoft.com/en-us/library/hh831484(v=ws.11).aspx) is a directory service which is commonly used in Windows environments. + +Assuming the following Active Directory server setup: + +* IP address: `10.0.0.1` +* Domain: `CORP` +* DNS name: `corp.local` + +**LDAP specific configuration file (ldap.toml):** +```bash +[[servers]] +host = "10.0.0.1" +port = 3269 +use_ssl = true +start_tls = false +ssl_skip_verify = true +bind_dn = "CORP\\%s" +search_filter = "(sAMAccountName=%s)" +search_base_dns = ["dc=corp,dc=local"] + +[servers.attributes] +name = "givenName" +surname = "sn" +username = "sAMAccountName" +member_of = "memberOf" +email = "mail" + +# [[servers.group_mappings]] omitted for clarity +``` + +#### Port requirements + +In above example SSL is enabled and an encrypted port have been configured. If your Active Directory don't support SSL please change `enable_ssl = false` and `port = 389`. +Please inspect your Active Directory configuration and documentation to find the correct settings. For more information about Active Directory and port requirements see [link](https://technet.microsoft.com/en-us/library/dd772723(v=ws.10)). + +## Troubleshooting + +To troubleshoot and get more log info enable ldap debug logging in the [main config file]({{< relref "installation/configuration.md" >}}). + +```bash +[log] +filters = ldap:debug +``` From a5bcd4b8e42bc35cb29653768b1331c8b86a793d Mon Sep 17 00:00:00 2001 From: David Date: Wed, 12 Sep 2018 18:10:57 +0200 Subject: [PATCH 099/666] Adhoc-filtering for prometheus dashboards (#13212) * Basic adhoc-filtering support for prometheus --- .../app/features/dashboard/ad_hoc_filters.ts | 6 +- .../app/features/templating/template_srv.ts | 7 +- .../prometheus/add_label_to_query.ts | 93 +++++++++++++++++++ .../datasource/prometheus/datasource.ts | 85 ++++------------- .../specs/add_label_to_query.test.ts | 42 +++++++++ .../prometheus/specs/datasource.test.ts | 63 ++++++++----- 6 files changed, 196 insertions(+), 100 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/add_label_to_query.ts create mode 100644 public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index 68b068152b5..0ceac9ddbba 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -59,10 +59,10 @@ export class AdHocFiltersCtrl { let promise = null; if (segment.type !== 'value') { - promise = ds.getTagKeys(); + promise = ds.getTagKeys ? ds.getTagKeys() : Promise.resolve([]); } else { options.key = this.segments[index - 2].value; - promise = ds.getTagValues(options); + promise = ds.getTagValues ? ds.getTagValues(options) : Promise.resolve([]); } return promise.then(results => { @@ -99,7 +99,7 @@ export class AdHocFiltersCtrl { this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); } this.segments.push(this.uiSegmentSrv.newOperator('=')); - this.segments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value')); + this.segments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value')); segment.type = 'key'; segment.cssClass = 'query-segment-key'; } diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 6eab51abbfa..def9fda1f56 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -56,11 +56,10 @@ export class TemplateSrv { continue; } - if (variable.datasource === datasourceName) { + // null is the "default" datasource + if (variable.datasource === null || variable.datasource === datasourceName) { filters = filters.concat(variable.filters); - } - - if (variable.datasource.indexOf('$') === 0) { + } else if (variable.datasource.indexOf('$') === 0) { if (this.replace(variable.datasource) === datasourceName) { filters = filters.concat(variable.filters); } diff --git a/public/app/plugins/datasource/prometheus/add_label_to_query.ts b/public/app/plugins/datasource/prometheus/add_label_to_query.ts new file mode 100644 index 00000000000..9ea01ed755a --- /dev/null +++ b/public/app/plugins/datasource/prometheus/add_label_to_query.ts @@ -0,0 +1,93 @@ +import _ from 'lodash'; + +const keywords = 'by|without|on|ignoring|group_left|group_right'; + +// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded. +const builtInWords = [ + keywords, + 'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile', + 'true|false|null|__name__|job', + 'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv', + 'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2', + 'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time', + 'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time', +] + .join('|') + .split('|'); + +const metricNameRegexp = /([A-Za-z]\w*)\b(?![\(\]{=!",])/g; +const selectorRegexp = /{([^{]*)}/g; + +// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}' +export function addLabelToQuery(query: string, key: string, value: string, operator?: string): string { + if (!key || !value) { + throw new Error('Need label to add to query.'); + } + + // Add empty selectors to bare metric names + let previousWord; + query = query.replace(metricNameRegexp, (match, word, offset) => { + const insideSelector = isPositionInsideChars(query, offset, '{', '}'); + // Handle "sum by (key) (metric)" + const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1; + previousWord = word; + if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) { + return `${word}{}`; + } + return word; + }); + + // Adding label to existing selectors + let match = selectorRegexp.exec(query); + const parts = []; + let lastIndex = 0; + let suffix = ''; + + while (match) { + const prefix = query.slice(lastIndex, match.index); + const selector = match[1]; + const selectorWithLabel = addLabelToSelector(selector, key, value, operator); + lastIndex = match.index + match[1].length + 2; + suffix = query.slice(match.index + match[0].length); + parts.push(prefix, '{', selectorWithLabel, '}'); + match = selectorRegexp.exec(query); + } + + parts.push(suffix); + return parts.join(''); +} + +const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g; + +function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) { + const parsedLabels = []; + + // Split selector into labels + if (selector) { + let match = labelRegexp.exec(selector); + while (match) { + parsedLabels.push({ key: match[1], operator: match[2], value: match[3] }); + match = labelRegexp.exec(selector); + } + } + + // Add new label + const operatorForLabelKey = labelOperator || '='; + parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` }); + + // Sort labels by key and put them together + return _.chain(parsedLabels) + .compact() + .sortBy('key') + .map(({ key, operator, value }) => `${key}${operator}${value}`) + .value() + .join(','); +} + +function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) { + const nextSelectorStart = text.slice(position).indexOf(openChar); + const nextSelectorEnd = text.slice(position).indexOf(closeChar); + return nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd); +} + +export default addLabelToQuery; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 624e5694294..a07949490b2 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -7,6 +7,8 @@ import PrometheusMetricFindQuery from './metric_find_query'; import { ResultTransformer } from './result_transformer'; import { BackendSrv } from 'app/core/services/backend_srv'; +import addLabelToQuery from './add_label_to_query'; + export function alignRange(start, end, step) { const alignedEnd = Math.ceil(end / step) * step; const alignedStart = Math.floor(start / step) * step; @@ -16,74 +18,6 @@ export function alignRange(start, end, step) { }; } -const keywords = 'by|without|on|ignoring|group_left|group_right'; - -// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded. -const builtInWords = [ - keywords, - 'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile', - 'true|false|null|__name__|job', - 'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv', - 'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2', - 'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time', - 'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time', -] - .join('|') - .split('|'); - -// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}' -export function addLabelToQuery(query: string, key: string, value: string): string { - if (!key || !value) { - throw new Error('Need label to add to query.'); - } - - // Add empty selector to bare metric name - let previousWord; - query = query.replace(/([A-Za-z]\w*)\b(?![\(\]{=",])/g, (match, word, offset) => { - // Check if inside a selector - const nextSelectorStart = query.slice(offset).indexOf('{'); - const nextSelectorEnd = query.slice(offset).indexOf('}'); - const insideSelector = nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd); - // Handle "sum by (key) (metric)" - const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1; - previousWord = word; - if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) { - return `${word}{}`; - } - return word; - }); - - // Adding label to existing selectors - const selectorRegexp = /{([^{]*)}/g; - let match = selectorRegexp.exec(query); - const parts = []; - let lastIndex = 0; - let suffix = ''; - - while (match) { - const prefix = query.slice(lastIndex, match.index); - const selectorParts = match[1].split(','); - const labels = selectorParts.reduce((acc, label) => { - const labelParts = label.split('='); - if (labelParts.length === 2) { - acc[labelParts[0]] = labelParts[1]; - } - return acc; - }, {}); - labels[key] = `"${value}"`; - const selector = Object.keys(labels) - .sort() - .map(key => `${key}=${labels[key]}`) - .join(','); - lastIndex = match.index + match[1].length + 2; - suffix = query.slice(match.index + match[0].length); - parts.push(prefix, '{', selector, '}'); - match = selectorRegexp.exec(query); - } - parts.push(suffix); - return parts.join(''); -} - export function determineQueryHints(series: any[], datasource?: any): any[] { const hints = series.map((s, i) => { const query: string = s.query; @@ -406,8 +340,21 @@ export class PrometheusDatasource { } query.step = interval; + let expr = target.expr; + + // Apply adhoc filters + const adhocFilters = this.templateSrv.getAdhocFilters(this.name); + expr = adhocFilters.reduce((acc, filter) => { + const { key, operator } = filter; + let { value } = filter; + if (operator === '=~' || operator === '!~') { + value = prometheusSpecialRegexEscape(value); + } + return addLabelToQuery(acc, key, value, operator); + }, expr); + // Only replace vars in expression after having (possibly) updated interval vars - query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr); + query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr); query.requestId = options.panelId + target.refId; // Align query interval with step diff --git a/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts b/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts new file mode 100644 index 00000000000..9c654e8e467 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts @@ -0,0 +1,42 @@ +import addLabelToQuery from '../add_label_to_query'; + +describe('addLabelToQuery()', () => { + it('should add label to simple query', () => { + expect(() => { + addLabelToQuery('foo', '', ''); + }).toThrow(); + expect(addLabelToQuery('foo', 'bar', 'baz')).toBe('foo{bar="baz"}'); + expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}'); + expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}'); + expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001'); + }); + + it('should add custom operator', () => { + expect(addLabelToQuery('foo{}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz"}'); + expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz",x="yy"}'); + }); + + it('should not modify ranges', () => { + expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])'); + }); + + it('should detect in-order function use', () => { + expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})'); + }); + + it('should handle selectors with punctuation', () => { + expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( + 'foo{bar="baz",instance="my-host.com:9100"}' + ); + expect(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz')).toBe('foo{bar="baz",list="a,b,c"}'); + }); + + it('should work on arithmetical expressions', () => { + expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}'); + expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}'); + expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'); + expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe( + 'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}' + ); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index f659c89c3ea..ae91e6647e0 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -8,11 +8,15 @@ import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape, - addLabelToQuery, } from '../datasource'; jest.mock('../metric_find_query'); +const DEFAULT_TEMPLATE_SRV_MOCK = { + getAdhocFilters: () => [], + replace: a => a, +}; + describe('PrometheusDatasource', () => { const ctx: any = {}; const instanceSettings = { @@ -25,9 +29,8 @@ describe('PrometheusDatasource', () => { ctx.backendSrvMock = {}; - ctx.templateSrvMock = { - replace: a => a, - }; + ctx.templateSrvMock = DEFAULT_TEMPLATE_SRV_MOCK; + ctx.timeSrvMock = { timeRange: () => { return { @@ -60,6 +63,37 @@ describe('PrometheusDatasource', () => { }); }); + describe('When using adhoc filters', () => { + const DEFAULT_QUERY_EXPRESSION = 'metric{job="foo"} - metric'; + const target = { expr: DEFAULT_QUERY_EXPRESSION }; + + afterEach(() => { + ctx.templateSrvMock.getAdhocFilters = DEFAULT_TEMPLATE_SRV_MOCK.getAdhocFilters; + }); + + it('should not modify expression with no filters', () => { + const result = ctx.ds.createQuery(target, { interval: '15s' }); + expect(result).toMatchObject({ expr: DEFAULT_QUERY_EXPRESSION }); + }); + + it('should add filters to expression', () => { + ctx.templateSrvMock.getAdhocFilters = () => [ + { + key: 'k1', + operator: '=', + value: 'v1', + }, + { + key: 'k2', + operator: '!=', + value: 'v2', + }, + ]; + const result = ctx.ds.createQuery(target, { interval: '15s' }); + expect(result).toMatchObject({ expr: 'metric{job="foo",k1="v1",k2!="v2"} - metric{k1="v1",k2!="v2"}' }); + }); + }); + describe('When performing performSuggestQuery', () => { it('should cache response', async () => { ctx.backendSrvMock.datasourceRequest.mockReturnValue( @@ -358,26 +392,6 @@ describe('PrometheusDatasource', () => { expect(intervalMs).toEqual({ text: 15000, value: 15000 }); }); }); - - describe('addLabelToQuery()', () => { - expect(() => { - addLabelToQuery('foo', '', ''); - }).toThrow(); - expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}'); - expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}'); - expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}'); - expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}'); - expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'); - expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe( - 'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}' - ); - expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})'); - expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( - 'foo{bar="baz",instance="my-host.com:9100"}' - ); - expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])'); - expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001'); - }); }); const SECOND = 1000; @@ -399,6 +413,7 @@ const backendSrv = { } as any; const templateSrv = { + getAdhocFilters: () => [], replace: jest.fn(str => str), }; From 8096cd8f3374a174e113ffe53276af0acd8cf434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 07:30:27 +0200 Subject: [PATCH 100/666] fix: added reducer test --- .../features/folders/state/reducers.test.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index 1a7f4310f76..ff37f13f97f 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,17 +1,42 @@ import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; import { inititalState, folderReducer } from './reducers'; +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + describe('folder reducer', () => { - it('should set teams', () => { - const payload = [getMockTeam()]; + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); const action: Action = { - type: ActionTypes.LoadTeams, - payload, + type: ActionTypes.LoadFolder, + payload: folder, }; - const result = teamsReducer(initialTeamsState, action); + const state = folderReducer(inititalState, action); - expect(result.teams).toEqual(payload); + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); }); }); From f360b6186b0d0726762382caec2a787c493cb386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 10:52:29 +0200 Subject: [PATCH 101/666] wip: first couple of things starting to work --- public/app/core/actions/permissions.ts | 24 +++++ .../DisabledPermissionListItem.tsx | 43 ++++++++ .../PermissionList/PermissionList.tsx | 61 +++++++++++ .../PermissionList/PermissionListItem.tsx | 100 ++++++++++++++++++ .../features/folders/FolderPermissions.tsx | 70 +++++++----- public/app/features/folders/state/actions.ts | 85 ++++++++++++++- public/app/features/folders/state/reducers.ts | 41 ++++++- public/app/types/acl.ts | 60 +++++++++++ public/app/types/folder.ts | 11 +- public/app/types/index.ts | 8 +- 10 files changed, 465 insertions(+), 38 deletions(-) create mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/components/PermissionList/DisabledPermissionListItem.tsx create mode 100644 public/app/core/components/PermissionList/PermissionList.tsx create mode 100644 public/app/core/components/PermissionList/PermissionListItem.tsx create mode 100644 public/app/types/acl.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts new file mode 100644 index 00000000000..2b07b7145dd --- /dev/null +++ b/public/app/core/actions/permissions.ts @@ -0,0 +1,24 @@ +import { DashboardAcl } from '../../types'; + +export enum ActionTypes { + LoadFolderPermissions = 'LoadFolderPermissions', +} + +export interface LoadFolderPermissionsAction { + type: ActionTypes.LoadFolderPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadFolderPermissions; + +export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); + +export function getFolderPermissions(uid: string): ThunkResult { + return async dispatch => { + const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx new file mode 100644 index 00000000000..d65595dae66 --- /dev/null +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; +import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; + +export interface Props { + item: any; +} + +export default class DisabledPermissionListItem extends Component { + render() { + const { item } = this.props; + + return ( + + + + + + {item.name} + (Role) + + + Can + +
    + {}} + value={item.permission} + disabled={true} + className={'gf-form-input--form-dropdown-right'} + /> +
    + + + + + + ); + } +} diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx new file mode 100644 index 00000000000..29f810a4358 --- /dev/null +++ b/public/app/core/components/PermissionList/PermissionList.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import PermissionsListItem from './PermissionListItem'; +import DisabledPermissionsListItem from './DisabledPermissionListItem'; +import { DashboardAcl, FolderInfo } from 'app/types'; + +export interface Props { + items: DashboardAcl[]; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: any; + isFetching: boolean; + folderInfo?: FolderInfo; +} + +class PermissionList extends PureComponent { + render() { + const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props; + + return ( + + + + {items.map((item, idx) => { + return ( + + ); + })} + {isFetching === true && items.length < 1 ? ( + + + + ) : null} + + {isFetching === false && items.length < 1 ? ( + + + + ) : null} + +
    + Loading permissions... +
    + No permissions are set. Will only be accessible by admins. +
    + ); + } +} + +export default PermissionList; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx new file mode 100644 index 00000000000..3e5aaf3ab2f --- /dev/null +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -0,0 +1,100 @@ +import React, { PureComponent } from 'react'; +import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; +import { dashboardPermissionLevels } from 'app/types/acl'; +import { DashboardAcl, FolderInfo, PermissionLevel } from 'app/types'; + +const setClassNameHelper = inherited => { + return inherited ? 'gf-form-disabled' : ''; +}; + +function ItemAvatar({ item }) { + if (item.userAvatarUrl) { + return ; + } + if (item.teamAvatarUrl) { + return ; + } + if (item.role === 'Editor') { + return ; + } + + return ; +} + +function ItemDescription({ item }) { + if (item.userId) { + return (User); + } + if (item.teamId) { + return (Team); + } + return (Role); +} + +interface Props { + item: DashboardAcl; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void; + folderInfo?: FolderInfo; +} + +export default class PermissionsListItem extends PureComponent { + onPermissionChanged = option => { + this.props.onPermissionChanged(this.props.item, option.value as PermissionLevel); + }; + + onRemoveItem = () => { + this.props.onRemoveItem(this.props.item); + }; + + render() { + const { item, folderInfo } = this.props; + const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; + + return ( + + + + + + {item.name} + + + {item.inherited && + folderInfo && ( + + Inherited from folder{' '} + + {folderInfo.title} + {' '} + + )} + {inheritedFromRoot && Default Permission} + + Can + +
    + +
    + + + {!item.inherited ? ( + + + + ) : ( + + )} + + + ); + } +} diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 512927c24e6..25de5f8be16 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; @@ -9,50 +8,61 @@ import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, FolderState } from 'app/types'; -import { getFolderByUid } from './state/actions'; -import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types'; +import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions'; import { getLoadingNav } from './state/navModel'; +import PermissionList from 'app/core/components/PermissionList/PermissionList'; export interface Props { navModel: NavModel; - getFolderByUid: typeof getFolderByUid; folderUid: string; folder: FolderState; - permissions: typeof PermissionsStore.Type; - backendSrv: any; + getFolderByUid: typeof getFolderByUid; + getFolderPermissions: typeof getFolderPermissions; + updateFolderPermission: typeof updateFolderPermission; + removeFolderPermission: typeof removeFolderPermission; } -@inject('permissions') -@observer -export class FolderPermissions extends Component { +export interface State { + isAdding: boolean; +} + +export class FolderPermissions extends Component { constructor(props) { super(props); - this.handleAddPermission = this.handleAddPermission.bind(this); + + this.state = { + isAdding: false, + }; } componentDidMount() { this.props.getFolderByUid(this.props.folderUid); + this.props.getFolderPermissions(this.props.folderUid); } - componentWillUnmount() { - const { permissions } = this.props; - permissions.hideAddPermissions(); - } + onOpenAddPermissions = () => { + this.setState({ isAdding: true }); + }; - handleAddPermission() { - const { permissions } = this.props; - permissions.toggleAddPermissions(); - } + onRemoveItem = (item: DashboardAcl) => { + this.props.removeFolderPermission(item); + }; + + onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { + this.props.updateFolderPermission(item, level); + }; render() { - const { navModel, permissions, backendSrv, folder } = this.props; + const { navModel, folder } = this.props; + const { isAdding } = this.state; if (folder.id === 0) { return ; } const dashboardId = folder.id; + const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; return (
    @@ -64,18 +74,17 @@ export class FolderPermissions extends Component {
    -
    - - - - +
    ); @@ -93,6 +102,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 5d153b2fb8a..29940cc7a31 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,7 +1,14 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, FolderState } from 'app/types'; +import { + FolderDTO, + FolderState, + DashboardAcl, + DashboardAclDTO, + PermissionLevel, + DashboardAclUpdateDTO, +} from 'app/types'; import { updateNavIndex, updateLocation } from 'app/core/actions'; import { buildNavModel } from './navModel'; import appEvents from 'app/core/app_events'; @@ -10,6 +17,7 @@ export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', SetFolderTitle = 'SET_FOLDER_TITLE', SaveFolder = 'SAVE_FOLDER', + LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS', } export interface LoadFolderAction { @@ -22,6 +30,15 @@ export interface SetFolderTitleAction { payload: string; } +export interface LoadFolderPermissionsAction { + type: ActionTypes.LoadFolderPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadFolderAction | SetFolderTitleAction | LoadFolderPermissionsAction; + +type ThunkResult = ThunkAction; + export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ type: ActionTypes.LoadFolder, payload: folder, @@ -32,10 +49,10 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ payload: newTitle, }); -export type Action = LoadFolderAction | SetFolderTitleAction; - -type ThunkResult = ThunkAction; - +export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -65,3 +82,61 @@ export function deleteFolder(uid: string): ThunkResult { dispatch(updateLocation({ path: `dashboards` })); }; } + +export function getFolderPermissions(uid: string): ThunkResult { + return async dispatch => { + const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} + +function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { + return { + userId: item.userId, + teamId: item.teamId, + role: item.role, + permission: item.permission, + }; +} + +export function updateFolderPermission(itemToUpdate: DashboardAcl, level: PermissionLevel): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited) { + continue; + } + + const updated = toUpdateItem(itemToUpdate); + + // if this is the item we want to update, update it's permisssion + if (itemToUpdate === item) { + updated.permission = level; + } + + itemsToUpdate.push(updated); + } + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} + +export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited || item === itemToDelete) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 41ae10d19e5..6e6a671685a 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,4 +1,4 @@ -import { FolderState } from 'app/types'; +import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { @@ -8,13 +8,15 @@ export const inititalState: FolderState = { url: '', canSave: false, hasChanged: false, - version: 0, + version: 1, + permissions: [], }; export const folderReducer = (state = inititalState, action: Action): FolderState => { switch (action.type) { case ActionTypes.LoadFolder: return { + ...state, ...action.payload, hasChanged: false, }; @@ -24,10 +26,45 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat title: action.payload, hasChanged: action.payload.trim().length > 0, }; + case ActionTypes.LoadFolderPermissions: + return { + ...state, + permissions: processAclItems(action.payload), + }; } return state; }; +function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} + export default { folder: folderReducer, }; diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts new file mode 100644 index 00000000000..d77fc4793fc --- /dev/null +++ b/public/app/types/acl.ts @@ -0,0 +1,60 @@ +export interface DashboardAclDTO { + id?: number; + dashboardId?: number; + userId?: number; + userLogin?: string; + userEmail?: string; + teamId?: number; + team?: string; + permission?: PermissionLevel; + permissionName?: string; + role?: string; + icon?: string; + inherited?: boolean; +} + +export interface DashboardAclUpdateDTO { + userId: number; + teamId: number; + role: string; + permission: PermissionLevel; +} + +export interface DashboardAcl { + id?: number; + dashboardId?: number; + userId?: number; + userLogin?: string; + userEmail?: string; + teamId?: number; + team?: string; + permission?: PermissionLevel; + permissionName?: string; + role?: string; + icon?: string; + name?: string; + inherited?: boolean; + sortRank?: number; +} + +export interface DashboardPermissionInfo { + value: PermissionLevel; + label: string; + description: string; +} + +export enum PermissionLevel { + View = 1, + Edit = 2, + Admin = 4, +} + +export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ + { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' }, + { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, + { + value: PermissionLevel.Admin, + label: 'Admin', + description: 'Can add/remove permissions and can add, edit and delete dashboards.', + }, +]; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts index 6fbe79cce8c..bbcae01fe59 100644 --- a/public/app/types/folder.ts +++ b/public/app/types/folder.ts @@ -1,3 +1,5 @@ +import { DashboardAcl } from './acl'; + export interface FolderDTO { id: number; uid: string; @@ -12,7 +14,14 @@ export interface FolderState { uid: string; title: string; url: string; - version: number; canSave: boolean; hasChanged: boolean; + version: number; + permissions: DashboardAcl[]; +} + +export interface FolderInfo { + id: number; + title: string; + url: string; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 52d1ba592c5..49f7fdb0f28 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,7 +2,8 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; -import { FolderDTO, FolderState } from './folder'; +import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl'; export { Team, @@ -22,6 +23,11 @@ export { UrlQueryValue, FolderDTO, FolderState, + FolderInfo, + DashboardAcl, + DashboardAclDTO, + DashboardAclUpdateDTO, + PermissionLevel, }; export interface StoreState { From 2926725bab128b7bced9ce4f03b8ae368107423c Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 1 May 2018 12:49:18 +0900 Subject: [PATCH 102/666] add annotation option to treat series value as timestamp --- .../datasource/prometheus/datasource.ts | 10 ++++- .../partials/annotations.editor.html | 11 +++++- .../prometheus/specs/datasource.test.ts | 39 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 9332a73caca..a60882e0470 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -487,15 +487,21 @@ export class PrometheusDatasource { .value(); for (const value of series.values) { - if (value[1] === '1') { + const valueIsTrue = value[1] === '1'; // e.g. ALERTS + if (valueIsTrue || annotation.useValueForTime) { const event = { annotation: annotation, - time: Math.floor(parseFloat(value[0])) * 1000, title: self.resultTransformer.renderTemplate(titleFormat, series.metric), tags: tags, text: self.resultTransformer.renderTemplate(textFormat, series.metric), }; + if (annotation.useValueForTime) { + event['time'] = Math.floor(parseFloat(value[1])); + } else { + event['time'] = Math.floor(parseFloat(value[0])) * 1000; + } + eventList.push(event); } } diff --git a/public/app/plugins/datasource/prometheus/partials/annotations.editor.html b/public/app/plugins/datasource/prometheus/partials/annotations.editor.html index 09ee52bda45..6e5982123fd 100644 --- a/public/app/plugins/datasource/prometheus/partials/annotations.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/annotations.editor.html @@ -10,7 +10,7 @@
    -
    Field formats
    +
    Field formats
    Title @@ -27,4 +27,13 @@
    + +
    Other options
    +
    +
    + + +
    +
    diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index ae91e6647e0..980574624ad 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -630,6 +630,45 @@ describe('PrometheusDatasource', () => { expect(results[0].text).toBe('testinstance'); expect(results[0].time).toBe(123 * 1000); }); + + it('should return annotation list with seriesValueAsTiemstamp', () => { + const options = { + annotation: { + expr: 'timestamp_seconds', + tagKeys: 'job', + titleFormat: '{{job}}', + textFormat: '{{instance}}', + useValueForTime: true, + }, + range: { + from: new Date('2014-04-10T05:20:10Z'), + to: new Date('2014-05-20T03:10:22Z'), + }, + }; + ctx.backendSrvMock.datasourceRequest.mockReturnValue( + Promise.resolve({ + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'timestamp_milliseconds', + instance: 'testinstance', + job: 'testjob', + }, + values: [[1443454528, '1500000000000']], + }, + ], + }, + }) + ); + ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + ctx.ds.annotationQuery(options).then(function (results) { + expect(results[0].time).toEqual(1500000000000); + ctx.backendSrvMock.datasourceRequest.mockReset(); + }); + }); }); describe('When resultFormat is table and instant = true', () => { From 3031c2e6fc1f907c0baa54756fe3aa8fa6935991 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 28 Aug 2018 01:58:53 +0900 Subject: [PATCH 103/666] fix test --- .../prometheus/specs/datasource.test.ts | 113 ++++++++---------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 980574624ad..1fa96d03fe7 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -581,7 +581,7 @@ describe('PrometheusDatasource', () => { describe('When performing annotationQuery', () => { let results; - const options = { + const options: any = { annotation: { expr: 'ALERTS{alertstate="firing"}', tagKeys: 'job', @@ -594,79 +594,60 @@ describe('PrometheusDatasource', () => { }, }; - beforeEach(async () => { - const response = { - status: 'success', + const response = { + status: 'success', + data: { data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - values: [[123, '1']], + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'ALERTS', + alertname: 'InstanceDown', + alertstate: 'firing', + instance: 'testinstance', + job: 'testjob', }, - ], - }, + values: [[123, '1']], + }, + ], }, - }; + }, + }; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + describe('not use useValueForTime', () => { + beforeEach(async () => { + options.annotation.useValueForTime = false; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); - await ctx.ds.annotationQuery(options).then(data => { - results = data; + await ctx.ds.annotationQuery(options).then(function (data) { + results = data; + }); + }); + + it('should return annotation list', () => { + expect(results.length).toBe(1); + expect(results[0].tags).toContain('testjob'); + expect(results[0].title).toBe('InstanceDown'); + expect(results[0].text).toBe('testinstance'); + expect(results[0].time).toBe(123 * 1000); }); }); - it('should return annotation list', () => { - expect(results.length).toBe(1); - expect(results[0].tags).toContain('testjob'); - expect(results[0].title).toBe('InstanceDown'); - expect(results[0].text).toBe('testinstance'); - expect(results[0].time).toBe(123 * 1000); - }); - it('should return annotation list with seriesValueAsTiemstamp', () => { - const options = { - annotation: { - expr: 'timestamp_seconds', - tagKeys: 'job', - titleFormat: '{{job}}', - textFormat: '{{instance}}', - useValueForTime: true, - }, - range: { - from: new Date('2014-04-10T05:20:10Z'), - to: new Date('2014-05-20T03:10:22Z'), - }, - }; - ctx.backendSrvMock.datasourceRequest.mockReturnValue( - Promise.resolve({ - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'timestamp_milliseconds', - instance: 'testinstance', - job: 'testjob', - }, - values: [[1443454528, '1500000000000']], - }, - ], - }, - }) - ); - ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); - ctx.ds.annotationQuery(options).then(function (results) { - expect(results[0].time).toEqual(1500000000000); - ctx.backendSrvMock.datasourceRequest.mockReset(); + describe('use useValueForTime', () => { + beforeEach(async () => { + options.annotation.useValueForTime = true; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.annotationQuery(options).then(function (data) { + results = data; + }); + }); + + it('should return annotation list', () => { + expect(results[0].time).toEqual(1); }); }); }); From dc08093f6c8077735fb78d24ca3791aa382ede28 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 13 Sep 2018 20:15:33 +0900 Subject: [PATCH 104/666] minor fix --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- .../datasource/prometheus/specs/datasource.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index a60882e0470..ca80b3760a7 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -585,7 +585,7 @@ export class PrometheusDatasource { } getTimeRange(): { start: number; end: number } { - let range = this.timeSrv.timeRange(); + const range = this.timeSrv.timeRange(); return { start: this.getPrometheusTime(range.from, false), end: this.getPrometheusTime(range.to, true), diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 1fa96d03fe7..eef2bbd56b6 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -619,9 +619,9 @@ describe('PrometheusDatasource', () => { beforeEach(async () => { options.annotation.useValueForTime = false; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.annotationQuery(options).then(function (data) { + await ctx.ds.annotationQuery(options).then(data => { results = data; }); }); @@ -639,9 +639,9 @@ describe('PrometheusDatasource', () => { beforeEach(async () => { options.annotation.useValueForTime = true; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.annotationQuery(options).then(function (data) { + await ctx.ds.annotationQuery(options).then(data => { results = data; }); }); From d35eca333feb144d841693552842848402973644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 13:56:09 +0200 Subject: [PATCH 105/666] folder permissions in redux --- .../PermissionList/AddPermission.tsx | 142 ++++++++++++++++++ .../PermissionList/PermissionList.tsx | 3 +- .../PermissionList/PermissionListItem.tsx | 4 +- .../features/folders/FolderPermissions.tsx | 36 +++-- .../folders/FolderSettingsPage.test.tsx | 1 + public/app/features/folders/state/actions.ts | 31 +++- public/app/features/folders/state/reducers.ts | 3 +- public/app/types/acl.ts | 27 ++++ public/app/types/index.ts | 5 - 9 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 public/app/core/components/PermissionList/AddPermission.tsx diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx new file mode 100644 index 00000000000..76bcfac4780 --- /dev/null +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -0,0 +1,142 @@ +import React, { Component } from 'react'; +import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; +import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; +import { + dashboardPermissionLevels, + dashboardAclTargets, + AclTarget, + PermissionLevel, + NewDashboardAclItem, +} from 'app/types/acl'; + +export interface Props { + onAddPermission: (item: NewDashboardAclItem) => void; + onCancel: () => void; +} + +class AddPermissions extends Component { + constructor(props) { + super(props); + this.state = this.getCleanState(); + } + + getCleanState() { + return { + userId: 0, + teamId: 0, + role: '', + type: AclTarget.Team, + permission: PermissionLevel.View, + }; + } + + onTypeChanged = evt => { + this.setState({ type: evt.target.value as AclTarget }); + }; + + onUserSelected = (user: User) => { + this.setState({ + userId: user ? user.id : 0, + teamId: 0, + }); + }; + + onTeamSelected = (team: Team) => { + this.setState({ + userId: 0, + teamId: team ? team.id : 0, + }); + }; + + onPermissionChanged = (permission: OptionWithDescription) => { + this.setState({ permission: permission.value }); + }; + + onSubmit = async evt => { + evt.preventDefault(); + await this.props.onAddPermission(this.state); + this.setState(this.getCleanState()); + }; + + isValid() { + switch (this.state.type) { + case AclTarget.Team: + return this.state.teamId > 0; + case AclTarget.User: + return this.state.userId > 0; + } + return true; + } + + render() { + const { onCancel } = this.props; + const newItem = this.state; + const pickerClassName = 'width-20'; + const isValid = this.isValid(); + + return ( +
    + +
    +
    Add Permission For
    +
    +
    +
    + +
    +
    + + {newItem.type === AclTarget.User ? ( +
    + +
    + ) : null} + + {newItem.type === AclTarget.Team ? ( +
    + +
    + ) : null} + +
    + +
    + +
    + +
    +
    +
    +
    + ); + } +} + +export default AddPermissions; diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx index 29f810a4358..772baa0c274 100644 --- a/public/app/core/components/PermissionList/PermissionList.tsx +++ b/public/app/core/components/PermissionList/PermissionList.tsx @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; import PermissionsListItem from './PermissionListItem'; import DisabledPermissionsListItem from './DisabledPermissionListItem'; -import { DashboardAcl, FolderInfo } from 'app/types'; +import { FolderInfo } from 'app/types'; +import { DashboardAcl } from 'app/types/acl'; export interface Props { items: DashboardAcl[]; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx index 3e5aaf3ab2f..b846f98a063 100644 --- a/public/app/core/components/PermissionList/PermissionListItem.tsx +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { dashboardPermissionLevels } from 'app/types/acl'; -import { DashboardAcl, FolderInfo, PermissionLevel } from 'app/types'; +import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl'; +import { FolderInfo } from 'app/types'; const setClassNameHelper = inherited => { return inherited ? 'gf-form-disabled' : ''; diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 25de5f8be16..c86137a55ce 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,17 +1,23 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; -import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types'; -import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; +import { + getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, + addFolderPermission, +} from './state/actions'; import { getLoadingNav } from './state/navModel'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; +import AddPermission from 'app/core/components/PermissionList/AddPermission'; +import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; export interface Props { navModel: NavModel; @@ -21,13 +27,14 @@ export interface Props { getFolderPermissions: typeof getFolderPermissions; updateFolderPermission: typeof updateFolderPermission; removeFolderPermission: typeof removeFolderPermission; + addFolderPermission: typeof addFolderPermission; } export interface State { isAdding: boolean; } -export class FolderPermissions extends Component { +export class FolderPermissions extends PureComponent { constructor(props) { super(props); @@ -53,6 +60,14 @@ export class FolderPermissions extends Component { this.props.updateFolderPermission(item, level); }; + onAddPermission = (newItem: NewDashboardAclItem) => { + return this.props.addFolderPermission(newItem); + }; + + onCancelAddPermission = () => { + this.setState({ isAdding: false }); + }; + render() { const { navModel, folder } = this.props; const { isAdding } = this.state; @@ -61,8 +76,7 @@ export class FolderPermissions extends Component { return ; } - const dashboardId = folder.id; - const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; + const folderInfo = { title: folder.title, url: folder.url, id: folder.id }; return (
    @@ -78,6 +92,9 @@ export class FolderPermissions extends Component { Add Permission
    + + + { url: 'url', hasChanged: false, version: 1, + permissions: [], }, getFolderByUid: jest.fn(), setFolderTitle: jest.fn(), diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 29940cc7a31..4f15f813a68 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,14 +1,15 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; import { - FolderDTO, - FolderState, DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO, -} from 'app/types'; + NewDashboardAclItem, +} from 'app/types/acl'; + import { updateNavIndex, updateLocation } from 'app/core/actions'; import { buildNavModel } from './navModel'; import appEvents from 'app/core/app_events'; @@ -140,3 +141,27 @@ export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult< await dispatch(getFolderPermissions(folder.uid)); }; } + +export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + itemsToUpdate.push({ + userId: newItem.userId, + teamId: newItem.teamId, + role: item.role, + permission: item.permission, + }); + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 6e6a671685a..9b73312790c 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,4 +1,5 @@ -import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types'; +import { FolderState } from 'app/types'; +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index d77fc4793fc..feca062b355 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -43,12 +43,39 @@ export interface DashboardPermissionInfo { description: string; } +export interface NewDashboardAclItem { + teamId: number; + userId: number; + role: string; + permission: PermissionLevel; + type: AclTarget; +} + export enum PermissionLevel { View = 1, Edit = 2, Admin = 4, } +export enum AclTarget { + Team = 'team', + User = 'user', + Viewer = 'viewer', + Editor = 'editor', +} + +export interface AclTargetInfo { + value: AclTarget; + text: string; +} + +export const dashboardAclTargets: AclTargetInfo[] = [ + { value: AclTarget.Team, text: 'Team' }, + { value: AclTarget.User, text: 'User' }, + { value: AclTarget.Viewer, text: 'Everyone With Viewer Role' }, + { value: AclTarget.Editor, text: 'Everyone With Editor Role' }, +]; + export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' }, { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 49f7fdb0f28..6f052c7c503 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -3,7 +3,6 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; -import { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl'; export { Team, @@ -24,10 +23,6 @@ export { FolderDTO, FolderState, FolderInfo, - DashboardAcl, - DashboardAclDTO, - DashboardAclUpdateDTO, - PermissionLevel, }; export interface StoreState { From f2edb82e797d38ec8a53ee2bffd0c044a6571f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:10:51 +0200 Subject: [PATCH 106/666] Folder pages to redux (#13235) * creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * wip: began folder to redux migration * team settings * actions for group sync * wip: progress on redux folder store * wip: folder to redux * wip: folder settings page to redux progress * mobx -> redux: major progress on folder migration * redux: moved folders to it's own features folder * fix: added loading nav states * fix: gofmt issues * wip: working on reducer test * fix: added reducer test --- public/app/containers/ContainerProps.ts | 14 -- .../ManageDashboards/FolderSettings.test.tsx | 84 --------- .../ManageDashboards/FolderSettings.tsx | 160 ------------------ public/app/core/reducers/location.ts | 4 +- public/app/core/selectors/navModel.ts | 10 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ---------- .../folders}/FolderPermissions.tsx | 56 +++--- .../folders/FolderSettingsPage.test.tsx | 55 ++++++ .../features/folders/FolderSettingsPage.tsx | 105 ++++++++++++ .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++ public/app/features/folders/state/actions.ts | 67 ++++++++ public/app/features/folders/state/navModel.ts | 53 ++++++ .../features/folders/state/reducers.test.ts | 42 +++++ public/app/features/folders/state/reducers.ts | 33 ++++ public/app/features/teams/TeamPages.tsx | 8 +- public/app/features/teams/state/actions.ts | 117 +++---------- public/app/features/teams/state/navModel.ts | 67 ++++++++ public/app/routes/routes.ts | 6 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/stores/RootStore/RootStore.ts | 2 - public/app/stores/configureStore.ts | 2 + public/app/types/folder.ts | 18 ++ public/app/types/index.ts | 4 + yarn.lock | 30 +--- 26 files changed, 656 insertions(+), 578 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.test.tsx delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts rename public/app/{containers/ManageDashboards => features/folders}/FolderPermissions.tsx (60%) create mode 100644 public/app/features/folders/FolderSettingsPage.test.tsx create mode 100644 public/app/features/folders/FolderSettingsPage.tsx create mode 100644 public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap create mode 100644 public/app/features/folders/state/actions.ts create mode 100644 public/app/features/folders/state/navModel.ts create mode 100644 public/app/features/folders/state/reducers.test.ts create mode 100644 public/app/features/folders/state/reducers.ts create mode 100644 public/app/features/teams/state/navModel.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts create mode 100644 public/app/types/folder.ts diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index ce09b992f80..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/containers/ManageDashboards/FolderSettings.test.tsx deleted file mode 100644 index bed3d569bcc..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; - -describe('FolderSettings', () => { - let wrapper; - let page; - - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ - id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', - canSave: true, - version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, - }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); -}); diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

    Loading

    ; - } - - return ( -
    - -
    -

    Folder Settings

    - -
    -
    -
    - - -
    -
    - - -
    - -
    -
    -
    - ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 60% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 072908d2b8e..512927c24e6 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,25 +1,38 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +40,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { - return

    Loading

    ; + if (folder.id === 0) { + return ; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
    - +

    Folder Permissions

    @@ -77,4 +82,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx new file mode 100644 index 00000000000..3680fa9a197 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel } from 'app/types'; +import { shallow } from 'enzyme'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + url: 'url', + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { + id: 1, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: true, + version: 1, + }, + }); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx new file mode 100644 index 00000000000..1eb7ccafc65 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; + +export interface Props { + navModel: NavModel; + folderUid: string; + folder: FolderState; + getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; +} + +export class FolderSettingsPage extends PureComponent { + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; + + render() { + const { navModel, folder } = this.props; + + return ( +
    + +
    +

    Folder Settings

    + +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    + ); + } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
    + +
    +

    + Folder Settings +

    +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +`; + +exports[`Render should render component 1`] = ` +
    + +
    +

    + Folder Settings +

    +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +`; diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts new file mode 100644 index 00000000000..5d153b2fb8a --- /dev/null +++ b/public/app/features/folders/state/actions.ts @@ -0,0 +1,67 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', + SetFolderTitle = 'SET_FOLDER_TITLE', + SaveFolder = 'SAVE_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export interface SetFolderTitleAction { + type: ActionTypes.SetFolderTitle; + payload: string; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ + type: ActionTypes.SetFolderTitle, + payload: newTitle, +}); + +export type Action = LoadFolderAction | SetFolderTitleAction; + +type ThunkResult = ThunkAction; + + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); + }; +} + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..e6ef763d019 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,53 @@ +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..ff37f13f97f --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,42 @@ +import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; +import { inititalState, folderReducer } from './reducers'; + +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + +describe('folder reducer', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); + + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); +}); diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts new file mode 100644 index 00000000000..41ae10d19e5 --- /dev/null +++ b/public/app/features/folders/state/reducers.ts @@ -0,0 +1,33 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = { + id: 0, + uid: 'loading', + title: 'loading', + url: '', + canSave: false, + hasChanged: false, + version: 0, +}; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + hasChanged: false, + }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: action.payload.trim().length > 0, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,148 +90,73 @@ export function loadTeams(): ThunkResult { }; } -function buildNavModel(team: Team): NavModelItem { - const navModel = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: false, - icon: 'gicon gicon-team', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `team-settings-${team.id}`, - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (config.buildInfo.isEnterprise) { - navModel.children.push({ - active: false, - icon: 'fa fa-fw fa-refresh', - id: `team-groupsync-${team.id}`, - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - return navModel; -} - export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); }; } export function loadTeamMembers(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/members`) - .then(response => { - dispatch(teamMembersLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + dispatch(teamMembersLoaded(response)); }; } export function addTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/members`, { userId: id }) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + dispatch(loadTeamMembers()); }; } export function removeTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/members/${id}`) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + dispatch(loadTeamMembers()); }; } export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv() - .put(`/api/teams/${team.id}`, { - name, - email, - }) - .then(() => { - dispatch(loadTeam(team.id)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + dispatch(loadTeam(team.id)); }; } export function loadTeamGroups(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/groups`) - .then(response => { - dispatch(teamGroupsLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + dispatch(teamGroupsLoaded(response)); }; } export function addTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups()); }; } export function removeTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/groups/${groupId}`) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 519008d70f5..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; -import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...foldersReducers, }); export let store; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts new file mode 100644 index 00000000000..6fbe79cce8c --- /dev/null +++ b/public/app/types/folder.ts @@ -0,0 +1,18 @@ +export interface FolderDTO { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 92bcdb32836..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; +import { FolderDTO, FolderState } from './folder'; export { Team, @@ -19,6 +20,8 @@ export { NavIndex, UrlQueryMap, UrlQueryValue, + FolderDTO, + FolderState, }; export interface StoreState { @@ -27,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: From bae560717d6f1c7694b0817a997ec278c83ee14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:23:43 +0200 Subject: [PATCH 107/666] fix: fixed tslint issue introduced in recent prometheus PR merge --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 9332a73caca..5674e1353a1 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -579,7 +579,7 @@ export class PrometheusDatasource { } getTimeRange(): { start: number; end: number } { - let range = this.timeSrv.timeRange(); + const range = this.timeSrv.timeRange(); return { start: this.getPrometheusTime(range.from, false), end: this.getPrometheusTime(range.to, true), From e1a1da9064b7ffd7b22764a99a800b04f7cbc747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:31:12 +0200 Subject: [PATCH 108/666] fix: add folder permission fix --- public/app/features/folders/state/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 4f15f813a68..df81acb91eb 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -157,8 +157,8 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult Date: Thu, 13 Sep 2018 15:15:42 +0200 Subject: [PATCH 109/666] renames PartialMatch to MatchAny --- pkg/api/annotations.go | 22 +++++++++---------- pkg/services/annotations/annotations.go | 2 +- pkg/services/sqlstore/annotation.go | 2 +- pkg/services/sqlstore/annotation_test.go | 10 ++++----- .../plugins/datasource/grafana/datasource.ts | 2 +- .../grafana/partials/annotations.editor.html | 10 ++++----- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index eec07bb9f81..242b5531f51 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -14,17 +14,17 @@ import ( func GetAnnotations(c *m.ReqContext) Response { query := &annotations.ItemQuery{ - From: c.QueryInt64("from"), - To: c.QueryInt64("to"), - OrgId: c.OrgId, - UserId: c.QueryInt64("userId"), - AlertId: c.QueryInt64("alertId"), - DashboardId: c.QueryInt64("dashboardId"), - PanelId: c.QueryInt64("panelId"), - Limit: c.QueryInt64("limit"), - Tags: c.QueryStrings("tags"), - Type: c.Query("type"), - PartialMatch: c.QueryBool("partialMatch"), + From: c.QueryInt64("from"), + To: c.QueryInt64("to"), + OrgId: c.OrgId, + UserId: c.QueryInt64("userId"), + AlertId: c.QueryInt64("alertId"), + DashboardId: c.QueryInt64("dashboardId"), + PanelId: c.QueryInt64("panelId"), + Limit: c.QueryInt64("limit"), + Tags: c.QueryStrings("tags"), + Type: c.Query("type"), + MatchAny: c.QueryBool("matchAny"), } repo := annotations.GetRepository() diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index daea43863f4..60a92aa897a 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -21,7 +21,7 @@ type ItemQuery struct { RegionId int64 `json:"regionId"` Tags []string `json:"tags"` Type string `json:"type"` - PartialMatch bool `json:"partialMatch"` + MatchAny bool `json:"matchAny"` Limit int64 `json:"limit"` } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 6e25ce432f3..ceafaaad0e3 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -211,7 +211,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ) `, strings.Join(keyValueFilters, " OR ")) - if query.PartialMatch { + if query.MatchAny { sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery)) } else { sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index 4c31e0442c8..d3459527e7d 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -199,11 +199,11 @@ func TestAnnotations(t *testing.T) { Convey("Should find two annotations using partial match", func() { items, err := repo.Find(&annotations.ItemQuery{ - OrgId: 1, - From: 1, - To: 25, - PartialMatch: true, - Tags: []string{"rollback", "deploy"}, + OrgId: 1, + From: 1, + To: 25, + MatchAny: true, + Tags: []string{"rollback", "deploy"}, }) So(err, ShouldBeNil) diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 4ddfa8df40d..3bf772d160c 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -40,7 +40,7 @@ class GrafanaDatasource { to: options.range.to.valueOf(), limit: options.annotation.limit, tags: options.annotation.tags, - partialMatch: options.annotation.partialMatch, + matchAny: options.annotation.matchAny, }; if (options.annotation.type === 'dashboard') { diff --git a/public/app/plugins/datasource/grafana/partials/annotations.editor.html b/public/app/plugins/datasource/grafana/partials/annotations.editor.html index ba68a08cefd..e5a67d6a7dc 100644 --- a/public/app/plugins/datasource/grafana/partials/annotations.editor.html +++ b/public/app/plugins/datasource/grafana/partials/annotations.editor.html @@ -26,11 +26,11 @@
    -
    From c7fdea1dfb3e245a031023c1b440c52f9af78092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 16:00:02 +0200 Subject: [PATCH 110/666] wip: dashboard permissions to redux --- public/app/core/actions/permissions.ts | 24 ---- public/app/core/angular_wrappers.ts | 2 - public/app/core/reducers/processsAclItems.ts | 31 +++++ public/app/core/utils/acl.ts | 31 +++++ .../DashboardPermissions.tsx | 106 ++++++++++++++++ public/app/features/dashboard/all.ts | 6 + .../app/features/dashboard/state/actions.ts | 115 ++++++++++++++++++ .../app/features/dashboard/state/reducers.ts | 22 ++++ public/app/features/folders/state/reducers.ts | 32 +---- public/app/stores/configureStore.ts | 2 + public/app/types/dashboard.ts | 5 + public/app/types/index.ts | 2 + 12 files changed, 321 insertions(+), 57 deletions(-) delete mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/reducers/processsAclItems.ts create mode 100644 public/app/core/utils/acl.ts create mode 100644 public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx create mode 100644 public/app/features/dashboard/state/actions.ts create mode 100644 public/app/features/dashboard/state/reducers.ts create mode 100644 public/app/types/dashboard.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts deleted file mode 100644 index 2b07b7145dd..00000000000 --- a/public/app/core/actions/permissions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DashboardAcl } from '../../types'; - -export enum ActionTypes { - LoadFolderPermissions = 'LoadFolderPermissions', -} - -export interface LoadFolderPermissionsAction { - type: ActionTypes.LoadFolderPermissions; - payload: DashboardAcl[]; -} - -export type Action = LoadFolderPermissions; - -export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({ - type: ActionTypes.LoadFolderPermissions, - payload: items, -}); - -export function getFolderPermissions(uid: string): ThunkResult { - return async dispatch => { - const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); - dispatch(loadFolderPermissions(permissions)); - }; -} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 18e9d8dbd84..6974d40aac8 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; -import DashboardPermissions from './components/Permissions/DashboardPermissions'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -18,5 +17,4 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); } diff --git a/public/app/core/reducers/processsAclItems.ts b/public/app/core/reducers/processsAclItems.ts new file mode 100644 index 00000000000..57578d6b2d8 --- /dev/null +++ b/public/app/core/reducers/processsAclItems.ts @@ -0,0 +1,31 @@ +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; + +export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} diff --git a/public/app/core/utils/acl.ts b/public/app/core/utils/acl.ts new file mode 100644 index 00000000000..57578d6b2d8 --- /dev/null +++ b/public/app/core/utils/acl.ts @@ -0,0 +1,31 @@ +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; + +export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx new file mode 100644 index 00000000000..ad7d9c7f504 --- /dev/null +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -0,0 +1,106 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import SlideDown from 'app/core/components/Animations/SlideDown'; +import { StoreState, FolderInfo } from 'app/types'; +import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; +import { getDashboardPermissions } from '../state/actions'; +import PermissionList from 'app/core/components/PermissionList/PermissionList'; +import AddPermission from 'app/core/components/PermissionList/AddPermission'; +import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import { store } from 'app/stores/configureStore'; + +export interface Props { + dashboardId: number; + folder?: FolderInfo; + getDashboardPermissions: typeof getDashboardPermissions; + permissions: DashboardAcl[]; +} + +export interface State { + isAdding: boolean; +} + +export class DashboardPermissions extends PureComponent { + constructor(props) { + super(props); + + this.state = { + isAdding: false, + }; + } + + componentDidMount() { + this.props.getDashboardPermissions(this.props.dashboardId); + } + + onOpenAddPermissions = () => { + this.setState({ isAdding: true }); + }; + + onRemoveItem = (item: DashboardAcl) => { + // this.props.removeFolderPermission(item); + }; + + onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { + // this.props.updateFolderPermission(item, level); + }; + + onAddPermission = (newItem: NewDashboardAclItem) => { + // return this.props.addFolderPermission(newItem); + }; + + onCancelAddPermission = () => { + this.setState({ isAdding: false }); + }; + + render() { + const { permissions, folder } = this.props; + const { isAdding } = this.state; + console.log('DashboardPermissions', this.props); + + return ( +
    +
    +
    +

    Permissions

    + + + +
    + +
    +
    + + + + +
    + ); + } +} + +function connectWithStore(WrappedComponent, ...args) { + const ConnectedWrappedComponent = connect(...args)(WrappedComponent); + return props => { + return ; + }; +} + +const mapStateToProps = (state: StoreState) => ({ + permissions: state.dashboard.permissions, +}); + +const mapDispatchToProps = { + getDashboardPermissions, +}; + +export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index adb665c47b5..817ed83aaa0 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -30,6 +30,12 @@ import './settings/settings'; import './panellinks/module'; import './dashlinks/module'; +// angular wrappers +import { react2AngularDirective } from 'app/core/utils/react2angular'; +import DashboardPermissions from './DashboardPermissions/DashboardPermissions'; + +react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']); + import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts new file mode 100644 index 00000000000..b1d25d1f57f --- /dev/null +++ b/public/app/features/dashboard/state/actions.ts @@ -0,0 +1,115 @@ +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; + +import { + DashboardAcl, + DashboardAclDTO, + PermissionLevel, + DashboardAclUpdateDTO, + NewDashboardAclItem, +} from 'app/types/acl'; + +export enum ActionTypes { + LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS', +} + +export interface LoadDashboardPermissionsAction { + type: ActionTypes.LoadDashboardPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadDashboardPermissionsAction; + +type ThunkResult = ThunkAction; + +export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({ + type: ActionTypes.LoadDashboardPermissions, + payload: items, +}); + +export function getDashboardPermissions(id: number): ThunkResult { + return async dispatch => { + const permissions = await getBackendSrv().get(`/api/dashboards/id/${id}/permissions`); + dispatch(loadDashboardPermissions(permissions)); + }; +} + +function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { + return { + userId: item.userId, + teamId: item.teamId, + role: item.role, + permission: item.permission, + }; +} + +export function updateDashboardPermission( + dashboardId: number, + itemToUpdate: DashboardAcl, + level: PermissionLevel +): ThunkResult { + return async (dispatch, getStore) => { + const { dashboard } = getStore(); + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited) { + continue; + } + + const updated = toUpdateItem(itemToUpdate); + + // if this is the item we want to update, update it's permisssion + if (itemToUpdate === item) { + updated.permission = level; + } + + itemsToUpdate.push(updated); + } + + await getBackendSrv().post(`/api/dashboard/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} + +export function removeDashboardPermission(dashboardId: number, itemToDelete: DashboardAcl): ThunkResult { + return async (dispatch, getStore) => { + const dashboard = getStore().dashboard; + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited || item === itemToDelete) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} + +export function addDashboardPermission(dashboardId: number, newItem: NewDashboardAclItem): ThunkResult { + return async (dispatch, getStore) => { + const { dashboard } = getStore(); + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + itemsToUpdate.push({ + userId: newItem.userId, + teamId: newItem.teamId, + role: newItem.role, + permission: newItem.permission, + }); + + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts new file mode 100644 index 00000000000..5100529d973 --- /dev/null +++ b/public/app/features/dashboard/state/reducers.ts @@ -0,0 +1,22 @@ +import { DashboardState } from 'app/types'; +import { Action, ActionTypes } from './actions'; +import { processAclItems } from 'app/core/utils/acl'; + +export const inititalState: DashboardState = { + permissions: [], +}; + +export const dashboardReducer = (state = inititalState, action: Action): DashboardState => { + switch (action.type) { + case ActionTypes.LoadDashboardPermissions: + return { + ...state, + permissions: processAclItems(action.payload), + }; + } + return state; +}; + +export default { + dashboard: dashboardReducer, +}; diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 9b73312790c..4560c999659 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,6 +1,6 @@ import { FolderState } from 'app/types'; -import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; import { Action, ActionTypes } from './actions'; +import { processAclItems } from 'app/core/utils/acl'; export const inititalState: FolderState = { id: 0, @@ -36,36 +36,6 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat return state; }; -function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { - return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); -} - -function processAclItem(dto: DashboardAclDTO): DashboardAcl { - const item = dto as DashboardAcl; - - item.sortRank = 0; - if (item.userId > 0) { - item.name = item.userLogin; - item.sortRank = 10; - } else if (item.teamId > 0) { - item.name = item.team; - item.sortRank = 20; - } else if (item.role) { - item.icon = 'fa fa-fw fa-street-view'; - item.name = item.role; - item.sortRank = 30; - if (item.role === 'Editor') { - item.sortRank += 1; - } - } - - if (item.inherited) { - item.sortRank += 100; - } - - return item; -} - export default { folder: folderReducer, }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index e06317853f8..8f6cf25043d 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -5,12 +5,14 @@ import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; +import dashboardReducers from 'app/features/dashboard/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, ...foldersReducers, + ...dashboardReducers, }); export let store; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts new file mode 100644 index 00000000000..d33405c985e --- /dev/null +++ b/public/app/types/dashboard.ts @@ -0,0 +1,5 @@ +import { DashboardAcl } from './acl'; + +export interface DashboardState { + permissions: DashboardAcl[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 6f052c7c503..f2fe165a863 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -3,6 +3,7 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { DashboardState } from './dashboard'; export { Team, @@ -32,4 +33,5 @@ export interface StoreState { teams: TeamsState; team: TeamState; folder: FolderState; + dashboard: DashboardState; } From bff350166ef1f37e59625eb6b0d0dc287f36b6ef Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 13 Sep 2018 14:36:16 +0200 Subject: [PATCH 111/666] disabling internal metrics disables /metric endpoint but we will still keep sending metrics to graphite closes #10638 --- pkg/api/http_server.go | 4 ++++ pkg/metrics/service.go | 1 - pkg/metrics/settings.go | 5 ----- pkg/setting/setting.go | 3 +++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 0de63ce5e08..432d6a18369 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -233,6 +233,10 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { } func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { + if !hs.Cfg.MetricsEndpointEnabled { + return + } + if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" { return } diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go index ec38e0acfec..3d7fa6a1269 100644 --- a/pkg/metrics/service.go +++ b/pkg/metrics/service.go @@ -28,7 +28,6 @@ func init() { type InternalMetricsService struct { Cfg *setting.Cfg `inject:""` - enabled bool intervalSeconds int64 graphiteCfg *graphitebridge.Config } diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index 58b84a7192f..048e4134690 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -16,13 +16,8 @@ func (im *InternalMetricsService) readSettings() error { return fmt.Errorf("Unable to find metrics config section %v", err) } - im.enabled = section.Key("enabled").MustBool(false) im.intervalSeconds = section.Key("interval_seconds").MustInt64(10) - if !im.enabled { - return nil - } - if err := im.parseGraphiteSettings(); err != nil { return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index fb23a192a85..1a253b9b238 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -203,6 +203,8 @@ type Cfg struct { DisableBruteForceLoginProtection bool TempDataLifetime time.Duration + + MetricsEndpointEnabled bool } type CommandLineArgs struct { @@ -659,6 +661,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.ImagesDir = filepath.Join(DataPath, "png") cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) + cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) analytics := iniFile.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) From 1d66f9a42c697e829b71e8d468c5e6d233a9d912 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 12 Sep 2018 19:27:21 +0200 Subject: [PATCH 112/666] anonymous usage stats for authentication types --- pkg/metrics/metrics.go | 20 +++++++++++++++++++- pkg/metrics/metrics_test.go | 29 ++++++++++++++++++++++++++--- pkg/metrics/service.go | 3 ++- pkg/metrics/settings.go | 4 ++++ pkg/social/social.go | 26 ++++++++++++++++++++++++-- 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index dcdfbf124e1..e2cdb5656b0 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -350,7 +350,7 @@ func getEdition() string { } } -func sendUsageStats() { +func sendUsageStats(oauthProviders map[string]bool) { if !setting.ReportingEnabled { return } @@ -450,6 +450,24 @@ func sendUsageStats() { metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count } + authTypes := map[string]bool{} + authTypes["anonymous"] = setting.AnonymousEnabled + authTypes["basic_auth"] = setting.BasicAuthEnabled + authTypes["ldap"] = setting.LdapEnabled + authTypes["auth_proxy"] = setting.AuthProxyEnabled + + for provider, enabled := range oauthProviders { + authTypes["oauth_"+provider] = enabled + } + + for authType, enabled := range authTypes { + enabledValue := 0 + if enabled { + enabledValue = 1 + } + metrics["stats.auth_enabled."+authType+".count"] = enabledValue + } + out, _ := json.MarshalIndent(report, "", " ") data := bytes.NewBuffer(out) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 9fbfd0c26a2..43739221f1e 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -147,11 +147,19 @@ func TestMetrics(t *testing.T) { })) usageStatsURL = ts.URL - sendUsageStats() + oauthProviders := map[string]bool{ + "github": true, + "gitlab": true, + "google": true, + "generic_oauth": true, + "grafana_com": true, + } + + sendUsageStats(oauthProviders) Convey("Given reporting not enabled and sending usage stats", func() { setting.ReportingEnabled = false - sendUsageStats() + sendUsageStats(oauthProviders) Convey("Should not gather stats or call http endpoint", func() { So(getSystemStatsQuery, ShouldBeNil) @@ -164,8 +172,13 @@ func TestMetrics(t *testing.T) { Convey("Given reporting enabled and sending usage stats", func() { setting.ReportingEnabled = true setting.BuildVersion = "5.0.0" + setting.AnonymousEnabled = true + setting.BasicAuthEnabled = true + setting.LdapEnabled = true + setting.AuthProxyEnabled = true + wg.Add(1) - sendUsageStats() + sendUsageStats(oauthProviders) Convey("Should gather stats and call http endpoint", func() { if waitTimeout(&wg, 2*time.Second) { @@ -220,6 +233,16 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.alert_notifiers.slack.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.alert_notifiers.webhook.count").MustInt(), ShouldEqual, 2) + + So(metrics.Get("stats.auth_enabled.anonymous.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.basic_auth.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.ldap.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.auth_proxy.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_github.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_gitlab.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1) }) }) diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go index ec38e0acfec..3e66f8686c1 100644 --- a/pkg/metrics/service.go +++ b/pkg/metrics/service.go @@ -31,6 +31,7 @@ type InternalMetricsService struct { enabled bool intervalSeconds int64 graphiteCfg *graphitebridge.Config + oauthProviders map[string]bool } func (im *InternalMetricsService) Init() error { @@ -61,7 +62,7 @@ func (im *InternalMetricsService) Run(ctx context.Context) error { for { select { case <-onceEveryDayTick.C: - sendUsageStats() + sendUsageStats(im.oauthProviders) case <-everyMinuteTicker.C: updateTotalStats() case <-ctx.Done(): diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index 58b84a7192f..ed4b9fe09de 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/social" + "github.com/grafana/grafana/pkg/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" @@ -27,6 +29,8 @@ func (im *InternalMetricsService) readSettings() error { return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } + im.oauthProviders = social.GetOAuthProviders(im.Cfg) + return nil } diff --git a/pkg/social/social.go b/pkg/social/social.go index e96b67fe031..721070ab789 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -49,14 +49,13 @@ func (e *Error) Error() string { var ( SocialBaseUrl = "/login/" SocialMap = make(map[string]SocialConnector) + allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} ) func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} - for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) info := &setting.OAuthInfo{ @@ -184,3 +183,26 @@ func NewOAuthService() { } } } + +// GetOAuthProviders returns available oauth providers and if they're enabled or not +var GetOAuthProviders = func(cfg *setting.Cfg) map[string]bool { + result := map[string]bool{} + + if cfg == nil || cfg.Raw == nil { + return result + } + + for _, name := range allOauthes { + if name == "grafananet" { + name = "grafana_com" + } + + sec := cfg.Raw.Section("auth." + name) + if sec == nil { + continue + } + result[name] = sec.Key("enabled").MustBool() + } + + return result +} From 0254a29e35e9404ccc0ccfdc06af8dcac0cd508f Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Thu, 13 Sep 2018 16:51:00 +0200 Subject: [PATCH 113/666] Interpolate $__interval in backend for alerting with sql datasources (#13156) add support for interpolate $__interval and $__interval_ms in sql datasources --- pkg/tsdb/elasticsearch/client/client.go | 2 +- pkg/tsdb/influxdb/query.go | 3 +- pkg/tsdb/interval.go | 4 ++ pkg/tsdb/mssql/macros.go | 22 ++--------- pkg/tsdb/mssql/mssql_test.go | 40 ++++++++++++++++++++ pkg/tsdb/mysql/macros.go | 23 ++---------- pkg/tsdb/mysql/mysql_test.go | 40 ++++++++++++++++++++ pkg/tsdb/postgres/macros.go | 26 +++---------- pkg/tsdb/postgres/postgres_test.go | 40 ++++++++++++++++++++ pkg/tsdb/sql_engine.go | 50 ++++++++++++++++++++++++- pkg/tsdb/sql_engine_test.go | 31 +++++++++++++++ 11 files changed, 218 insertions(+), 63 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index dff626a79eb..78973b3faa6 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -138,7 +138,7 @@ func (c *baseClientImpl) encodeBatchRequests(requests []*multiRequest) ([]byte, } body := string(reqBody) - body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) + body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Milliseconds(), 10), -1) body = strings.Replace(body, "$__interval", r.interval.Text, -1) payload.WriteString(body + "\n") diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 0637a5bbb44..7cb8f0ecd82 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -4,7 +4,6 @@ import ( "fmt" "strconv" "strings" - "time" "regexp" @@ -34,7 +33,7 @@ func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) { res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), -1) res = strings.Replace(res, "$interval", interval.Text, -1) - res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) + res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1) res = strings.Replace(res, "$__interval", interval.Text, -1) return res, nil } diff --git a/pkg/tsdb/interval.go b/pkg/tsdb/interval.go index 49904f27a37..fd6adee39d7 100644 --- a/pkg/tsdb/interval.go +++ b/pkg/tsdb/interval.go @@ -49,6 +49,10 @@ func NewIntervalCalculator(opt *IntervalOptions) *intervalCalculator { return calc } +func (i *Interval) Milliseconds() int64 { + return i.Value.Nanoseconds() / int64(time.Millisecond) +} + func (ic *intervalCalculator) Calculate(timerange *TimeRange, minInterval time.Duration) Interval { to := timerange.MustGetTo().UnixNano() from := timerange.MustGetFrom().UnixNano() diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index caba043e7b6..9303712a480 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -13,12 +13,13 @@ const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type msSqlMacroEngine struct { + *tsdb.SqlMacroEngineBase timeRange *tsdb.TimeRange query *tsdb.Query } func newMssqlMacroEngine() tsdb.SqlMacroEngine { - return &msSqlMacroEngine{} + return &msSqlMacroEngine{SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase()} } func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -27,7 +28,7 @@ func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa rExp, _ := regexp.Compile(sExpr) var macroError error - sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") @@ -47,23 +48,6 @@ func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa return sql, nil } -func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + repl(groups) - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 30d1da3bda1..f9525fc37ac 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -35,6 +35,11 @@ func TestMSSQL(t *testing.T) { return x, nil } + origInterpolate := tsdb.Interpolate + tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + return sql, nil + } + endpoint, err := newMssqlQueryEndpoint(&models.DataSource{ JsonData: simplejson.New(), SecureJsonData: securejsondata.SecureJsonData{}, @@ -47,6 +52,7 @@ func TestMSSQL(t *testing.T) { Reset(func() { sess.Close() tsdb.NewXormEngine = origXormEngine + tsdb.Interpolate = origInterpolate }) Convey("Given a table with different native data types", func() { @@ -295,6 +301,40 @@ func TestMSSQL(t *testing.T) { }) + Convey("When doing a metric query using timeGroup and $__interval", func() { + mockInterpolate := tsdb.Interpolate + tsdb.Interpolate = origInterpolate + + Reset(func() { + tsdb.Interpolate = mockInterpolate + }) + + Convey("Should replace $__interval", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + DataSource: &models.DataSource{}, + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY $__timeGroup(time, $__interval) ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 ORDER BY 1") + }) + }) + Convey("When doing a metric query using timeGroup with float fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 0dabdd7c283..0f1c4fcaf2c 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -9,17 +9,17 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -//const rsString = `(?:"([^"]*)")`; const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type mySqlMacroEngine struct { + *tsdb.SqlMacroEngineBase timeRange *tsdb.TimeRange query *tsdb.Query } func newMysqlMacroEngine() tsdb.SqlMacroEngine { - return &mySqlMacroEngine{} + return &mySqlMacroEngine{SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase()} } func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -28,7 +28,7 @@ func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa rExp, _ := regexp.Compile(sExpr) var macroError error - sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") @@ -48,23 +48,6 @@ func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa return sql, nil } -func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + repl(groups) - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__timeEpoch", "__time": diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index ca6df8e360e..13d9040a738 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -42,6 +42,11 @@ func TestMySQL(t *testing.T) { return x, nil } + origInterpolate := tsdb.Interpolate + tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + return sql, nil + } + endpoint, err := newMysqlQueryEndpoint(&models.DataSource{ JsonData: simplejson.New(), SecureJsonData: securejsondata.SecureJsonData{}, @@ -54,6 +59,7 @@ func TestMySQL(t *testing.T) { Reset(func() { sess.Close() tsdb.NewXormEngine = origXormEngine + tsdb.Interpolate = origInterpolate }) Convey("Given a table with different native data types", func() { @@ -295,6 +301,40 @@ func TestMySQL(t *testing.T) { }) + Convey("When doing a metric query using timeGroup and $__interval", func() { + mockInterpolate := tsdb.Interpolate + tsdb.Interpolate = origInterpolate + + Reset(func() { + tsdb.Interpolate = mockInterpolate + }) + + Convey("Should replace $__interval", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + DataSource: &models.DataSource{}, + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT UNIX_TIMESTAMP(time) DIV 60 * 60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1") + }) + }) + Convey("When doing a metric query using timeGroup with value fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 0a2ea1d2af6..16f4adb68a6 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -9,18 +9,21 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -//const rsString = `(?:"([^"]*)")`; const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type postgresMacroEngine struct { + *tsdb.SqlMacroEngineBase timeRange *tsdb.TimeRange query *tsdb.Query timescaledb bool } func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine { - return &postgresMacroEngine{timescaledb: timescaledb} + return &postgresMacroEngine{ + SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase(), + timescaledb: timescaledb, + } } func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -29,7 +32,7 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim rExp, _ := regexp.Compile(sExpr) var macroError error - sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { // detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility // if there is a ',' directly after the macro call $__timeGroup is probably used @@ -66,23 +69,6 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim return sql, nil } -func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + repl(groups) - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 4e05f676682..fc1a5f34253 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -43,6 +43,11 @@ func TestPostgres(t *testing.T) { return x, nil } + origInterpolate := tsdb.Interpolate + tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + return sql, nil + } + endpoint, err := newPostgresQueryEndpoint(&models.DataSource{ JsonData: simplejson.New(), SecureJsonData: securejsondata.SecureJsonData{}, @@ -55,6 +60,7 @@ func TestPostgres(t *testing.T) { Reset(func() { sess.Close() tsdb.NewXormEngine = origXormEngine + tsdb.Interpolate = origInterpolate }) Convey("Given a table with different native data types", func() { @@ -222,6 +228,40 @@ func TestPostgres(t *testing.T) { } }) + Convey("When doing a metric query using timeGroup and $__interval", func() { + mockInterpolate := tsdb.Interpolate + tsdb.Interpolate = origInterpolate + + Reset(func() { + tsdb.Interpolate = mockInterpolate + }) + + Convey("Should replace $__interval", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + DataSource: &models.DataSource{}, + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1") + }) + }) + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 454853c7cc8..18e02e328d1 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "math" + "regexp" "strconv" "strings" "sync" @@ -43,6 +44,8 @@ var engineCache = engineCacheType{ versions: make(map[int64]int), } +var sqlIntervalCalculator = NewIntervalCalculator(nil) + var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) { return xorm.NewEngine(driverName, connectionString) } @@ -126,7 +129,15 @@ func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, queryResult := &QueryResult{Meta: simplejson.New(), RefId: query.RefId} result.Results[query.RefId] = queryResult - rawSQL, err := e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL) + // global substitutions + rawSQL, err := Interpolate(query, tsdbQuery.TimeRange, rawSQL) + if err != nil { + queryResult.Error = err + continue + } + + // datasource specific substitutions + rawSQL, err = e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL) if err != nil { queryResult.Error = err continue @@ -163,6 +174,20 @@ func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, return result, nil } +// global macros/substitutions for all sql datasources +var Interpolate = func(query *Query, timeRange *TimeRange, sql string) (string, error) { + minInterval, err := GetIntervalFrom(query.DataSource, query.Model, time.Second*60) + if err != nil { + return sql, nil + } + interval := sqlIntervalCalculator.Calculate(timeRange, minInterval) + + sql = strings.Replace(sql, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1) + sql = strings.Replace(sql, "$__interval", interval.Text, -1) + + return sql, nil +} + func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error { columnNames, err := rows.Columns() columnCount := len(columnNames) @@ -589,3 +614,26 @@ func SetupFillmode(query *Query, interval time.Duration, fillmode string) error return nil } + +type SqlMacroEngineBase struct{} + +func NewSqlMacroEngineBase() *SqlMacroEngineBase { + return &SqlMacroEngineBase{} +} + +func (m *SqlMacroEngineBase) ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { + result := "" + lastIndex := 0 + + for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { + groups := []string{} + for i := 0; i < len(v); i += 2 { + groups = append(groups, str[v[i]:v[i+1]]) + } + + result += str[lastIndex:v[0]] + repl(groups) + lastIndex = v[1] + } + + return result + str[lastIndex:] +} diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go index 854734fac31..05b8a51ae6f 100644 --- a/pkg/tsdb/sql_engine_test.go +++ b/pkg/tsdb/sql_engine_test.go @@ -5,6 +5,8 @@ import ( "time" "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -14,6 +16,35 @@ func TestSqlEngine(t *testing.T) { dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC) earlyDt := time.Date(1970, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC) + Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { + from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC) + to := from.Add(5 * time.Minute) + timeRange := NewFakeTimeRange("5m", "now", to) + query := &Query{DataSource: &models.DataSource{}, Model: simplejson.New()} + + Convey("interpolate $__interval", func() { + sql, err := Interpolate(query, timeRange, "select $__interval ") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 1m ") + }) + + Convey("interpolate $__interval in $__timeGroup", func() { + sql, err := Interpolate(query, timeRange, "select $__timeGroupAlias(time,$__interval)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select $__timeGroupAlias(time,1m)") + }) + + Convey("interpolate $__interval_ms", func() { + sql, err := Interpolate(query, timeRange, "select $__interval_ms ") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 60000 ") + }) + + }) + Convey("Given row values with time.Time as time columns", func() { var nilPointer *time.Time From e33b2d5fce9a3184a9f33fcd7df219d9c011f478 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 13 Sep 2018 17:29:37 +0200 Subject: [PATCH 114/666] changelog: add notes about closing #11555 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a9ffbec9c..939d75f3a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Minor * **Alerting**: Link to view full size image in Microsoft Teams alert notifier [#13121](https://github.com/grafana/grafana/issues/13121), thx [@holiiveira](https://github.com/holiiveira) +* **Postgres/MySQL/MSSQL**: Add support for replacing $__interval and $__interval_ms in alert queries [#11555](https://github.com/grafana/grafana/issues/11555), thx [@svenklemm](https://github.com/svenklemm) # 5.3.0-beta1 (2018-09-06) From 379227c75d23f2dfb6280dbc775b6f2d1e45af37 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 13 Sep 2018 17:52:28 +0200 Subject: [PATCH 115/666] Updated CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 939d75f3a53..fd2357fbfa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ * **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon) * **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc) +* **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212) # 5.3.0 (unreleased) From fbfcc622698a951b39a557f6be96d4aace2027c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 21:08:41 +0200 Subject: [PATCH 116/666] fix: added loading screen error scenario (#13256) --- public/views/index.template.html | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/public/views/index.template.html b/public/views/index.template.html index ad55f810d0e..606db2c769e 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -145,6 +145,29 @@ } } + /* Fail info */ + .preloader__text--fail { + display: none; + } + + /* stop logo animation */ + .preloader--done .preloader__bounce, + .preloader--done .preloader__logo { + animation-name: none; + display: none; + } + + .preloader--done .preloader__logo, + .preloader--done .preloader__text { + display: none; + color: #ff5705 !important; + font-size: 15px; + } + + .preloader--done .preloader__text--fail { + display: block; + } + [ng\:cloak], [ng-cloak], .ng-cloak { @@ -159,6 +182,20 @@
    Loading Grafana
    +
    +

    + If your seeing this Grafana has failed to load its application files +
    +
    +

    +

    + 1. This could be caused by your reverse proxy settings.

    + 2. If you host grafana under subpath make sure your grafana.ini root_path setting includes subpath

    + 3. If you have a local dev build make sure you build frontend using: npm run dev, npm run watch, or npm run + build

    + 4. Sometimes restarting grafana-server can help
    +

    +
    @@ -236,6 +273,10 @@ // insert it at the end of the head in a legacy-friendly manner document.head.insertBefore(myCSS, document.head.childNodes[document.head.childNodes.length - 1].nextSibling); + // switch loader to show all has loaded + window.onload = function() { + document.getElementsByClassName("preloader")[0].className = "preloader preloader--done"; + }; [[if .GoogleTagManagerId]] From ec5aa332ac8780b0b89084298b3dc4b86ea4d1df Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 13 Sep 2018 21:09:21 +0200 Subject: [PATCH 117/666] removes old unused examples (#13260) Just wanted to reduce the amount of files/folders in root --- examples/README.md | 5 - examples/alerting-dashboard.json | 800 --------- examples/alerting-multiple-alerts.json | 2216 ------------------------ 3 files changed, 3021 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/alerting-dashboard.json delete mode 100644 examples/alerting-multiple-alerts.json diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 75f1f9a9a86..00000000000 --- a/examples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## Example plugin implementations - -datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource) -app: [example-app](https://github.com/grafana/example-app) -panel: [grafana-piechart-panel](https://github.com/grafana/piechart-panel) diff --git a/examples/alerting-dashboard.json b/examples/alerting-dashboard.json deleted file mode 100644 index 744460d7847..00000000000 --- a/examples/alerting-dashboard.json +++ /dev/null @@ -1,800 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_GRAPHITE", - "label": "graphite", - "description": "", - "type": "datasource", - "pluginId": "graphite", - "pluginName": "Graphite" - } - ], - "__requires": [ - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "3.1.0" - }, - { - "type": "datasource", - "id": "graphite", - "name": "Graphite", - "version": "1.0.0" - } - ], - "id": null, - "title": "Alerting example", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 355 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 1, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - }, - { - "refId": "B", - "target": "aliasByNode(scale(statsd.$apa.counters.session_start.*.count, 10), 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 355 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 2, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 1 - ], - "type": "lt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "count" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "No datapoints", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 20, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 1, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Count datapoints", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "Row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "lt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Alert below value", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 17, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "lt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Alert below value", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 10, - 80 - ], - "type": "outside_range" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Alert is outside range", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 18, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "lt", - "value": 10 - }, - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 80 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Alert is outside range", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 60, - 80 - ], - "type": "within_range" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Alert is within range", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 19, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 60 - }, - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "lt", - "value": 80 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Alert is within range", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - } - ], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [ - { - "current": { - "text": "fakesite", - "value": "fakesite" - }, - "datasource": null, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "apa", - "options": [ - { - "selected": true, - "text": "fakesite", - "value": "fakesite" - } - ], - "query": "fakesite", - "refresh": 0, - "type": "custom" - } - ] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 15, - "links": [], - "gnetId": null -} \ No newline at end of file diff --git a/examples/alerting-multiple-alerts.json b/examples/alerting-multiple-alerts.json deleted file mode 100644 index e6e729ecc06..00000000000 --- a/examples/alerting-multiple-alerts.json +++ /dev/null @@ -1,2216 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_GRAPHITE", - "label": "graphite", - "description": "", - "type": "datasource", - "pluginId": "graphite", - "pluginName": "Graphite" - } - ], - "__requires": [ - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "3.1.0" - }, - { - "type": "datasource", - "id": "graphite", - "name": "Graphite", - "version": "1.0.0" - } - ], - "id": null, - "title": "Dashboard with many alerts", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 1, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 5, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 6, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 8, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "Row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 2, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 3, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 4, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 7, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 9, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 10, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 11, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 12, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 13, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 14, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 15, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 16, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - }, - { - "title": "New row", - "height": "250px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "Alert below value", - "error": false, - "span": 3, - "editable": true, - "type": "graph", - "isNew": true, - "id": 17, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "refId": "A" - } - ], - "datasource": "${DS_GRAPHITE}", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "alert": { - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "lt", - "params": [ - 20 - ] - } - } - ], - "severity": "critical", - "frequency": "60s", - "handler": 1, - "notifications": [], - "name": "Alert below value", - "enabled": true - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - "thresholds": [ - { - "value": 20, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - } - ], - "links": [] - }, - { - "title": "Alert is outside range", - "error": false, - "span": 3, - "editable": true, - "type": "graph", - "isNew": true, - "id": 18, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "refId": "A" - } - ], - "datasource": "${DS_GRAPHITE}", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "alert": { - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "outside_range", - "params": [ - 10, - 80 - ] - } - } - ], - "severity": "critical", - "frequency": "10s", - "handler": 1, - "notifications": [], - "name": "Alert is outside range", - "enabled": true - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - "thresholds": [ - { - "value": 10, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - }, - { - "value": 80, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - } - ], - "links": [] - }, - { - "title": "Alert is within range", - "error": false, - "span": 3, - "editable": true, - "type": "graph", - "isNew": true, - "id": 19, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "refId": "A" - } - ], - "datasource": "${DS_GRAPHITE}", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "alert": { - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "within_range", - "params": [ - 60, - 80 - ] - } - } - ], - "severity": "critical", - "frequency": "10s", - "handler": 1, - "notifications": [], - "name": "Alert is within range", - "enabled": true - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - "thresholds": [ - { - "value": 60, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - }, - { - "value": 80, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - } - ], - "links": [] - } - ] - } - ], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 50, - "links": [], - "gnetId": null -} \ No newline at end of file From 124b21a6aa3da3c184e8d634422756fa85beb7e3 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 13 Sep 2018 17:19:51 -0400 Subject: [PATCH 118/666] use pluginName consistently when upgrading plugins --- pkg/cmd/grafana-cli/commands/upgrade_command.go | 6 +++--- pkg/cmd/grafana-cli/services/services.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index 355ccab3d1c..396371d3577 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -16,7 +16,7 @@ func upgradeCommand(c CommandLine) error { return err } - v, err2 := s.GetPlugin(localPlugin.Id, c.RepoDirectory()) + v, err2 := s.GetPlugin(pluginName, c.RepoDirectory()) if err2 != nil { return err2 @@ -24,9 +24,9 @@ func upgradeCommand(c CommandLine) error { if ShouldUpgrade(localPlugin.Info.Version, v) { s.RemoveInstalledPlugin(pluginsDir, pluginName) - return InstallPlugin(localPlugin.Id, "", c) + return InstallPlugin(pluginName, "", c) } - logger.Infof("%s %s is up to date \n", color.GreenString("✔"), localPlugin.Id) + logger.Infof("%s %s is up to date \n", color.GreenString("✔"), pluginName) return nil } diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index b4e50ac84df..338975bc130 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -63,7 +63,7 @@ func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { var data m.PluginRepo err = json.Unmarshal(body, &data) if err != nil { - logger.Info("Failed to unmarshal graphite response error:", err) + logger.Info("Failed to unmarshal plugin repo response error:", err) return m.PluginRepo{}, err } @@ -140,7 +140,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { var data m.Plugin err = json.Unmarshal(body, &data) if err != nil { - logger.Info("Failed to unmarshal graphite response error:", err) + logger.Info("Failed to unmarshal plugin repo response error:", err) return m.Plugin{}, err } From 74912dca8d48298eb5783d46bd4c0de641d39567 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 13 Sep 2018 15:41:49 -0700 Subject: [PATCH 119/666] Fix gauge display accuracy for "percent (0.0-1.0)" The "Decimals" value was incorrectly applied to the metric value used to calculate the gauge display in addition to the text value (so a "Decimals" value of "1" turns "45.1%" into "50%" in the gauge display even though the label still correctly says "45.1%"). --- public/app/plugins/panel/singlestat/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c44b09449be..eafa3cf23f4 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -544,7 +544,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { elem.append(plotCanvas); const plotSeries = { - data: [[0, data.valueRounded]], + data: [[0, data.value]], }; $.plot(plotCanvas, [plotSeries], options); From 7bb010926196cc9c5f25a0c850baac13834d89c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 07:47:33 +0200 Subject: [PATCH 120/666] feat: dashboard permissions are working --- .../PermissionList/AddPermission.tsx | 3 +- .../PermissionsInfo.tsx | 0 .../Permissions/AddPermissions.test.tsx | 90 ------------ .../components/Permissions/AddPermissions.tsx | 128 ------------------ .../Permissions/DashboardPermissions.tsx | 71 ---------- .../DisabledPermissionsListItem.tsx | 43 ------ .../core/components/Permissions/FolderInfo.ts | 5 - .../components/Permissions/Permissions.tsx | 91 ------------- .../Permissions/PermissionsList.tsx | 64 --------- .../Permissions/PermissionsListItem.tsx | 91 ------------- .../DashboardPermissions.tsx | 23 +++- .../app/features/dashboard/state/actions.ts | 2 +- .../features/folders/FolderPermissions.tsx | 2 +- .../features/folders/state/reducers.test.ts | 92 ++++++++++--- public/app/types/acl.ts | 24 ++-- public/app/types/index.ts | 5 + scripts/webpack/webpack.common.js | 3 + 17 files changed, 117 insertions(+), 620 deletions(-) rename public/app/core/components/{Permissions => PermissionList}/PermissionsInfo.tsx (100%) delete mode 100644 public/app/core/components/Permissions/AddPermissions.test.tsx delete mode 100644 public/app/core/components/Permissions/AddPermissions.tsx delete mode 100644 public/app/core/components/Permissions/DashboardPermissions.tsx delete mode 100644 public/app/core/components/Permissions/DisabledPermissionsListItem.tsx delete mode 100644 public/app/core/components/Permissions/FolderInfo.ts delete mode 100644 public/app/core/components/Permissions/Permissions.tsx delete mode 100644 public/app/core/components/Permissions/PermissionsList.tsx delete mode 100644 public/app/core/components/Permissions/PermissionsListItem.tsx diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 76bcfac4780..73bffdaf97b 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -8,6 +8,7 @@ import { AclTarget, PermissionLevel, NewDashboardAclItem, + OrgRole, } from 'app/types/acl'; export interface Props { @@ -25,7 +26,7 @@ class AddPermissions extends Component { return { userId: 0, teamId: 0, - role: '', + role: OrgRole.Viewer, type: AclTarget.Team, permission: PermissionLevel.View, }; diff --git a/public/app/core/components/Permissions/PermissionsInfo.tsx b/public/app/core/components/PermissionList/PermissionsInfo.tsx similarity index 100% rename from public/app/core/components/Permissions/PermissionsInfo.tsx rename to public/app/core/components/PermissionList/PermissionsInfo.tsx diff --git a/public/app/core/components/Permissions/AddPermissions.test.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx deleted file mode 100644 index c6d1ab381b8..00000000000 --- a/public/app/core/components/Permissions/AddPermissions.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import AddPermissions from './AddPermissions'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { getBackendSrv } from 'app/core/services/backend_srv'; - -jest.mock('app/core/services/backend_srv', () => ({ - getBackendSrv: () => { - return { - get: () => { - return Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - ]); - }, - post: jest.fn(() => Promise.resolve({})), - }; - }, -})); - -describe('AddPermissions', () => { - let wrapper; - let store; - let instance; - const backendSrv: any = getBackendSrv(); - - beforeAll(() => { - store = RootStore.create({}, { backendSrv: backendSrv }); - wrapper = shallow(); - instance = wrapper.instance(); - return store.permissions.load(1, true, false); - }); - - describe('when permission for a user is added', () => { - it('should save permission to db', () => { - const evt = { - target: { - value: 'User', - }, - }; - const userItem = { - id: 2, - login: 'user2', - }; - - instance.onTypeChanged(evt); - instance.onUserSelected(userItem); - - wrapper.update(); - - expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false); - - wrapper.find('form').simulate('submit', { preventDefault() {} }); - - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - }); - - describe('when permission for team is added', () => { - it('should save permission to db', () => { - const evt = { - target: { - value: 'Group', - }, - }; - - const teamItem = { - id: 2, - name: 'ug1', - }; - - instance.onTypeChanged(evt); - instance.onTeamSelected(teamItem); - - wrapper.update(); - - expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false); - - wrapper.find('form').simulate('submit', { preventDefault() {} }); - - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - }); - - afterEach(() => { - backendSrv.post.mockClear(); - }); -}); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx deleted file mode 100644 index 289e27aa731..00000000000 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; -import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; -import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; -import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -export interface Props { - permissions: any; -} - -@observer -class AddPermissions extends Component { - constructor(props) { - super(props); - } - - componentWillMount() { - const { permissions } = this.props; - permissions.resetNewType(); - } - - onTypeChanged = evt => { - const { value } = evt.target; - const { permissions } = this.props; - - permissions.setNewType(value); - }; - - onUserSelected = (user: User) => { - const { permissions } = this.props; - if (!user) { - permissions.newItem.setUser(null, null); - return; - } - return permissions.newItem.setUser(user.id, user.login, user.avatarUrl); - }; - - onTeamSelected = (team: Team) => { - const { permissions } = this.props; - if (!team) { - permissions.newItem.setTeam(null, null); - return; - } - return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl); - }; - - onPermissionChanged = (permission: OptionWithDescription) => { - const { permissions } = this.props; - return permissions.newItem.setPermission(permission.value); - }; - - resetNewType() { - const { permissions } = this.props; - return permissions.resetNewType(); - } - - onSubmit = evt => { - evt.preventDefault(); - const { permissions } = this.props; - permissions.addStoreItem(); - }; - - render() { - const { permissions } = this.props; - const newItem = permissions.newItem; - const pickerClassName = 'width-20'; - - const isValid = newItem.isValid(); - - return ( -
    - -
    -
    Add Permission For
    -
    -
    -
    - -
    -
    - - {newItem.type === 'User' ? ( -
    - -
    - ) : null} - - {newItem.type === 'Group' ? ( -
    - -
    - ) : null} - -
    - -
    - -
    - -
    -
    -
    -
    - ); - } -} - -export default AddPermissions; diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx deleted file mode 100644 index 38a646b2473..00000000000 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { store } from 'app/stores/store'; -import Permissions from 'app/core/components/Permissions/Permissions'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; -import AddPermissions from 'app/core/components/Permissions/AddPermissions'; -import SlideDown from 'app/core/components/Animations/SlideDown'; -import { FolderInfo } from './FolderInfo'; - -export interface Props { - dashboardId: number; - folder?: FolderInfo; - backendSrv: any; -} - -@observer -class DashboardPermissions extends Component { - permissions: any; - - constructor(props) { - super(props); - this.handleAddPermission = this.handleAddPermission.bind(this); - this.permissions = store.permissions; - } - - handleAddPermission() { - this.permissions.toggleAddPermissions(); - } - - componentWillUnmount() { - this.permissions.hideAddPermissions(); - } - - render() { - const { dashboardId, folder, backendSrv } = this.props; - - return ( -
    -
    -
    -

    Permissions

    - - - -
    - -
    -
    - - - - -
    - ); - } -} - -export default DashboardPermissions; diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx deleted file mode 100644 index d65595dae66..00000000000 --- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { Component } from 'react'; -import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -export interface Props { - item: any; -} - -export default class DisabledPermissionListItem extends Component { - render() { - const { item } = this.props; - - return ( - - - - - - {item.name} - (Role) - - - Can - -
    - {}} - value={item.permission} - disabled={true} - className={'gf-form-input--form-dropdown-right'} - /> -
    - - - - - - ); - } -} diff --git a/public/app/core/components/Permissions/FolderInfo.ts b/public/app/core/components/Permissions/FolderInfo.ts deleted file mode 100644 index d4a6020bb71..00000000000 --- a/public/app/core/components/Permissions/FolderInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface FolderInfo { - id: number; - title: string; - url: string; -} diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx deleted file mode 100644 index d17899c891f..00000000000 --- a/public/app/core/components/Permissions/Permissions.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react'; -import PermissionsList from './PermissionsList'; -import { observer } from 'mobx-react'; -import { FolderInfo } from './FolderInfo'; - -export interface DashboardAcl { - id?: number; - dashboardId?: number; - userId?: number; - userLogin?: string; - userEmail?: string; - teamId?: number; - team?: string; - permission?: number; - permissionName?: string; - role?: string; - icon?: string; - name?: string; - inherited?: boolean; - sortRank?: number; -} - -export interface Props { - dashboardId: number; - folderInfo?: FolderInfo; - permissions?: any; - isFolder: boolean; - backendSrv: any; -} - -@observer -class Permissions extends Component { - constructor(props) { - super(props); - const { dashboardId, isFolder, folderInfo } = this.props; - this.permissionChanged = this.permissionChanged.bind(this); - this.typeChanged = this.typeChanged.bind(this); - this.removeItem = this.removeItem.bind(this); - this.loadStore(dashboardId, isFolder, folderInfo && folderInfo.id === 0); - } - - loadStore(dashboardId, isFolder, isInRoot = false) { - return this.props.permissions.load(dashboardId, isFolder, isInRoot); - } - - permissionChanged(index: number, permission: number, permissionName: string) { - const { permissions } = this.props; - permissions.updatePermissionOnIndex(index, permission, permissionName); - } - - removeItem(index: number) { - const { permissions } = this.props; - permissions.removeStoreItem(index); - } - - resetNewType() { - const { permissions } = this.props; - permissions.resetNewType(); - } - - typeChanged(evt) { - const { value } = evt.target; - const { permissions, dashboardId } = this.props; - - if (value === 'Viewer' || value === 'Editor') { - permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId); - this.resetNewType(); - return; - } - - permissions.setNewType(value); - } - - render() { - const { permissions, folderInfo } = this.props; - - return ( -
    - -
    - ); - } -} - -export default Permissions; diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx deleted file mode 100644 index 7e64de012e4..00000000000 --- a/public/app/core/components/Permissions/PermissionsList.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component } from 'react'; -import PermissionsListItem from './PermissionsListItem'; -import DisabledPermissionsListItem from './DisabledPermissionsListItem'; -import { observer } from 'mobx-react'; -import { FolderInfo } from './FolderInfo'; - -export interface Props { - permissions: any[]; - removeItem: any; - permissionChanged: any; - fetching: boolean; - folderInfo?: FolderInfo; -} - -@observer -class PermissionsList extends Component { - render() { - const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props; - - return ( - - - - {permissions.map((item, idx) => { - return ( - - ); - })} - {fetching === true && permissions.length < 1 ? ( - - - - ) : null} - - {fetching === false && permissions.length < 1 ? ( - - - - ) : null} - -
    - Loading permissions... -
    - No permissions are set. Will only be accessible by admins. -
    - ); - } -} - -export default PermissionsList; diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx deleted file mode 100644 index a17aa8c04df..00000000000 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { observer } from 'mobx-react'; -import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -const setClassNameHelper = inherited => { - return inherited ? 'gf-form-disabled' : ''; -}; - -function ItemAvatar({ item }) { - if (item.userAvatarUrl) { - return ; - } - if (item.teamAvatarUrl) { - return ; - } - if (item.role === 'Editor') { - return ; - } - - return ; -} - -function ItemDescription({ item }) { - if (item.userId) { - return (User); - } - if (item.teamId) { - return (Team); - } - return (Role); -} - -export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => { - const handleRemoveItem = evt => { - evt.preventDefault(); - removeItem(itemIndex); - }; - - const handleChangePermission = permissionOption => { - permissionChanged(itemIndex, permissionOption.value, permissionOption.label); - }; - - const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; - - return ( - - - - - - {item.name} - - - {item.inherited && - folderInfo && ( - - Inherited from folder{' '} - - {folderInfo.title} - {' '} - - )} - {inheritedFromRoot && Default Permission} - - Can - -
    - -
    - - - {!item.inherited ? ( - - - - ) : ( - - )} - - - ); -}); diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx index ad7d9c7f504..6ea7ba12721 100644 --- a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -4,17 +4,25 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; -import { getDashboardPermissions } from '../state/actions'; +import { + getDashboardPermissions, + addDashboardPermission, + removeDashboardPermission, + updateDashboardPermission, +} from '../state/actions'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; import { store } from 'app/stores/configureStore'; export interface Props { dashboardId: number; folder?: FolderInfo; - getDashboardPermissions: typeof getDashboardPermissions; permissions: DashboardAcl[]; + getDashboardPermissions: typeof getDashboardPermissions; + updateDashboardPermission: typeof updateDashboardPermission; + removeDashboardPermission: typeof removeDashboardPermission; + addDashboardPermission: typeof addDashboardPermission; } export interface State { @@ -39,15 +47,15 @@ export class DashboardPermissions extends PureComponent { }; onRemoveItem = (item: DashboardAcl) => { - // this.props.removeFolderPermission(item); + this.props.removeDashboardPermission(this.props.dashboardId, item); }; onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { - // this.props.updateFolderPermission(item, level); + this.props.updateDashboardPermission(this.props.dashboardId, item, level); }; onAddPermission = (newItem: NewDashboardAclItem) => { - // return this.props.addFolderPermission(newItem); + return this.props.addDashboardPermission(this.props.dashboardId, newItem); }; onCancelAddPermission = () => { @@ -101,6 +109,9 @@ const mapStateToProps = (state: StoreState) => ({ const mapDispatchToProps = { getDashboardPermissions, + addDashboardPermission, + removeDashboardPermission, + updateDashboardPermission, }; export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index b1d25d1f57f..82333817b2b 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -68,7 +68,7 @@ export function updateDashboardPermission( itemsToUpdate.push(updated); } - await getBackendSrv().post(`/api/dashboard/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); await dispatch(getDashboardPermissions(dashboardId)); }; } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index c86137a55ce..176e270038b 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -17,7 +17,7 @@ import { import { getLoadingNav } from './state/navModel'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; export interface Props { navModel: NavModel; diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index ff37f13f97f..be45c643e77 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from './actions'; -import { FolderDTO } from 'app/types'; +import { FolderDTO, OrgRole, PermissionLevel, FolderState } from 'app/types'; import { inititalState, folderReducer } from './reducers'; function getTestFolder(): FolderDTO { @@ -14,29 +14,85 @@ function getTestFolder(): FolderDTO { } describe('folder reducer', () => { - it('should load folder and set hasChanged to false', () => { - const folder = getTestFolder(); + describe('loadFolder', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); - const action: Action = { - type: ActionTypes.LoadFolder, - payload: folder, - }; + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; - const state = folderReducer(inititalState, action); + const state = folderReducer(inititalState, action); - expect(state.hasChanged).toEqual(false); - expect(state.title).toEqual('test folder'); + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); }); - it('should set title', () => { - const action: Action = { - type: ActionTypes.SetFolderTitle, - payload: 'new title', - }; + describe('detFolderTitle', () => { + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; - const state = folderReducer(inititalState, action); + const state = folderReducer(inititalState, action); - expect(state.hasChanged).toEqual(true); - expect(state.title).toEqual('new title'); + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); + }); + + describe('loadFolderPermissions', () => { + let state: FolderState; + + beforeEach(() => { + const action: Action = { + type: ActionTypes.LoadFolderPermissions, + payload: [ + { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, + { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, + { + id: 4, + dashboardId: 10, + permission: PermissionLevel.View, + teamId: 1, + team: 'MyTestTeam', + inherited: true, + }, + { + id: 5, + dashboardId: 1, + permission: PermissionLevel.View, + userId: 1, + userLogin: 'MyTestUser', + }, + { + id: 6, + dashboardId: 1, + permission: PermissionLevel.Edit, + teamId: 2, + team: 'MyTestTeam2', + }, + ], + }; + + state = folderReducer(inititalState, action); + }); + + it('should add permissions to state', async () => { + expect(state.permissions.length).toBe(5); + expect(state.permissions.length).toBe(5); + }); + + it('should be sorted by sort rank and alphabetically', async () => { + expect(state.permissions[0].name).toBe('MyTestTeam'); + expect(state.permissions[0].dashboardId).toBe(10); + expect(state.permissions[1].name).toBe('Editor'); + expect(state.permissions[2].name).toBe('Viewer'); + expect(state.permissions[3].name).toBe('MyTestTeam2'); + expect(state.permissions[4].name).toBe('MyTestUser'); + }); }); }); diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index feca062b355..d6589f8bf40 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -1,3 +1,9 @@ +export enum OrgRole { + Viewer = 'Viewer', + Editor = 'Editor', + Admin = 'Admin', +} + export interface DashboardAclDTO { id?: number; dashboardId?: number; @@ -7,8 +13,7 @@ export interface DashboardAclDTO { teamId?: number; team?: string; permission?: PermissionLevel; - permissionName?: string; - role?: string; + role?: OrgRole; icon?: string; inherited?: boolean; } @@ -16,7 +21,7 @@ export interface DashboardAclDTO { export interface DashboardAclUpdateDTO { userId: number; teamId: number; - role: string; + role: OrgRole; permission: PermissionLevel; } @@ -29,8 +34,7 @@ export interface DashboardAcl { teamId?: number; team?: string; permission?: PermissionLevel; - permissionName?: string; - role?: string; + role?: OrgRole; icon?: string; name?: string; inherited?: boolean; @@ -46,7 +50,7 @@ export interface DashboardPermissionInfo { export interface NewDashboardAclItem { teamId: number; userId: number; - role: string; + role: OrgRole; permission: PermissionLevel; type: AclTarget; } @@ -58,10 +62,10 @@ export enum PermissionLevel { } export enum AclTarget { - Team = 'team', - User = 'user', - Viewer = 'viewer', - Editor = 'editor', + Team = 'Team', + User = 'User', + Viewer = 'Viewer', + Editor = 'Editor', } export interface AclTargetInfo { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index f2fe165a863..8fcfcc7e88d 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -4,6 +4,7 @@ import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './loc import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; import { DashboardState } from './dashboard'; +import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; export { Team, @@ -24,6 +25,10 @@ export { FolderDTO, FolderState, FolderInfo, + DashboardState, + DashboardAcl, + OrgRole, + PermissionLevel, }; export interface StoreState { diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index eea3ebbed2d..d367016c4fb 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -24,6 +24,9 @@ module.exports = { path.resolve('node_modules') ], }, + stats: { + warningsFilter: /export .* was not found in/ + }, node: { fs: 'empty', }, From 776d81189f2db5e49577d47b853b1a3b46ca0a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 07:52:17 +0200 Subject: [PATCH 121/666] test: added simple dashboard reducer test --- public/app/features/folders/state/reducers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index be45c643e77..72e97f39562 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -83,7 +83,6 @@ describe('folder reducer', () => { it('should add permissions to state', async () => { expect(state.permissions.length).toBe(5); - expect(state.permissions.length).toBe(5); }); it('should be sorted by sort rank and alphabetically', async () => { From 331be7d47a9c2f252336c189925a35e8cb2a05d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 08:25:35 +0200 Subject: [PATCH 122/666] fix: add permission fixes --- .../PermissionList/AddPermission.tsx | 26 ++++++++++++------- .../features/dashboard/state/reducers.test.ts | 24 +++++++++++++++++ public/app/types/acl.ts | 2 +- 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard/state/reducers.test.ts diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 73bffdaf97b..77ac6953b74 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -26,28 +26,34 @@ class AddPermissions extends Component { return { userId: 0, teamId: 0, - role: OrgRole.Viewer, type: AclTarget.Team, permission: PermissionLevel.View, }; } onTypeChanged = evt => { - this.setState({ type: evt.target.value as AclTarget }); + const type = evt.target.value as AclTarget; + + switch (type) { + case AclTarget.User: + case AclTarget.Team: + this.setState({ type: type, userId: 0, teamId: 0, role: undefined }); + break; + case AclTarget.Editor: + this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Editor }); + break; + case AclTarget.Viewer: + this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Viewer }); + break; + } }; onUserSelected = (user: User) => { - this.setState({ - userId: user ? user.id : 0, - teamId: 0, - }); + this.setState({ userId: user ? user.id : 0 }); }; onTeamSelected = (team: Team) => { - this.setState({ - userId: 0, - teamId: team ? team.id : 0, - }); + this.setState({ teamId: team ? team.id : 0 }); }; onPermissionChanged = (permission: OptionWithDescription) => { diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts new file mode 100644 index 00000000000..c5b67f58ac9 --- /dev/null +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -0,0 +1,24 @@ +import { Action, ActionTypes } from './actions'; +import { OrgRole, PermissionLevel, DashboardState } from 'app/types'; +import { inititalState, dashboardReducer } from './reducers'; + +describe('dashboard reducer', () => { + describe('loadDashboardPermissions', () => { + let state: DashboardState; + + beforeEach(() => { + const action: Action = { + type: ActionTypes.LoadDashboardPermissions, + payload: [ + { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, + { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, + ], + }; + state = dashboardReducer(inititalState, action); + }); + + it('should add permissions to state', async () => { + expect(state.permissions.length).toBe(2); + }); + }); +}); diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index d6589f8bf40..fa5ace388c4 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -50,7 +50,7 @@ export interface DashboardPermissionInfo { export interface NewDashboardAclItem { teamId: number; userId: number; - role: OrgRole; + role?: OrgRole; permission: PermissionLevel; type: AclTarget; } From 7b0215380f64304713492b73210d977396523e38 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 14 Sep 2018 08:31:41 +0200 Subject: [PATCH 123/666] added underline to links in table --- public/sass/components/_panel_table.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 225238b102c..e47e639a65e 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -86,6 +86,8 @@ padding: 0.45em 0 0.45em 1.1em; height: 100%; display: inline-block; + text-decoration: underline; + text-underline-position: under; } } From 0e9a6dcedc0ede60b1a6db3d3245b28b92fa4207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 09:30:32 +0200 Subject: [PATCH 124/666] Use datasource cache for backend tsdb/query endpoint (#13266) fix: use datasource cache for backend datasources --- pkg/api/api.go | 2 +- pkg/api/dataproxy.go | 12 +++++------- pkg/api/metrics.go | 14 +++++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 906481bbb8a..39b332aeb9f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -320,7 +320,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/search/", Search) // metrics - apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(QueryMetrics)) + apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(hs.QueryMetrics)) apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios)) apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData)) apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk)) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 33839ca985d..f455d3dbd29 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -13,19 +13,20 @@ import ( const HeaderNameNoBackendCache = "X-Grafana-NoCache" -func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) { +func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) { + nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" cacheKey := fmt.Sprintf("ds-%d", id) if !nocache { if cached, found := hs.cache.Get(cacheKey); found { ds := cached.(*m.DataSource) - if ds.OrgId == orgID { + if ds.OrgId == c.OrgId { return ds, nil } } } - query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID} + query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { return nil, err } @@ -37,10 +38,7 @@ func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) - nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - - ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache) - + ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c) if err != nil { c.JsonApiErr(500, "Unable to load datasource meta data", err) return diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index f2bc79df7ad..cb80bd346b8 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -13,21 +13,21 @@ import ( ) // POST /api/tsdb/query -func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { +func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To) if len(reqDto.Queries) == 0 { return Error(400, "No queries found in query", nil) } - dsID, err := reqDto.Queries[0].Get("datasourceId").Int64() + datasourceId, err := reqDto.Queries[0].Get("datasourceId").Int64() if err != nil { return Error(400, "Query missing datasourceId", nil) } - dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId} - if err := bus.Dispatch(&dsQuery); err != nil { - return Error(500, "failed to fetch data source", err) + ds, err := hs.getDatasourceFromCache(datasourceId, c) + if err != nil { + return Error(500, "Unable to load datasource meta data", err) } request := &tsdb.TsdbQuery{TimeRange: timeRange} @@ -38,11 +38,11 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { MaxDataPoints: query.Get("maxDataPoints").MustInt64(100), IntervalMs: query.Get("intervalMs").MustInt64(1000), Model: query, - DataSource: dsQuery.Result, + DataSource: ds, }) } - resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request) + resp, err := tsdb.HandleRequest(c.Req.Context(), ds, request) if err != nil { return Error(500, "Metric request error", err) } From f0f19e0c0334d51ce264d3d151b9d3e4a87a4f9a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 4 Sep 2018 13:21:02 +0200 Subject: [PATCH 125/666] Adds stackdriver frontend skeleton --- .../datasource/stackdriver/config_ctrl.ts | 11 +++ .../datasource/stackdriver/datasource.ts | 13 +++ .../plugins/datasource/stackdriver/module.ts | 14 ++++ .../partials/annotations.editor.html | 13 +++ .../stackdriver/partials/config.html | 6 ++ .../stackdriver/partials/query.editor.html | 81 +++++++++++++++++++ .../datasource/stackdriver/plugin.json | 16 ++++ .../datasource/stackdriver/query_ctrl.ts | 11 +++ 8 files changed, 165 insertions(+) create mode 100644 public/app/plugins/datasource/stackdriver/config_ctrl.ts create mode 100644 public/app/plugins/datasource/stackdriver/datasource.ts create mode 100644 public/app/plugins/datasource/stackdriver/module.ts create mode 100644 public/app/plugins/datasource/stackdriver/partials/annotations.editor.html create mode 100644 public/app/plugins/datasource/stackdriver/partials/config.html create mode 100755 public/app/plugins/datasource/stackdriver/partials/query.editor.html create mode 100644 public/app/plugins/datasource/stackdriver/plugin.json create mode 100644 public/app/plugins/datasource/stackdriver/query_ctrl.ts diff --git a/public/app/plugins/datasource/stackdriver/config_ctrl.ts b/public/app/plugins/datasource/stackdriver/config_ctrl.ts new file mode 100644 index 00000000000..405d91833cd --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/config_ctrl.ts @@ -0,0 +1,11 @@ +export class StackdriverConfigCtrl { + static templateUrl = 'public/app/plugins/datasource/stackdriver/partials/config.html'; + datasourceSrv: any; + current: any; + + /** @ngInject */ + constructor($scope, datasourceSrv) { + this.datasourceSrv = datasourceSrv; + this.current.jsonData = this.current.jsonData || {}; + } +} diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts new file mode 100644 index 00000000000..1bb2d4721a8 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -0,0 +1,13 @@ +/** @ngInject */ +export function StackdriverDatasource(this: any, instanceSettings, $q, backendSrv, templateSrv) { + // this.basicAuth = instanceSettings.basicAuth; + // this.url = instanceSettings.url; + // this.name = instanceSettings.name; + // this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || '0.9'; + // this.supportsTags = supportsTags(this.graphiteVersion); + // this.cacheTimeout = instanceSettings.cacheTimeout; + // this.withCredentials = instanceSettings.withCredentials; + // this.render_method = instanceSettings.render_method || 'POST'; + // this.funcDefs = null; + // this.funcDefsPromise = null; +} diff --git a/public/app/plugins/datasource/stackdriver/module.ts b/public/app/plugins/datasource/stackdriver/module.ts new file mode 100644 index 00000000000..52d3aa58453 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/module.ts @@ -0,0 +1,14 @@ +// import { StackdriverDatasource } from './datasource'; +// import { StackdriverQueryCtrl } from './query_ctrl'; +import { StackdriverConfigCtrl } from './config_ctrl'; + +// class AnnotationsQueryCtrl { +// static templateUrl = 'partials/annotations.editor.html'; +// } + +export { + // StackdriverDatasource as Datasource, + // StackdriverQueryCtrl as QueryCtrl, + StackdriverConfigCtrl as ConfigCtrl, + // AnnotationsQueryCtrl, +}; diff --git a/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html b/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html new file mode 100644 index 00000000000..9d228b8e4f9 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html @@ -0,0 +1,13 @@ +
    +
    + Graphite query + +
    + +
    Or
    + +
    + Graphite events tags + +
    +
    diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html new file mode 100644 index 00000000000..c0af41fb891 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -0,0 +1,6 @@ + + + +

    Hello Stackdriver

    diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html new file mode 100755 index 00000000000..51c25100c1e --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -0,0 +1,81 @@ + + +
    + +
    + +
    +
    +
    + +
    + +
    + + + + +
    + + + + + +
    + +
    + +
    +
    +
    +
    + +
    +
    + +
    + +
    + +
    + + + +
    +
    +
    +
    +
    + +
    diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json new file mode 100644 index 00000000000..c2d04eb9717 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -0,0 +1,16 @@ +{ + "name": "Stackdriver", + "type": "datasource", + "id": "stackdriver", + "metrics": true, + "alerting": false, + "annotations": false, + "queryOptions": { + "maxDataPoints": true, + "cacheTimeout": true + }, + "info": { + "description": "Data Source for Stackdriver", + "version": "1.0.0" + } +} \ No newline at end of file diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts new file mode 100644 index 00000000000..94389237eb2 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -0,0 +1,11 @@ +import './add_graphite_func'; +import './func_editor'; +import { QueryCtrl } from 'app/plugins/sdk'; + +export class StackdriverQueryCtrl extends QueryCtrl { + static templateUrl = 'partials/query.editor.html'; + /** @ngInject */ + constructor($scope, $injector) { + super($scope, $injector); + } +} From 9ee61b660677f4eb2245264b694d4bbdf4ac8858 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 4 Sep 2018 13:21:58 +0200 Subject: [PATCH 126/666] Add stackdriver backend skeleton --- pkg/cmd/grafana-server/main.go | 1 + pkg/tsdb/stackdriver/stackdriver.go | 177 ++++++++++++++++++++++++++++ pkg/tsdb/stackdriver/types.go | 8 ++ 3 files changed, 186 insertions(+) create mode 100644 pkg/tsdb/stackdriver/stackdriver.go create mode 100644 pkg/tsdb/stackdriver/types.go diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index f1e298671d7..c85270b64d9 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -29,6 +29,7 @@ import ( _ "github.com/grafana/grafana/pkg/tsdb/opentsdb" _ "github.com/grafana/grafana/pkg/tsdb/postgres" _ "github.com/grafana/grafana/pkg/tsdb/prometheus" + _ "github.com/grafana/grafana/pkg/tsdb/stackdriver" _ "github.com/grafana/grafana/pkg/tsdb/testdata" ) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go new file mode 100644 index 00000000000..d755ccf12e2 --- /dev/null +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -0,0 +1,177 @@ +package stackdriver + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + "regexp" + "strings" + + "golang.org/x/net/context/ctxhttp" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb" + "github.com/opentracing/opentracing-go" +) + +type StackdriverExecutor struct { + HttpClient *http.Client +} + +func NewStackdriverExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + return &StackdriverExecutor{}, nil +} + +var glog = log.New("tsdb.stackdriver") + +func init() { + tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor) +} + +func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + result := &tsdb.Response{} + + from := "-" + formatTimeRange(tsdbQuery.TimeRange.From) + until := formatTimeRange(tsdbQuery.TimeRange.To) + var target string + + formData := url.Values{ + "from": []string{from}, + "until": []string{until}, + "format": []string{"json"}, + "maxDataPoints": []string{"500"}, + } + + for _, query := range tsdbQuery.Queries { + glog.Info("stackdriver", "query", query.Model) + if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { + target = fixIntervalFormat(fullTarget) + } else { + target = fixIntervalFormat(query.Model.Get("target").MustString()) + } + } + + formData["target"] = []string{target} + + if setting.Env == setting.DEV { + glog.Debug("Graphite request", "params", formData) + } + + req, err := e.createRequest(dsInfo, formData) + if err != nil { + return nil, err + } + + httpClient, err := dsInfo.GetHttpClient() + if err != nil { + return nil, err + } + + span, ctx := opentracing.StartSpanFromContext(ctx, "graphite query") + span.SetTag("target", target) + span.SetTag("from", from) + span.SetTag("until", until) + span.SetTag("datasource_id", dsInfo.Id) + span.SetTag("org_id", dsInfo.OrgId) + + defer span.Finish() + + opentracing.GlobalTracer().Inject( + span.Context(), + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(req.Header)) + + res, err := ctxhttp.Do(ctx, httpClient, req) + if err != nil { + return nil, err + } + + data, err := e.parseResponse(res) + if err != nil { + return nil, err + } + + result.Results = make(map[string]*tsdb.QueryResult) + queryRes := tsdb.NewQueryResult() + + for _, series := range data { + queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ + Name: series.Target, + Points: series.DataPoints, + }) + + if setting.Env == setting.DEV { + glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints)) + } + } + + result.Results["A"] = queryRes + return result, nil +} + +func (e *StackdriverExecutor) parseResponse(res *http.Response) ([]TargetResponseDTO, error) { + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return nil, err + } + + if res.StatusCode/100 != 2 { + glog.Info("Request failed", "status", res.Status, "body", string(body)) + return nil, fmt.Errorf("Request failed status: %v", res.Status) + } + + var data []TargetResponseDTO + err = json.Unmarshal(body, &data) + if err != nil { + glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body)) + return nil, err + } + + return data, nil +} + +func (e *StackdriverExecutor) createRequest(dsInfo *models.DataSource, data url.Values) (*http.Request, error) { + u, _ := url.Parse(dsInfo.Url) + u.Path = path.Join(u.Path, "render") + + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode())) + if err != nil { + glog.Info("Failed to create request", "error", err) + return nil, fmt.Errorf("Failed to create request. error: %v", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if dsInfo.BasicAuth { + req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword) + } + + return req, err +} + +func formatTimeRange(input string) string { + if input == "now" { + return input + } + return strings.Replace(strings.Replace(strings.Replace(input, "now", "", -1), "m", "min", -1), "M", "mon", -1) +} + +func fixIntervalFormat(target string) string { + rMinute := regexp.MustCompile(`'(\d+)m'`) + rMin := regexp.MustCompile("m") + target = rMinute.ReplaceAllStringFunc(target, func(m string) string { + return rMin.ReplaceAllString(m, "min") + }) + rMonth := regexp.MustCompile(`'(\d+)M'`) + rMon := regexp.MustCompile("M") + target = rMonth.ReplaceAllStringFunc(target, func(M string) string { + return rMon.ReplaceAllString(M, "mon") + }) + return target +} diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go new file mode 100644 index 00000000000..3fb4cdc43f9 --- /dev/null +++ b/pkg/tsdb/stackdriver/types.go @@ -0,0 +1,8 @@ +package stackdriver + +import "github.com/grafana/grafana/pkg/tsdb" + +type TargetResponseDTO struct { + Target string `json:"target"` + DataPoints tsdb.TimeSeriesPoints `json:"datapoints"` +} From 834d06c35b6c0e5823714b881969ebbf0079de1c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 4 Sep 2018 13:23:14 +0200 Subject: [PATCH 127/666] Build new stackdriver frontend script --- public/app/features/plugins/built_in_plugins.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 2c5bf459eda..e29e1709ccf 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -11,6 +11,7 @@ import * as postgresPlugin from 'app/plugins/datasource/postgres/module'; import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module'; import * as mssqlPlugin from 'app/plugins/datasource/mssql/module'; import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module'; +import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module'; import * as textPanel from 'app/plugins/panel/text/module'; import * as graphPanel from 'app/plugins/panel/graph/module'; @@ -36,6 +37,7 @@ const builtInPlugins = { 'app/plugins/datasource/mssql/module': mssqlPlugin, 'app/plugins/datasource/prometheus/module': prometheusPlugin, 'app/plugins/datasource/testdata/module': testDataDSPlugin, + 'app/plugins/datasource/stackdriver/module': stackdriverPlugin, 'app/plugins/panel/text/module': textPanel, 'app/plugins/panel/graph/module': graphPanel, From ef3beb1f0e169df967c2d8eea0fde9cd56c24394 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 4 Sep 2018 13:25:22 +0200 Subject: [PATCH 128/666] Adds poc code for retrieving google auth accesstoken --- pkg/api/pluginproxy/ds_proxy.go | 96 +++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index fb2cab9b9b1..54c272dd5d6 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -2,7 +2,7 @@ package pluginproxy import ( "bytes" - "encoding/json" + "context" "errors" "fmt" "io/ioutil" @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "golang.org/x/oauth2/jwt" ) var ( @@ -353,49 +354,64 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) { } func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) { - if cachedToken, found := tokenCache[proxy.getAccessTokenCacheKey()]; found { - if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) { - logger.Info("Using token from cache") - return cachedToken.AccessToken, nil - } + conf := jwt.Config{ + Email: "raintank-production-stackdrive@raintank-production.iam.gserviceaccount.com", + PrivateKey: []byte("-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb1u1Srw8ICWfX\nb+hh0qyRoWJzHkf4jxdpOjjqiaYqlipf3fCWoyNgHvQSmX2trYbb1Kg+8Nv9/iaz\n6el48MvOlRN6WbcMfhFoT8AKkdjYD3DM+vK3C3uXmWeVMHzQimjECFWsX4WRU4fj\nLJ44B3svFvShe3bRJpt2e5LdfjcEQER7Bvte/zQi8v1jloHmcptyz8wb9NddVHs9\nINwFbrSaUiQnoJTDSIYEMiKDTvZHRenmLz/RexjG5RXbdA+I9ZB5EASoEbG+7Ssc\nsn+Bhu/J29s/+NVM5sYgcjOm54NxCYFwYlApbroa0+KcDDCs4DzgzA41sOcvBWIX\nOZuQRD6BAgMBAAECggEAGXUo28MBP5zZu9XqLmDOFBQ7Evc1ZqNpfaUnOxk1beuO\nDI8jCFiqJL+pu4gbgc3BJBQ/T9jk9z8Xb3iczUb45ExyHCCfyIinq1Sr2I4u0Ezl\nbnboQ4K6s+85fqOnICIcLzn1VO1d0nnEzxWw2xJNy0mCuQaESHJ4Hwjc2xYNQsJD\newfeAQh2bUw7R4xyIJieP5a0fQbZWAENbVKstyfd9NJNM+6wUmwXR4ALbLM31f1n\nExbHfUe8TLp892ZgeSL+C0C31xDqkqi/DfUOFpBt79Rr5p3++rDEe98NKrDmw6yz\nbprZHBslzx612md2L/ljKQROs3tHl6BvGfFtQMOjSQKBgQDXidmYq5FQwRiVkl4r\ny8WzNbflbdfMRxetaPwR7lPWgiyGgU54Y0xUAaCy1bbpmJkfRcxJuSBYpensV9x2\nXwAI8vzuJcmteVAeZ59YYJYOA+AdT8MQW7aCeUK5qgLgpO0dt4wCK4xERdhbQdvC\nMMCu3UfgeVNyo+EhTqM97VuHTwKBgQC5GB8PwM8H7NxaPZhTfy7S54OZHdhoUpTc\nZ+qWTSG6QgjHDzNaqZ+p6ehDCnO9EyuwpYHXcFIavlmSxszUPNy6f3TosvKcvm6q\n5CFzdt4fgev3cgGB+P1mT1gyi6UjntWuAj1fFvxh+o87kq9v4p6YVX+woll10CaH\n++O3QNlpLwKBgQCRV5qM0by26M8MJVwtSkaxdxrfsjdfv9zeibnY2Y5dSwB9Xvqs\nQcF5sHNNxMGIOeefZ/C/EgAW5yKbxg+bHqqmXjxi1sZtnS2CozuXW+Iz5zccbOnL\nwRyMVPrCujsggvaGIHxgBj+a1kJ0Hy/yfe+guwS6APZditbIIAACRWmADwKBgAuA\nHC32ZOaxKN/Sg+xsMpSYHe0dlZylxOoM6t573GSeRb1YjHBNqcX86pl/xMEyt7w6\nDF8+c1uGCDq+b2ugfHZ6BOGQfNKQYn/rvMhX0mVSxT6SrtVMizIYK/q4AoK8E7rE\nGNwXqYbM8qlY692fzwrYBR8Md1KCpGI+nF9+gAOxAoGAAwJO+jr44SRldSSsli0d\nDbGRmlMc093MebjwNsO2NGoF8uTRyYIzchP2l57PMPKFX/r5IcchjVh3wHWwtzMS\nhB8zfRDj7RFjW0H8U1Qf5k2ID3ACJtxnt+o744Lggqzf9puf4RKwwAjIwJy2lhbi\nyA+63fAHMAwG+k22IkBqcu4=\n-----END PRIVATE KEY-----\n"), + Scopes: []string{"https://www.googleapis.com/auth/monitoring.read"}, + TokenURL: "https://oauth2.googleapis.com/token", } - - urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data) + ctx := context.Background() + tokenSrc := conf.TokenSource(ctx) + // logger.Info("Accesstoken", tokenSrc.Token.AccessToken) + token, err := tokenSrc.Token() if err != nil { - return "", err + logger.Info("GetToken", "Error", err) } - - params := make(url.Values) - for key, value := range proxy.route.TokenAuth.Params { - interpolatedParam, err := interpolateString(value, data) - if err != nil { - return "", err - } - params.Add(key, interpolatedParam) - } - - getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode())) - getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") - getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode()))) - - resp, err := client.Do(getTokenReq) - if err != nil { - return "", err - } - - defer resp.Body.Close() - - var token jwtToken - if err := json.NewDecoder(resp.Body).Decode(&token); err != nil { - return "", err - } - - expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64) - token.ExpiresOn = time.Unix(expiresOnEpoch, 0) - tokenCache[proxy.getAccessTokenCacheKey()] = &token - - logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn) + logger.Info("GetToken", "Token", token.AccessToken) return token.AccessToken, nil + // if cachedToken, found := tokenCache[proxy.getAccessTokenCacheKey()]; found { + // if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) { + // logger.Info("Using token from cache") + // return cachedToken.AccessToken, nil + // } + // } + + // urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data) + // if err != nil { + // return "", err + // } + + // params := make(url.Values) + // for key, value := range proxy.route.TokenAuth.Params { + // interpolatedParam, err := interpolateString(value, data) + // if err != nil { + // return "", err + // } + // params.Add(key, interpolatedParam) + // } + + // getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode())) + // getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") + // getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode()))) + + // resp, err := client.Do(getTokenReq) + // if err != nil { + // return "", err + // } + + // defer resp.Body.Close() + + // var token jwtToken + // if err := json.NewDecoder(resp.Body).Decode(&token); err != nil { + // return "", err + // } + + // expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64) + // token.ExpiresOn = time.Unix(expiresOnEpoch, 0) + // tokenCache[proxy.getAccessTokenCacheKey()] = &token + + // logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn) + // return token.AccessToken, nil } func (proxy *DataSourceProxy) getAccessTokenCacheKey() string { From 4fa4c275372843e15028dcc6882733703a658af8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 4 Sep 2018 14:32:25 +0200 Subject: [PATCH 129/666] Upload: Fixing link function in directive --- public/app/features/dashboard/upload.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 974a0c35cd2..5ef329fc76e 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -1,7 +1,7 @@ import coreModule from 'app/core/core_module'; const template = ` - +