From 3a3272e225c986cdeb762197a82f84b84b9e769f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 12 Dec 2017 09:35:57 +0100 Subject: [PATCH 0001/2611] 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 13efc529ecbd4b68dcab6c76ed1b5c48be801afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jan 2018 14:52:30 +0100 Subject: [PATCH 0002/2611] poc: began react panel experiments --- .../dashboard/dashgrid/DashboardPanel.tsx | 78 ++++++++++++++++--- .../app/features/plugins/built_in_plugins.ts | 2 + public/app/plugins/panel/text2/README.md | 5 ++ .../panel/text2/img/icn-text-panel.svg | 26 +++++++ public/app/plugins/panel/text2/module.tsx | 13 ++++ public/app/plugins/panel/text2/plugin.json | 17 ++++ 6 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 public/app/plugins/panel/text2/README.md create mode 100644 public/app/plugins/panel/text2/img/icn-text-panel.svg create mode 100644 public/app/plugins/panel/text2/module.tsx create mode 100644 public/app/plugins/panel/text2/plugin.json diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 27fe64d4660..562b79a859e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import {PanelModel} from '../panel_model'; -import {PanelContainer} from './PanelContainer'; -import {AttachedPanel} from './PanelLoader'; -import {DashboardRow} from './DashboardRow'; -import {AddPanelPanel} from './AddPanelPanel'; +import config from 'app/core/config'; +import classNames from 'classnames'; +import { PanelModel } from '../panel_model'; +import { PanelContainer } from './PanelContainer'; +import { AttachedPanel } from './PanelLoader'; +import { DashboardRow } from './DashboardRow'; +import { AddPanelPanel } from './AddPanelPanel'; +import { importPluginModule } from 'app/features/plugins/plugin_loader'; export interface DashboardPanelProps { panel: PanelModel; @@ -13,10 +16,26 @@ export interface DashboardPanelProps { export class DashboardPanel extends React.Component { element: any; attachedPanel: AttachedPanel; + pluginInfo: any; + pluginExports: any; + specialPanels = {}; constructor(props) { super(props); this.state = {}; + + this.specialPanels['row'] = this.renderRow.bind(this); + this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); + + if (!this.isSpecial()) { + this.pluginInfo = config.panels[this.props.panel.type]; + + // load panel plugin + importPluginModule(this.pluginInfo.module).then(pluginExports => { + this.pluginExports = pluginExports; + this.forceUpdate(); + }); + } } componentDidMount() { @@ -36,19 +55,54 @@ export class DashboardPanel extends React.Component { } } + isSpecial() { + return this.specialPanels[this.props.panel.type]; + } + + renderRow() { + return ; + } + + renderAddPanel() { + return ; + } + render() { - // special handling for rows - if (this.props.panel.type === 'row') { - return ; + if (this.isSpecial()) { + return this.specialPanels[this.props.panel.type](); } - if (this.props.panel.type === 'add-panel') { - return ; + let isFullscreen = false; + let isLoading = false; + let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + let PanelComponent = null; + + if (this.pluginExports && this.pluginExports.PanelComponent) { + PanelComponent = this.pluginExports.PanelComponent; } return ( -
this.element = element} className="panel-height-helper" /> +
+
+ + + + + + {isLoading && ( + + + + )} +
{this.props.panel.title}
+
+ +
{PanelComponent && }
+
); + + // return ( + //
this.element = element} className="panel-height-helper" /> + // ); } } - diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index c86efc4f695..a4657f84aa1 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -10,6 +10,7 @@ import * as postgresPlugin from 'app/plugins/datasource/postgres/module'; import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module'; import * as textPanel from 'app/plugins/panel/text/module'; +import * as text2Panel from 'app/plugins/panel/text2/module'; import * as graphPanel from 'app/plugins/panel/graph/module'; import * as dashListPanel from 'app/plugins/panel/dashlist/module'; import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; @@ -37,6 +38,7 @@ const builtInPlugins = { 'app/plugins/app/testdata/datasource/module': testDataDSPlugin, 'app/plugins/panel/text/module': textPanel, + 'app/plugins/panel/text2/module': text2Panel, 'app/plugins/panel/graph/module': graphPanel, 'app/plugins/panel/dashlist/module': dashListPanel, 'app/plugins/panel/pluginlist/module': pluginsListPanel, diff --git a/public/app/plugins/panel/text2/README.md b/public/app/plugins/panel/text2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/text2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/text2/img/icn-text-panel.svg b/public/app/plugins/panel/text2/img/icn-text-panel.svg new file mode 100644 index 00000000000..a9d0a1d2c4a --- /dev/null +++ b/public/app/plugins/panel/text2/img/icn-text-panel.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx new file mode 100644 index 00000000000..7f5e363891c --- /dev/null +++ b/public/app/plugins/panel/text2/module.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export class ReactTestPanel extends React.Component { + constructor(props) { + super(props); + } + + render() { + return

Panel content

; + } +} + +export { ReactTestPanel as PanelComponent }; diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json new file mode 100644 index 00000000000..95d821bfd50 --- /dev/null +++ b/public/app/plugins/panel/text2/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "Text2", + "id": "text2", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" + } + } +} + From 3eb5f232094e20caf03d428d1f7031c8b6e15459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 3 Jan 2018 13:03:26 +0100 Subject: [PATCH 0003/2611] poc: began react panel experiments, step2 --- .../dashboard/dashgrid/DashboardPanel.tsx | 77 +++++++++++++++---- public/app/features/panel/panel_header.ts | 15 ---- public/sass/pages/_dashboard.scss | 22 +++--- 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 562b79a859e..7a0a9bb2e86 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,6 +1,7 @@ import React from 'react'; import config from 'app/core/config'; import classNames from 'classnames'; +import appEvents from 'app/core/app_events'; import { PanelModel } from '../panel_model'; import { PanelContainer } from './PanelContainer'; import { AttachedPanel } from './PanelLoader'; @@ -72,9 +73,6 @@ export class DashboardPanel extends React.Component { return this.specialPanels[this.props.panel.type](); } - let isFullscreen = false; - let isLoading = false; - let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); let PanelComponent = null; if (this.pluginExports && this.pluginExports.PanelComponent) { @@ -83,20 +81,7 @@ export class DashboardPanel extends React.Component { return (
-
- - - - - - {isLoading && ( - - - - )} -
{this.props.panel.title}
-
- +
{PanelComponent && }
); @@ -106,3 +91,61 @@ export class DashboardPanel extends React.Component { // ); } } + +interface PanelHeaderProps { + panel: any; +} + +export class PanelHeader extends React.Component { + onEditPanel = () => { + appEvents.emit('panel-change-view', { + fullscreen: true, + edit: true, + panelId: this.props.panel.id, + }); + }; + + render() { + let isFullscreen = false; + let isLoading = false; + let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + + return ( +
+ + + + + + {isLoading && ( + + + + )} + +
+ + + {this.props.panel.title} + + + + + + 4m + + +
+
+ ); + } +} diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index ca6ed68b648..cb9f2d5c563 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -10,21 +10,6 @@ var template = ` {{ctrl.timeInfo}} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index af9a02caa2a..6d4e94a5175 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -40,6 +40,14 @@ div.flot-text { background-color: transparent; border: none; } + + &:hover { + .panel-menu-toggle { + visibility: visible; + transition: opacity 0.1s ease-in 0.2s; + opacity: 1; + } + } } .panel-content { @@ -159,7 +167,7 @@ div.flot-text { display: block; @include panel-corner-color(lighten($panel-bg, 4%)); .fa:before { - content: "\f129"; + content: '\f129'; } } @@ -170,7 +178,7 @@ div.flot-text { left: -5px; } .fa:before { - content: "\f08e"; + content: '\f08e'; } } @@ -179,19 +187,11 @@ div.flot-text { color: $text-color; @include panel-corner-color($popover-error-bg); .fa:before { - content: "\f12a"; + content: '\f12a'; } } } -.panel-hover-highlight { - .panel-menu-toggle { - visibility: visible; - transition: opacity 0.1s ease-in 0.2s; - opacity: 1; - } -} - .panel-time-info { font-weight: bold; float: right; From 456b4d2a66add0f15313dbbe6c8cdfb666ad9c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 3 Jan 2018 13:33:54 +0100 Subject: [PATCH 0004/2611] poc: began react panel experiments, step2 --- public/app/core/directives/dash_class.js | 40 ------------------- public/app/core/directives/dash_class.ts | 34 ++++++++++++++++ .../dashboard/dashgrid/AddPanelPanel.tsx | 7 ++-- .../dashboard/dashgrid/DashboardGrid.tsx | 10 +++-- .../dashboard/dashgrid/DashboardPanel.tsx | 36 ++++------------- .../dashboard/dashgrid/DashboardRow.tsx | 20 +++------- .../dashboard/specs/DashboardRow.jest.tsx | 14 ++----- 7 files changed, 60 insertions(+), 101 deletions(-) delete mode 100644 public/app/core/directives/dash_class.js create mode 100644 public/app/core/directives/dash_class.ts diff --git a/public/app/core/directives/dash_class.js b/public/app/core/directives/dash_class.js deleted file mode 100644 index 9df53bdbd48..00000000000 --- a/public/app/core/directives/dash_class.js +++ /dev/null @@ -1,40 +0,0 @@ -define([ - 'lodash', - 'jquery', - '../core_module', -], -function (_, $, coreModule) { - 'use strict'; - - coreModule.default.directive('dashClass', function() { - return { - link: function($scope, elem) { - - $scope.onAppEvent('panel-fullscreen-enter', function() { - elem.toggleClass('panel-in-fullscreen', true); - }); - - $scope.onAppEvent('panel-fullscreen-exit', function() { - elem.toggleClass('panel-in-fullscreen', false); - }); - - $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) { - elem.toggleClass('playlist-active', newValue === true); - }); - - $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) { - if (newValue) { - elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); - setTimeout(function() { - elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); - }, 10); - } else { - elem.removeClass('dashboard-page--settings-opening'); - elem.removeClass('dashboard-page--settings-open'); - } - }); - } - }; - }); - -}); diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts new file mode 100644 index 00000000000..f0723f4fec7 --- /dev/null +++ b/public/app/core/directives/dash_class.ts @@ -0,0 +1,34 @@ +import _ from 'lodash'; +import coreModule from '../core_module'; + +coreModule.directive('dashClass', function($timeout) { + return { + link: function($scope, elem) { + $scope.ctrl.dashboard.events.on('view-mode-changed', function(panel) { + $timeout(() => { + elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); + }); + }); + + $scope.onAppEvent('panel-fullscreen-exit', function() { + elem.toggleClass('panel-in-fullscreen', false); + }); + + $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) { + elem.toggleClass('playlist-active', newValue === true); + }); + + $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) { + if (newValue) { + elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); + setTimeout(function() { + elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); + }, 10); + } else { + elem.removeClass('dashboard-page--settings-opening'); + elem.removeClass('dashboard-page--settings-open'); + } + }); + }, + }; +}); diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 1f143f3d7f7..483ccb52bda 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -3,14 +3,14 @@ import _ from 'lodash'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; export interface AddPanelPanelProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export interface AddPanelPanelState { @@ -55,8 +55,7 @@ export class AddPanelPanel extends React.Component { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); + const dashboard = this.props.dashboard; const { gridPos } = this.props.panel; var newPanel: any = { diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 3f65c33c90d..0bb75c54963 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -50,7 +50,8 @@ function GridWrapper({ onResize={onResize} onResizeStop={onResizeStop} onDragStop={onDragStop} - onLayoutChange={onLayoutChange}> + onLayoutChange={onLayoutChange} + > {children} ); @@ -177,8 +178,8 @@ export class DashboardGrid extends React.Component { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
- -
, + +
); } @@ -196,7 +197,8 @@ export class DashboardGrid extends React.Component { onWidthChange={this.onWidthChange} onDragStop={this.onDragStop} onResize={this.onResize} - onResizeStop={this.onResizeStop}> + onResizeStop={this.onResizeStop} + > {this.renderPanels()} ); diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 7a0a9bb2e86..eecf532922d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -3,7 +3,7 @@ import config from 'app/core/config'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import { AttachedPanel } from './PanelLoader'; import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; @@ -11,7 +11,7 @@ import { importPluginModule } from 'app/features/plugins/plugin_loader'; export interface DashboardPanelProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardPanel extends React.Component { @@ -39,33 +39,16 @@ export class DashboardPanel extends React.Component { } } - componentDidMount() { - if (!this.element) { - return; - } - - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - const loader = panelContainer.getPanelLoader(); - this.attachedPanel = loader.load(this.element, this.props.panel, dashboard); - } - - componentWillUnmount() { - if (this.attachedPanel) { - this.attachedPanel.destroy(); - } - } - isSpecial() { return this.specialPanels[this.props.panel.type]; } renderRow() { - return ; + return ; } renderAddPanel() { - return ; + return ; } render() { @@ -81,7 +64,7 @@ export class DashboardPanel extends React.Component { return (
- +
{PanelComponent && }
); @@ -93,16 +76,13 @@ export class DashboardPanel extends React.Component { } interface PanelHeaderProps { - panel: any; + panel: PanelModel; + dashboard: DashboardModel; } export class PanelHeader extends React.Component { onEditPanel = () => { - appEvents.emit('panel-change-view', { - fullscreen: true, - edit: true, - panelId: this.props.panel.id, - }); + this.props.dashboard.setViewMode(this.props.panel, true, true); }; render() { diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index fad7c120f65..17dd1681c66 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -1,19 +1,16 @@ import React from 'react'; import classNames from 'classnames'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import templateSrv from 'app/features/templating/template_srv'; import appEvents from 'app/core/app_events'; export interface DashboardRowProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardRow extends React.Component { - dashboard: any; - panelContainer: any; - constructor(props) { super(props); @@ -21,16 +18,13 @@ export class DashboardRow extends React.Component { collapsed: this.props.panel.collapsed, }; - this.panelContainer = this.props.getPanelContainer(); - this.dashboard = this.panelContainer.getDashboard(); - this.toggle = this.toggle.bind(this); this.openSettings = this.openSettings.bind(this); this.delete = this.delete.bind(this); } toggle() { - this.dashboard.toggleRow(this.props.panel); + this.props.dashboard.toggleRow(this.props.panel); this.setState(prevState => { return { collapsed: !prevState.collapsed }; @@ -55,14 +49,10 @@ export class DashboardRow extends React.Component { altActionText: 'Delete row only', icon: 'fa-trash', onConfirm: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, true); + this.props.dashboard.removeRow(this.props.panel, true); }, onAltAction: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, false); + this.props.dashboard.removeRow(this.props.panel, false); }, }); } diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.jest.tsx index 2d44f2e0e74..3270abc9de9 100644 --- a/public/app/features/dashboard/specs/DashboardRow.jest.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.jest.tsx @@ -4,18 +4,13 @@ import { DashboardRow } from '../dashgrid/DashboardRow'; import { PanelModel } from '../panel_model'; describe('DashboardRow', () => { - let wrapper, panel, getPanelContainer, dashboardMock; + let wrapper, panel, dashboardMock; beforeEach(() => { - dashboardMock = {toggleRow: jest.fn()}; + dashboardMock = { toggleRow: jest.fn() }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn() - }); - - panel = new PanelModel({collapsed: false}); - wrapper = shallow(); + panel = new PanelModel({ collapsed: false }); + wrapper = shallow(); }); it('Should not have collapsed class when collaped is false', () => { @@ -29,5 +24,4 @@ describe('DashboardRow', () => { expect(wrapper.find('.dashboard-row--collapsed')).toHaveLength(1); expect(dashboardMock.toggleRow.mock.calls).toHaveLength(1); }); - }); From 636cca83320ebce7b82b2bb65b97f67581f9986e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 2 Feb 2018 16:01:01 +0100 Subject: [PATCH 0005/2611] poc: merge sync --- public/app/features/dashboard/dashgrid/AddPanelPanel.tsx | 4 +--- public/app/features/dashboard/dashgrid/DashboardPanel.tsx | 1 - public/app/features/dashboard/dashgrid/DashboardRow.tsx | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index eb6a4de00ae..beae2650d3e 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -86,9 +86,7 @@ export class AddPanelPanel extends React.Component { } update() { - this.dashboard.processRepeats(); + this.props.dashboard.processRepeats(); this.forceUpdate(); } From 3091aece2b5dcdbf42b129c892eb505eba0325bc Mon Sep 17 00:00:00 2001 From: Patrick Schuster Date: Wed, 28 Mar 2018 11:56:54 +0200 Subject: [PATCH 0006/2611] Add Google Hangouts Chat notifier. --- pkg/services/alerting/notifiers/googlechat.go | 215 ++++++++++++++++++ .../alerting/notifiers/googlechat_test.go | 53 +++++ 2 files changed, 268 insertions(+) create mode 100644 pkg/services/alerting/notifiers/googlechat.go create mode 100644 pkg/services/alerting/notifiers/googlechat_test.go diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go new file mode 100644 index 00000000000..5d8fba1f8c6 --- /dev/null +++ b/pkg/services/alerting/notifiers/googlechat.go @@ -0,0 +1,215 @@ +package notifiers + +import ( + "encoding/json" + "fmt" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/setting" + "time" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "googlechat", + Name: "Google Hangouts Chat", + Description: "Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message " + + "format (https://developers.google.com/hangouts/chat/reference/message-formats/).", + Factory: NewGoogleChatNotifier, + OptionsTemplate: ` +

Google Hangouts Chat settings

+
+ Url + +
+ `, + }) +} + +func NewGoogleChatNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} + } + + return &GoogleChatNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Url: url, + log: log.New("alerting.notifier.googlechat"), + }, nil +} + +type GoogleChatNotifier struct { + NotifierBase + Url string + method string + log log.Logger +} + +/** +Structs used to build a custom Google Hangouts Chat message card. +See: https://developers.google.com/hangouts/chat/reference/message-formats/cards +*/ +type outerStruct struct { + Cards []card `json:"cards"` +} + +type card struct { + Header header `json:"header"` + Sections []section `json:"sections"` +} + +type header struct { + Title string `json:"title"` +} + +type section struct { + Widgets []widget `json:"widgets"` +} + +// "generic" widget used to add different types of widgets (buttonWidget, textParagraphWidget, imageWidget) +type widget interface { +} + +type buttonWidget struct { + Buttons []button `json:"buttons"` +} + +type textParagraphWidget struct { + Text text `json:"textParagraph"` +} + +type text struct { + Text string `json:"text"` +} + +type imageWidget struct { + Image image `json:"image"` +} + +type image struct { + ImageUrl string `json:"imageUrl"` +} + +type button struct { + TextButton textButton `json:"textButton"` +} + +type textButton struct { + Text string `json:"text"` + OnClick onClick `json:"onClick"` +} + +type onClick struct { + OpenLink openLink `json:"openLink"` +} + +type openLink struct { + Url string `json:"url"` +} + +func (this *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Executing Google Chat notification") + + headers := map[string]string{ + "Content-Type": "application/json; charset=UTF-8", + } + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("evalContext returned an invalid rule URL") + } + + // add a text paragraph widget for the message + widgets := []widget{ + textParagraphWidget{ + Text: text{ + Text: evalContext.Rule.Message, + }, + }, + } + + // add a text paragraph widget for the fields + var fields []textParagraphWidget + fieldLimitCount := 4 + for index, evt := range evalContext.EvalMatches { + fields = append(fields, + textParagraphWidget{ + Text: text{ + Text: "" + evt.Metric + ": " + fmt.Sprint(evt.Value) + "", + }, + }, + ) + if index > fieldLimitCount { + break + } + } + widgets = append(widgets, fields) + + // if an image exists, add it as an image widget + if evalContext.ImagePublicUrl != "" { + widgets = append(widgets, imageWidget{ + Image: image{ + ImageUrl: evalContext.ImagePublicUrl, + }, + }) + } else { + this.log.Info("Could not retrieve a public image URL.") + } + + // add a button widget (link to Grafana) + widgets = append(widgets, buttonWidget{ + Buttons: []button{ + { + TextButton: textButton{ + Text: "OPEN IN GRAFANA", + OnClick: onClick{ + OpenLink: openLink{ + Url: ruleUrl, + }, + }, + }, + }, + }, + }) + + // add text paragraph widget for the build version and timestamp + widgets = append(widgets, textParagraphWidget{ + Text: text{ + Text: "Grafana v" + setting.BuildVersion + " | " + (time.Now()).Format(time.RFC822), + }, + }) + + // nest the required structs + res1D := &outerStruct{ + Cards: []card{ + { + Header: header{ + Title: evalContext.GetNotificationTitle(), + }, + Sections: []section{ + { + Widgets: widgets, + }, + }, + }, + }, + } + body, _ := json.Marshal(res1D) + + cmd := &m.SendWebhookSync{ + Url: this.Url, + HttpMethod: "POST", + HttpHeader: headers, + Body: string(body), + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/googlechat_test.go b/pkg/services/alerting/notifiers/googlechat_test.go new file mode 100644 index 00000000000..1fdce878926 --- /dev/null +++ b/pkg/services/alerting/notifiers/googlechat_test.go @@ -0,0 +1,53 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestGoogleChatNotifier(t *testing.T) { + Convey("Google Hangouts Chat notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "googlechat", + Settings: settingsJSON, + } + + _, err := NewGoogleChatNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("from settings", func() { + json := ` + { + "url": "http://google.com" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "googlechat", + Settings: settingsJSON, + } + + not, err := NewGoogleChatNotifier(model) + webhookNotifier := not.(*GoogleChatNotifier) + + So(err, ShouldBeNil) + So(webhookNotifier.Name, ShouldEqual, "ops") + So(webhookNotifier.Type, ShouldEqual, "googlechat") + So(webhookNotifier.Url, ShouldEqual, "http://google.com") + }) + + }) + }) +} From 069012639af8ed873a776772af22ddf883ae1722 Mon Sep 17 00:00:00 2001 From: Jonathan McCall Date: Fri, 20 Apr 2018 12:17:17 -0400 Subject: [PATCH 0007/2611] 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 0008/2611] 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 0009/2611] 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 0010/2611] 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 0011/2611] 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 0012/2611] 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 35403c18753e9c6364a90b1e84d81a0e28725282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jun 2018 08:42:41 +0200 Subject: [PATCH 0013/2611] wip: react panel makeover mini progress --- public/app/core/directives/dash_class.ts | 4 +- .../dashboard/dashgrid/DashboardGrid.tsx | 1 + .../dashboard/dashgrid/DashboardPanel.tsx | 77 ++++++++++++++++- public/app/features/dashboard/panel_model.ts | 6 ++ .../dashboard/specs/AddPanelPanel.jest.tsx | 7 +- .../app/features/dashboard/view_state_srv.ts | 84 +++++-------------- 6 files changed, 102 insertions(+), 77 deletions(-) diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index f0723f4fec7..1dab57da0d4 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -10,9 +10,7 @@ coreModule.directive('dashClass', function($timeout) { }); }); - $scope.onAppEvent('panel-fullscreen-exit', function() { - elem.toggleClass('panel-in-fullscreen', false); - }); + elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) { elem.toggleClass('playlist-active', newValue === true); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 0bb75c54963..9f2e449b49e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -175,6 +175,7 @@ export class DashboardGrid extends React.Component { const panelElements = []; for (let panel of this.dashboard.panels) { + console.log('panel.fullscreen', panel.fullscreen); const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 2b864dc47d0..fe97ac6039a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import $ from 'jquery'; import config from 'app/core/config'; import classNames from 'classnames'; import { PanelModel } from '../panel_model'; @@ -7,6 +8,11 @@ import { AttachedPanel } from './PanelLoader'; import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; import { importPluginModule } from 'app/features/plugins/plugin_loader'; +import { store } from 'app/stores/store'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; + +const TITLE_HEIGHT = 27; +const PANEL_BORDER = 2; export interface DashboardPanelProps { panel: PanelModel; @@ -61,13 +67,40 @@ export class DashboardPanel extends React.Component { PanelComponent = this.pluginExports.PanelComponent; } + let panelContentStyle = { + height: this.getPanelHeight(), + }; + return ( -
- -
{PanelComponent && }
+
+
+ +
+ {PanelComponent && } +
+
+
+ {this.props.panel.isEditing && } +
); } + + getPanelHeight() { + const panel = this.props.panel; + let height = 0; + + if (panel.fullscreen) { + var docHeight = $(window).height(); + var editHeight = Math.floor(docHeight * 0.4); + var fullscreenHeight = Math.floor(docHeight * 0.8); + height = panel.isEditing ? editHeight : fullscreenHeight; + } else { + height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN; + } + + return height - PANEL_BORDER + TITLE_HEIGHT; + } } interface PanelHeaderProps { @@ -77,7 +110,11 @@ interface PanelHeaderProps { export class PanelHeader extends React.Component { onEditPanel = () => { - this.props.dashboard.setViewMode(this.props.panel, true, true); + store.view.updateQuery({ + panelId: this.props.panel.id, + edit: true, + fullscreen: true, + }); }; render() { @@ -124,3 +161,35 @@ export class PanelHeader extends React.Component { ); } } + +interface PanelEditorProps { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelEditor extends React.Component { + render() { + return ( +
+
+

{this.props.panel.type}

+ + + + +
+ +
testing
+
+ ); + } +} diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index daca8e60f8e..2cf46bfdf53 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -13,6 +13,7 @@ const notPersistedProperties: { [str: string]: boolean } = { events: true, fullscreen: true, isEditing: true, + editModeInitiated: true, }; export class PanelModel { @@ -36,6 +37,7 @@ export class PanelModel { fullscreen: boolean; isEditing: boolean; events: Emitter; + editModeInitiated: boolean; constructor(model) { this.events = new Emitter(); @@ -91,6 +93,10 @@ export class PanelModel { this.events.emit('panel-size-changed'); } + initEditMode() { + this.events.emit('panel-init-edit-mode'); + } + destroy() { this.events.removeAllListeners(); } diff --git a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx b/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx index 872d9296d12..7e952d72d69 100644 --- a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx +++ b/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx @@ -77,13 +77,8 @@ describe('AddPanelPanel', () => { dashboardMock = { toggleRow: jest.fn() }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn(), - }); - panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); }); it('should fetch all panels sorted with core plugins first', () => { diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 1ed2d61df71..3b99c06ad50 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -33,10 +33,6 @@ export class DashboardViewState { self.update(payload); }); - $scope.onAppEvent('panel-initialized', function(evt, payload) { - self.registerPanel(payload.scope); - }); - // this marks changes to location during this digest cycle as not to add history item // don't want url changes like adding orgId to add browser history $location.replace(); @@ -124,102 +120,62 @@ export class DashboardViewState { } syncState() { - if (this.panelScopes.length === 0) { - return; - } - if (this.dashboard.meta.fullscreen) { - var panelScope = this.getPanelScope(this.state.panelId); - if (!panelScope) { + var panel = this.dashboard.getPanelById(this.state.panelId); + + if (!panel) { return; } if (this.fullscreenPanel) { // if already fullscreen - if (this.fullscreenPanel === panelScope && this.editStateChanged === false) { + if (this.fullscreenPanel === panel && this.editStateChanged === false) { return; } else { this.leaveFullscreen(false); } } - if (!panelScope.ctrl.editModeInitiated) { - panelScope.ctrl.initEditMode(); - } - - if (!panelScope.ctrl.fullscreen) { - this.enterFullscreen(panelScope); + if (!panel.fullscreen) { + this.enterFullscreen(panel); } } else if (this.fullscreenPanel) { this.leaveFullscreen(true); } } - getPanelScope(id) { - return _.find(this.panelScopes, function(panelScope) { - return panelScope.ctrl.panel.id === id; - }); - } - leaveFullscreen(render) { - var self = this; - var ctrl = self.fullscreenPanel.ctrl; + var panel = this.fullscreenPanel; - ctrl.editMode = false; - ctrl.fullscreen = false; - - this.dashboard.setViewMode(ctrl.panel, false, false); - this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id }); + this.dashboard.setViewMode(panel, false, false); this.$scope.appEvent('dash-scroll', { restore: true }); if (!render) { return false; } - this.$timeout(function() { - if (self.oldTimeRange !== ctrl.range) { - self.$rootScope.$broadcast('refresh'); + this.$timeout(() => { + if (this.oldTimeRange !== this.dashboard.time) { + this.$rootScope.$broadcast('refresh'); } else { - self.$rootScope.$broadcast('render'); + this.$rootScope.$broadcast('render'); } - delete self.fullscreenPanel; + delete this.fullscreenPanel; }); + return true; } - enterFullscreen(panelScope) { - var ctrl = panelScope.ctrl; + enterFullscreen(panel) { + const isEditing = this.state.edit && this.dashboard.meta.canEdit; - ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit; - ctrl.fullscreen = true; - - this.oldTimeRange = ctrl.range; - this.fullscreenPanel = panelScope; + this.oldTimeRange = this.dashboard.time; + this.fullscreenPanel = panel; // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode() this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); - this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode); - this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id }); - } - - registerPanel(panelScope) { - var self = this; - self.panelScopes.push(panelScope); - - if (!self.dashboard.meta.soloMode) { - if (self.state.panelId === panelScope.ctrl.panel.id) { - if (self.state.edit) { - panelScope.ctrl.editPanel(); - } else { - panelScope.ctrl.viewPanel(); - } - } - } - - var unbind = panelScope.$on('$destroy', function() { - self.panelScopes = _.without(self.panelScopes, panelScope); - unbind(); - }); + console.log('viewstatesrv.setViewMode'); + this.dashboard.setViewMode(panel, true, isEditing); } } From aa5c9f199aa382af6301ad3c2da303fc26926f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jun 2018 14:51:57 +0200 Subject: [PATCH 0014/2611] react panel minor progress --- .../dashboard/dashgrid/DashboardGrid.tsx | 3 +- .../dashboard/dashgrid/DashboardPanel.tsx | 166 ++++-------------- .../dashboard/dashgrid/PanelChrome.tsx | 66 +++++++ .../dashboard/dashgrid/PanelEditor.tsx | 35 ++++ .../dashboard/dashgrid/PanelHeader.tsx | 64 +++++++ .../dashboard/specs/AddPanelPanel.jest.tsx | 2 +- 6 files changed, 200 insertions(+), 136 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/PanelChrome.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelEditor.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 9f2e449b49e..322b8d972d3 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -175,11 +175,10 @@ export class DashboardGrid extends React.Component { const panelElements = []; for (let panel of this.dashboard.panels) { - console.log('panel.fullscreen', panel.fullscreen); const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
- +
); } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index fe97ac6039a..b18d78c5cc4 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,22 +1,18 @@ import React from 'react'; -import $ from 'jquery'; import config from 'app/core/config'; -import classNames from 'classnames'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { AttachedPanel } from './PanelLoader'; import { DashboardRow } from './DashboardRow'; +import { PanelContainer } from './PanelContainer'; import { AddPanelPanel } from './AddPanelPanel'; import { importPluginModule } from 'app/features/plugins/plugin_loader'; -import { store } from 'app/stores/store'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; - -const TITLE_HEIGHT = 27; -const PANEL_BORDER = 2; +import { PanelChrome } from './PanelChrome'; export interface DashboardPanelProps { panel: PanelModel; dashboard: DashboardModel; + panelContainer: PanelContainer; } export class DashboardPanel extends React.Component { @@ -56,140 +52,44 @@ export class DashboardPanel extends React.Component { return ; } + componentDidUpdate() { + // skip loading angular component if we have no element + // or we have already loaded it + if (!this.element || this.attachedPanel) { + return; + } + + const loader = this.props.panelContainer.getPanelLoader(); + this.attachedPanel = loader.load(this.element, this.props.panel, this.props.dashboard); + } + + componentWillUnmount() { + if (this.attachedPanel) { + this.attachedPanel.destroy(); + } + } + render() { if (this.isSpecial()) { return this.specialPanels[this.props.panel.type](); } - let PanelComponent = null; - - if (this.pluginExports && this.pluginExports.PanelComponent) { - PanelComponent = this.pluginExports.PanelComponent; + if (!this.pluginExports) { + console.log('render null'); + return null; } - let panelContentStyle = { - height: this.getPanelHeight(), - }; - - return ( -
-
- -
- {PanelComponent && } -
-
-
- {this.props.panel.isEditing && } -
-
- ); - } - - getPanelHeight() { - const panel = this.props.panel; - let height = 0; - - if (panel.fullscreen) { - var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.4); - var fullscreenHeight = Math.floor(docHeight * 0.8); - height = panel.isEditing ? editHeight : fullscreenHeight; - } else { - height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN; + if (this.pluginExports.PanelComponent) { + return ( + + ); } - return height - PANEL_BORDER + TITLE_HEIGHT; - } -} - -interface PanelHeaderProps { - panel: PanelModel; - dashboard: DashboardModel; -} - -export class PanelHeader extends React.Component { - onEditPanel = () => { - store.view.updateQuery({ - panelId: this.props.panel.id, - edit: true, - fullscreen: true, - }); - }; - - render() { - let isFullscreen = false; - let isLoading = false; - let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - - return ( -
- - - - - - {isLoading && ( - - - - )} - -
- - - {this.props.panel.title} - - - - - - 4m - - -
-
- ); - } -} - -interface PanelEditorProps { - panel: PanelModel; - dashboard: DashboardModel; -} - -export class PanelEditor extends React.Component { - render() { - return ( -
-
-

{this.props.panel.type}

- - - - -
- -
testing
-
- ); + // legacy angular rendering + return
(this.element = element)} className="panel-height-helper" />; } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx new file mode 100644 index 00000000000..25e0875fbef --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import $ from 'jquery'; +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; +import { PanelHeader } from './PanelHeader'; +import { PanelEditor } from './PanelEditor'; + +const TITLE_HEIGHT = 27; +const PANEL_BORDER = 2; + +export interface PanelChromeProps { + panel: PanelModel; + dashboard: DashboardModel; + component: any; +} + +export class PanelChrome extends React.Component { + constructor(props) { + super(props); + + this.props.panel.events.on('panel-size-changed', this.triggerForceUpdate.bind(this)); + } + + triggerForceUpdate() { + this.forceUpdate(); + } + + render() { + let panelContentStyle = { + height: this.getPanelHeight(), + }; + + let PanelComponent = this.props.component; + + return ( +
+
+ +
+ {} +
+
+
+ {this.props.panel.isEditing && } +
+
+ ); + } + + getPanelHeight() { + const panel = this.props.panel; + let height = 0; + + if (panel.fullscreen) { + var docHeight = $(window).height(); + var editHeight = Math.floor(docHeight * 0.4); + var fullscreenHeight = Math.floor(docHeight * 0.8); + height = panel.isEditing ? editHeight : fullscreenHeight; + } else { + height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN; + } + + return height - PANEL_BORDER + TITLE_HEIGHT; + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx new file mode 100644 index 00000000000..ec0d63f9182 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; + +interface PanelEditorProps { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelEditor extends React.Component { + render() { + return ( +
+
+

{this.props.panel.type}

+ + + + +
+ +
testing
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx new file mode 100644 index 00000000000..cb806fefa43 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import classNames from 'classnames'; +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; +import { store } from 'app/stores/store'; + +interface PanelHeaderProps { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelHeader extends React.Component { + onEditPanel = () => { + store.view.updateQuery({ + panelId: this.props.panel.id, + edit: true, + fullscreen: true, + }); + }; + + render() { + let isFullscreen = false; + let isLoading = false; + let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + + return ( +
+ + + + + + {isLoading && ( + + + + )} + +
+ + + {this.props.panel.title} + + + + + + 4m + + +
+
+ ); + } +} diff --git a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx b/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx index 7e952d72d69..9bf99b5720c 100644 --- a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx +++ b/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx @@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({ })); describe('AddPanelPanel', () => { - let wrapper, dashboardMock, getPanelContainer, panel; + let wrapper, dashboardMock, panel; beforeEach(() => { config.panels = [ From db52ea66bd61e7a51699550816cb4ea61dea3f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jun 2018 16:57:55 +0200 Subject: [PATCH 0015/2611] react panels minor progress --- public/app/core/directives/dash_class.ts | 4 +--- .../dashboard/dashgrid/DashboardGrid.tsx | 10 ++++++---- .../dashboard/dashgrid/DashboardPanel.tsx | 1 - .../dashboard/dashgrid/PanelChrome.tsx | 3 ++- .../dashboard/dashgrid/PanelEditor.tsx | 2 -- public/app/features/dashboard/panel_model.ts | 3 --- .../app/features/dashboard/view_state_srv.ts | 1 - public/app/features/panel/panel_ctrl.ts | 19 ++++++++++--------- public/app/features/panel/panel_directive.ts | 4 ++-- public/app/plugins/panel/graph/graph.ts | 2 ++ public/app/plugins/panel/text2/module.tsx | 2 +- public/sass/components/_dashboard_grid.scss | 1 - 12 files changed, 24 insertions(+), 28 deletions(-) diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 1dab57da0d4..c164acf7bfc 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -5,9 +5,7 @@ coreModule.directive('dashClass', function($timeout) { return { link: function($scope, elem) { $scope.ctrl.dashboard.events.on('view-mode-changed', function(panel) { - $timeout(() => { - elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); - }); + elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); }); elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 322b8d972d3..653ed046e8e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -85,7 +85,7 @@ export class DashboardGrid extends React.Component { this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this)); + this.dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this)); this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); } @@ -142,6 +142,10 @@ export class DashboardGrid extends React.Component { } } + onViewModeChanged(payload) { + this.setState({ animated: payload.fullscreen }); + } + updateGridPos(item, layout) { this.panelMap[item.i].updateGridPos(item); @@ -165,9 +169,7 @@ export class DashboardGrid extends React.Component { componentDidMount() { setTimeout(() => { - this.setState(() => { - return { animated: true }; - }); + this.setState({ animated: true }); }); } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index b18d78c5cc4..2aa1b620ea6 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -75,7 +75,6 @@ export class DashboardPanel extends React.Component { } if (!this.pluginExports) { - console.log('render null'); return null; } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 25e0875fbef..30263edfb6c 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -23,7 +23,7 @@ export class PanelChrome extends React.Component { } triggerForceUpdate() { - this.forceUpdate(); + // this.forceUpdate(); } render() { @@ -32,6 +32,7 @@ export class PanelChrome extends React.Component { }; let PanelComponent = this.props.component; + console.log('PanelChrome render'); return (
diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index ec0d63f9182..646a0b65ecd 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -12,8 +12,6 @@ export class PanelEditor extends React.Component { return (
-

{this.props.panel.type}

-
  • Queries diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2cf46bfdf53..0bb2d2755d7 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -13,7 +13,6 @@ const notPersistedProperties: { [str: string]: boolean } = { events: true, fullscreen: true, isEditing: true, - editModeInitiated: true, }; export class PanelModel { @@ -37,7 +36,6 @@ export class PanelModel { fullscreen: boolean; isEditing: boolean; events: Emitter; - editModeInitiated: boolean; constructor(model) { this.events = new Emitter(); @@ -84,7 +82,6 @@ export class PanelModel { this.gridPos.h = newPos.h; if (sizeChanged) { - console.log('PanelModel sizeChanged event and render events fired'); this.events.emit('panel-size-changed'); } } diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 3b99c06ad50..73ec8fc0638 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -174,7 +174,6 @@ export class DashboardViewState { // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode() this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); - console.log('viewstatesrv.setViewMode'); this.dashboard.setViewMode(panel, true, isEditing); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 6402227164f..8f79a789e76 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -24,10 +24,8 @@ export class PanelCtrl { $injector: any; $location: any; $timeout: any; - fullscreen: boolean; inspector: any; editModeInitiated: boolean; - editMode: any; height: any; containerHeight: any; events: Emitter; @@ -130,6 +128,7 @@ export class PanelCtrl { return { templateUrl: directiveFn }; }; } + if (index) { this.editorTabs.splice(index, 0, editorTab); } else { @@ -190,7 +189,7 @@ export class PanelCtrl { getExtendedMenu() { let menu = []; - if (!this.fullscreen && this.dashboard.meta.canEdit) { + if (!this.panel.fullscreen && this.dashboard.meta.canEdit) { menu.push({ text: 'Duplicate', click: 'ctrl.duplicate()', @@ -220,15 +219,15 @@ export class PanelCtrl { } otherPanelInFullscreenMode() { - return this.dashboard.meta.fullscreen && !this.fullscreen; + return this.dashboard.meta.fullscreen && !this.panel.fullscreen; } calculatePanelHeight() { - if (this.fullscreen) { + if (this.panel.fullscreen) { var docHeight = $(window).height(); var editHeight = Math.floor(docHeight * 0.4); var fullscreenHeight = Math.floor(docHeight * 0.8); - this.containerHeight = this.editMode ? editHeight : fullscreenHeight; + this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight; } else { this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN; } @@ -237,6 +236,11 @@ export class PanelCtrl { this.containerHeight = $(window).height(); } + // hacky solution + if (this.panel.isEditing && !this.editModeInitiated) { + this.initEditMode(); + } + this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT); } @@ -247,9 +251,6 @@ export class PanelCtrl { duplicate() { this.dashboard.duplicatePanel(this.panel); - this.$timeout(() => { - this.$scope.$root.$broadcast('render'); - }); } removePanel() { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index e549ca262d3..685e527c944 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -7,7 +7,7 @@ var module = angular.module('grafana.directives'); var panelTemplate = `
    -
    +
    @@ -25,7 +25,7 @@ var panelTemplate = `
    -
    +

    diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 9e4fb42952e..33b8c8535fa 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -197,6 +197,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { // Function for rendering panel function render_panel() { panelWidth = elem.width(); + console.log('panelWidth', panelWidth); + if (shouldAbortRender()) { return; } diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 7f5e363891c..8987b738f0b 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -6,7 +6,7 @@ export class ReactTestPanel extends React.Component { } render() { - return

    Panel content

    ; + return

    I am a react panel, haha!

    ; } } diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index f1908ca8786..26326013dab 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -20,7 +20,6 @@ } // Disable grid interaction indicators in fullscreen panels - .panel-header:hover { background-color: inherit; } From 13bc9f6fb2932559074fbabcb755879ed8c03064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jun 2018 17:30:10 +0200 Subject: [PATCH 0016/2611] updated --- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 2 +- public/app/plugins/panel/graph/graph.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 30263edfb6c..a0a1dff7c16 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -23,7 +23,7 @@ export class PanelChrome extends React.Component { } triggerForceUpdate() { - // this.forceUpdate(); + this.forceUpdate(); } render() { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 33b8c8535fa..09fea9d6a37 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -197,7 +197,6 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { // Function for rendering panel function render_panel() { panelWidth = elem.width(); - console.log('panelWidth', panelWidth); if (shouldAbortRender()) { return; From 230606146d6db5afaad28ab591188c2cd1a248ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jun 2018 21:25:57 +0200 Subject: [PATCH 0017/2611] wip: react panel minor progrss --- public/app/core/components/grafana_app.ts | 5 +++ public/app/core/services/angular_loader.ts | 42 +++++++++++++++++++ public/app/features/dashboard/all.ts | 1 - .../app/features/dashboard/dashboard_ctrl.ts | 10 +---- .../dashboard/dashgrid/DashboardGrid.tsx | 34 +++++++-------- .../dashgrid/DashboardGridDirective.ts | 4 +- .../dashboard/dashgrid/DashboardPanel.tsx | 18 ++++---- .../dashboard/dashgrid/PanelChrome.tsx | 7 +--- .../dashboard/dashgrid/PanelContainer.ts | 7 ---- .../dashboard/dashgrid/PanelEditor.tsx | 36 +++++++++++++++- .../dashboard/dashgrid/PanelLoader.ts | 31 -------------- .../app/features/plugins/plugin_component.ts | 1 + public/app/partials/dashboard.html | 3 +- public/sass/components/_tabbed_view.scss | 9 ++++ 14 files changed, 121 insertions(+), 87 deletions(-) create mode 100644 public/app/core/services/angular_loader.ts delete mode 100644 public/app/features/dashboard/dashgrid/PanelContainer.ts delete mode 100644 public/app/features/dashboard/dashgrid/PanelLoader.ts diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index fd2e32db3a7..a888e2973e9 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 { AngularLoader, setAngularLoader } from 'app/core/services/angular_loader'; export class GrafanaCtrl { /** @ngInject */ @@ -22,8 +23,12 @@ export class GrafanaCtrl { contextSrv, bridgeSrv, backendSrv: BackendSrv, + angularLoader: AngularLoader, datasourceSrv: DatasourceSrv ) { + // make angular loader service available to react components + setAngularLoader(angularLoader); + // create store with env services createStore({ backendSrv, datasourceSrv }); $scope.init = function() { diff --git a/public/app/core/services/angular_loader.ts b/public/app/core/services/angular_loader.ts new file mode 100644 index 00000000000..36c49be6240 --- /dev/null +++ b/public/app/core/services/angular_loader.ts @@ -0,0 +1,42 @@ +import angular from 'angular'; +import coreModule from 'app/core/core_module'; +import _ from 'lodash'; + +export interface AngularComponent { + destroy(); +} + +export class AngularLoader { + /** @ngInject */ + constructor(private $compile, private $rootScope) {} + + load(elem, scopeProps, template): AngularComponent { + var scope = this.$rootScope.$new(); + + _.assign(scope, scopeProps); + + const compiledElem = this.$compile(template)(scope); + const rootNode = angular.element(elem); + rootNode.append(compiledElem); + + return { + destroy: () => { + scope.$destroy(); + compiledElem.remove(); + }, + }; + } +} + +coreModule.service('angularLoader', AngularLoader); + +let angularLoaderInstance: AngularLoader; + +export function setAngularLoader(pl: AngularLoader) { + angularLoaderInstance = pl; +} + +// away to access it from react +export function getAngularLoader(): AngularLoader { + return angularLoaderInstance; +} diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index a8f491f3ddd..6898b51d095 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -22,7 +22,6 @@ import './export_data/export_data_modal'; import './ad_hoc_filters'; import './repeat_option/repeat_option'; import './dashgrid/DashboardGridDirective'; -import './dashgrid/PanelLoader'; import './dashgrid/RowOptions'; import './folder_picker/folder_picker'; import './move_to_folder_modal/move_to_folder'; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 94d0b18f157..a7d1ff23ea4 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -1,11 +1,10 @@ import config from 'app/core/config'; import coreModule from 'app/core/core_module'; -import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; -export class DashboardCtrl implements PanelContainer { +export class DashboardCtrl { dashboard: DashboardModel; dashboardViewState: any; loadedFallbackDashboard: boolean; @@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer { private dashboardSrv, private unsavedChangesSrv, private dashboardViewStateSrv, - public playlistSrv, - private panelLoader + public playlistSrv ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -119,10 +117,6 @@ export class DashboardCtrl implements PanelContainer { return this.dashboard; } - getPanelLoader() { - return this.panelLoader; - } - timezoneChanged() { this.$rootScope.$broadcast('refresh'); } diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 653ed046e8e..9ee6cdbe1f8 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -3,7 +3,6 @@ import ReactGridLayout from 'react-grid-layout'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { DashboardPanel } from './DashboardPanel'; import { DashboardModel } from '../dashboard_model'; -import { PanelContainer } from './PanelContainer'; import { PanelModel } from '../panel_model'; import classNames from 'classnames'; import sizeMe from 'react-sizeme'; @@ -60,18 +59,15 @@ function GridWrapper({ const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); export interface DashboardGridProps { - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardGrid extends React.Component { gridToPanelMap: any; - panelContainer: PanelContainer; - dashboard: DashboardModel; panelMap: { [id: string]: PanelModel }; constructor(props) { super(props); - this.panelContainer = this.props.getPanelContainer(); this.onLayoutChange = this.onLayoutChange.bind(this); this.onResize = this.onResize.bind(this); this.onResizeStop = this.onResizeStop.bind(this); @@ -81,20 +77,20 @@ export class DashboardGrid extends React.Component { this.state = { animated: false }; // subscribe to dashboard events - this.dashboard = this.panelContainer.getDashboard(); - this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); - this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this)); - this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); + let dashboard = this.props.dashboard; + dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); + dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); + dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); + dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this)); + dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); + dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); } buildLayout() { const layout = []; this.panelMap = {}; - for (let panel of this.dashboard.panels) { + for (let panel of this.props.dashboard.panels) { let stringId = panel.id.toString(); this.panelMap[stringId] = panel; @@ -129,7 +125,7 @@ export class DashboardGrid extends React.Component { this.panelMap[newPos.i].updateGridPos(newPos); } - this.dashboard.sortPanelsByGridPos(); + this.props.dashboard.sortPanelsByGridPos(); } triggerForceUpdate() { @@ -137,7 +133,7 @@ export class DashboardGrid extends React.Component { } onWidthChange() { - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { panel.resizeDone(); } } @@ -176,11 +172,11 @@ export class DashboardGrid extends React.Component { renderPanels() { const panelElements = []; - for (let panel of this.dashboard.panels) { + for (let panel of this.props.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
    - +
    ); } @@ -193,8 +189,8 @@ export class DashboardGrid extends React.Component { { element: any; - attachedPanel: AttachedPanel; + angularPanel: AngularComponent; pluginInfo: any; pluginExports: any; specialPanels = {}; @@ -55,17 +53,19 @@ export class DashboardPanel extends React.Component { componentDidUpdate() { // skip loading angular component if we have no element // or we have already loaded it - if (!this.element || this.attachedPanel) { + if (!this.element || this.angularPanel) { return; } - const loader = this.props.panelContainer.getPanelLoader(); - this.attachedPanel = loader.load(this.element, this.props.panel, this.props.dashboard); + let loader = getAngularLoader(); + var template = ''; + let scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard }; + this.angularPanel = loader.load(this.element, scopeProps, template); } componentWillUnmount() { - if (this.attachedPanel) { - this.attachedPanel.destroy(); + if (this.angularPanel) { + this.angularPanel.destroy(); } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index a0a1dff7c16..bf5f8044a37 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -32,7 +32,6 @@ export class PanelChrome extends React.Component { }; let PanelComponent = this.props.component; - console.log('PanelChrome render'); return (
    @@ -42,9 +41,7 @@ export class PanelChrome extends React.Component { {}

    -
    - {this.props.panel.isEditing && } -
    + {this.props.panel.isEditing && }
    ); } @@ -55,7 +52,7 @@ export class PanelChrome extends React.Component { if (panel.fullscreen) { var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.4); + var editHeight = Math.floor(docHeight * 0.3); var fullscreenHeight = Math.floor(docHeight * 0.8); height = panel.isEditing ? editHeight : fullscreenHeight; } else { diff --git a/public/app/features/dashboard/dashgrid/PanelContainer.ts b/public/app/features/dashboard/dashgrid/PanelContainer.ts deleted file mode 100644 index 87f3235a176..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelContainer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DashboardModel } from '../dashboard_model'; -import { PanelLoader } from './PanelLoader'; - -export interface PanelContainer { - getPanelLoader(): PanelLoader; - getDashboard(): DashboardModel; -} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 646a0b65ecd..fc7967d087c 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; +import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader'; interface PanelEditorProps { panel: PanelModel; @@ -8,9 +9,38 @@ interface PanelEditorProps { } export class PanelEditor extends React.Component { + queryElement: any; + queryComp: AngularComponent; + + constructor(props) { + super(props); + } + + componentDidMount() { + if (!this.queryElement) { + return; + } + + let loader = getAngularLoader(); + var template = ''; + let scopeProps = { + ctrl: { + panel: this.props.panel, + dashboard: this.props.dashboard, + panelCtrl: { + panel: this.props.panel, + dashboard: this.props.dashboard, + }, + }, + target: {}, + }; + + this.queryComp = loader.load(this.queryElement, scopeProps, template); + } + render() { return ( -
    +
    • @@ -26,7 +56,9 @@ export class PanelEditor extends React.Component {
    -
    testing
    +
    +
    (this.queryElement = element)} className="panel-height-helper" /> +
    ); } diff --git a/public/app/features/dashboard/dashgrid/PanelLoader.ts b/public/app/features/dashboard/dashgrid/PanelLoader.ts deleted file mode 100644 index beda30bdff7..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelLoader.ts +++ /dev/null @@ -1,31 +0,0 @@ -import angular from 'angular'; -import coreModule from 'app/core/core_module'; - -export interface AttachedPanel { - destroy(); -} - -export class PanelLoader { - /** @ngInject */ - constructor(private $compile, private $rootScope) {} - - load(elem, panel, dashboard): AttachedPanel { - var template = ''; - var panelScope = this.$rootScope.$new(); - panelScope.panel = panel; - panelScope.dashboard = dashboard; - - const compiledElem = this.$compile(template)(panelScope); - const rootNode = angular.element(elem); - rootNode.append(compiledElem); - - return { - destroy: () => { - panelScope.$destroy(); - compiledElem.remove(); - }, - }; - } -} - -coreModule.service('panelLoader', PanelLoader); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 1936e57f558..2ca3dd6bd0b 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -110,6 +110,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ let datasource = scope.target.datasource || scope.ctrl.panel.datasource; return datasourceSrv.get(datasource).then(ds => { scope.datasource = ds; + console.log('scope', scope); return importPluginModule(ds.meta.module).then(dsModule => { return { diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 9506587c515..9e7d4fa1c6c 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -11,8 +11,7 @@ - - +
    diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index bf95d453504..a5d38d292a1 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -10,6 +10,15 @@ background: none; } } + + &.tabbed-view--panel-edit-new { + padding: 10px 0 0 0; + + .tabbed-view-header { + padding: 0px; + background: none; + } + } } .tabbed-view-header { From 1099daec38a8ed360e97ee7d018b90a5984cfef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 20 Jun 2018 12:05:03 +0200 Subject: [PATCH 0018/2611] wip: react panels, query editor loading from react PanelEditor view --- public/app/features/dashboard/panel_model.ts | 5 +++++ public/app/features/panel/metrics_panel_ctrl.ts | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 0bb2d2755d7..2de409eda57 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -31,6 +31,7 @@ export class PanelModel { collapsed?: boolean; panels?: any; soloMode?: boolean; + targets: any[]; // non persisted fullscreen: boolean; @@ -48,6 +49,10 @@ export class PanelModel { if (!this.gridPos) { this.gridPos = { x: 0, y: 0, h: 3, w: 6 }; } + + if (!this.targets) { + this.targets = [{}]; + } } getSaveModel() { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 75c0de3bc6e..ba97ce79c1f 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -45,10 +45,6 @@ class MetricsPanelCtrl extends PanelCtrl { this.scope = $scope; this.panel.datasource = this.panel.datasource || null; - if (!this.panel.targets) { - this.panel.targets = [{}]; - } - this.events.on('refresh', this.onMetricsPanelRefresh.bind(this)); this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this)); this.events.on('panel-teardown', this.onPanelTearDown.bind(this)); From cec70c1ed8e564737ace0e87ff5423cf1ca6f8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Jun 2018 16:32:01 +0200 Subject: [PATCH 0019/2611] feat: panels v2, metrics-tab loading --- .../app/features/dashboard/dashgrid/PanelEditor.tsx | 3 +-- public/app/features/dashboard/panel_model.ts | 12 +++++------- public/app/features/panel/metrics_tab.ts | 3 +++ yarn.lock | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index fc7967d087c..fa34dde1ab8 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -22,7 +22,7 @@ export class PanelEditor extends React.Component { } let loader = getAngularLoader(); - var template = ''; + var template = ''; let scopeProps = { ctrl: { panel: this.props.panel, @@ -32,7 +32,6 @@ export class PanelEditor extends React.Component { dashboard: this.props.dashboard, }, }, - target: {}, }; this.queryComp = loader.load(this.queryElement, scopeProps, template); diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2de409eda57..6ee49886a06 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -32,6 +32,7 @@ export class PanelModel { panels?: any; soloMode?: boolean; targets: any[]; + datasource: string; // non persisted fullscreen: boolean; @@ -46,13 +47,10 @@ export class PanelModel { this[property] = model[property]; } - if (!this.gridPos) { - this.gridPos = { x: 0, y: 0, h: 3, w: 6 }; - } - - if (!this.targets) { - this.targets = [{}]; - } + // defaults + this.gridPos = this.gridPos || { x: 0, y: 0, h: 3, w: 6 }; + this.datasource = this.datasource || null; + this.targets = this.targets || [{}]; } getSaveModel() { diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 4da40f214a1..b8b313ae198 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,5 +1,6 @@ import { DashboardModel } from '../dashboard/dashboard_model'; import Remarkable from 'remarkable'; +import coreModule from 'app/core/core_module'; export class MetricsTabCtrl { dsName: string; @@ -120,3 +121,5 @@ export function metricsTabDirective() { controller: MetricsTabCtrl, }; } + +coreModule.directive('metricsTab', metricsTabDirective); diff --git a/yarn.lock b/yarn.lock index 6cc48a7c79d..6772d7c14a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1114,7 +1114,7 @@ babel-plugin-istanbul@^4.1.5, babel-plugin-istanbul@^4.1.6: istanbul-lib-instrument "^1.10.1" test-exclude "^4.2.1" -babel-plugin-jest-hoist@^22.4.4: +babel-plugin-jest-hoist@^22.4.3, babel-plugin-jest-hoist@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.4.tgz#b9851906eab34c7bf6f8c895a2b08bea1a844c0b" @@ -6498,7 +6498,7 @@ jest-docblock@^22.4.0, jest-docblock@^22.4.3: dependencies: detect-newline "^2.1.0" -jest-environment-jsdom@^22.4.1: +jest-environment-jsdom@^22.4.1, jest-environment-jsdom@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz#d67daa4155e33516aecdd35afd82d4abf0fa8a1e" dependencies: @@ -6506,7 +6506,7 @@ jest-environment-jsdom@^22.4.1: jest-util "^22.4.3" jsdom "^11.5.1" -jest-environment-node@^22.4.1: +jest-environment-node@^22.4.1, jest-environment-node@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.4.3.tgz#54c4eaa374c83dd52a9da8759be14ebe1d0b9129" dependencies: @@ -6533,7 +6533,7 @@ jest-haste-map@^22.4.2: micromatch "^2.3.11" sane "^2.0.0" -jest-jasmine2@^22.4.4: +jest-jasmine2@^22.4.3, jest-jasmine2@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.4.4.tgz#c55f92c961a141f693f869f5f081a79a10d24e23" dependencies: @@ -6587,7 +6587,7 @@ jest-resolve-dependencies@^22.1.0: dependencies: jest-regex-util "^22.4.3" -jest-resolve@^22.4.2: +jest-resolve@^22.4.2, jest-resolve@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.4.3.tgz#0ce9d438c8438229aa9b916968ec6b05c1abb4ea" dependencies: @@ -6671,7 +6671,7 @@ jest-validate@^21.1.0: leven "^2.1.0" pretty-format "^21.2.1" -jest-validate@^22.4.4: +jest-validate@^22.4.3, jest-validate@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.4.4.tgz#1dd0b616ef46c995de61810d85f57119dbbcec4d" dependencies: From 2f5bcd37ec685e3a143c35d655ca39c83d66447b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Jun 2018 16:45:42 +0200 Subject: [PATCH 0020/2611] react panels wip --- public/app/features/dashboard/dashgrid/PanelEditor.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index fa34dde1ab8..45733559b68 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -11,9 +11,15 @@ interface PanelEditorProps { export class PanelEditor extends React.Component { queryElement: any; queryComp: AngularComponent; + tabs: any[]; constructor(props) { super(props); + + this.tabs = [ + { id: 'queries', text: 'Queries', icon: 'fa fa-database' }, + { id: 'viz', text: 'Visualization', icon: 'fa fa-line-chart' }, + ]; } componentDidMount() { @@ -37,6 +43,8 @@ export class PanelEditor extends React.Component { this.queryComp = loader.load(this.queryElement, scopeProps, template); } + onChangeTab = tabName => {}; + render() { return (
    From 682c792dfb173c64cf8f904b4ca1efef3d4e0eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Jun 2018 12:07:41 -0700 Subject: [PATCH 0021/2611] wip: react panels editor mode, tabs working --- .../dashboard/dashgrid/PanelEditor.tsx | 92 +++++++++++-------- .../dashboard/dashgrid/PanelHeader.tsx | 13 ++- .../dashboard/dashgrid/QueriesTab.tsx | 49 ++++++++++ public/app/stores/ViewStore/ViewStore.ts | 6 +- 4 files changed, 115 insertions(+), 45 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/QueriesTab.tsx diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 45733559b68..c1350729ddd 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -1,61 +1,54 @@ import React from 'react'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader'; +import { store } from 'app/stores/store'; +import { observer } from 'mobx-react'; +import { QueriesTab } from './QueriesTab'; +import classNames from 'classnames'; interface PanelEditorProps { panel: PanelModel; dashboard: DashboardModel; } -export class PanelEditor extends React.Component { - queryElement: any; - queryComp: AngularComponent; - tabs: any[]; +interface PanelEditorTab { + id: string; + text: string; + icon: string; +} +@observer +export class PanelEditor extends React.Component { constructor(props) { super(props); + } - this.tabs = [ + renderQueriesTab() { + return ; + } + + renderVizTab() { + return

    Visualizations

    ; + } + + onChangeTab = (tab: PanelEditorTab) => { + store.view.updateQuery({ tab: tab.id }, false); + }; + + render() { + const activeTab: string = store.view.query.get('tab') || 'queries'; + const tabs: PanelEditorTab[] = [ { id: 'queries', text: 'Queries', icon: 'fa fa-database' }, { id: 'viz', text: 'Visualization', icon: 'fa fa-line-chart' }, ]; - } - componentDidMount() { - if (!this.queryElement) { - return; - } - - let loader = getAngularLoader(); - var template = ''; - let scopeProps = { - ctrl: { - panel: this.props.panel, - dashboard: this.props.dashboard, - panelCtrl: { - panel: this.props.panel, - dashboard: this.props.dashboard, - }, - }, - }; - - this.queryComp = loader.load(this.queryElement, scopeProps, template); - } - - onChangeTab = tabName => {}; - - render() { return (
    -
    (this.queryElement = element)} className="panel-height-helper" /> + {activeTab === 'queries' && this.renderQueriesTab()} + {activeTab === 'viz' && this.renderVizTab()}
    ); } } + +interface TabItemParams { + tab: PanelEditorTab; + activeTab: string; + onClick: (tab: PanelEditorTab) => void; +} + +function TabItem({ tab, activeTab, onClick }: TabItemParams) { + const tabClasses = classNames({ + 'gf-tabs-link': true, + active: activeTab === tab.id, + }); + + return ( +
  • + onClick(tab)}> + + {tab.text} + +
  • + ); +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx index cb806fefa43..97d41e15a0c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -11,11 +11,14 @@ interface PanelHeaderProps { export class PanelHeader extends React.Component { onEditPanel = () => { - store.view.updateQuery({ - panelId: this.props.panel.id, - edit: true, - fullscreen: true, - }); + store.view.updateQuery( + { + panelId: this.props.panel.id, + edit: true, + fullscreen: true, + }, + false + ); }; render() { diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx new file mode 100644 index 00000000000..4cfc65c983b --- /dev/null +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; +import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader'; + +interface Props { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class QueriesTab extends React.Component { + element: any; + component: AngularComponent; + + constructor(props) { + super(props); + } + + componentDidMount() { + if (!this.element) { + return; + } + + let loader = getAngularLoader(); + var template = ''; + let scopeProps = { + ctrl: { + panel: this.props.panel, + dashboard: this.props.dashboard, + panelCtrl: { + panel: this.props.panel, + dashboard: this.props.dashboard, + }, + }, + }; + + this.component = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.component) { + this.component.destroy(); + } + } + + render() { + return
    (this.element = element)} className="panel-height-helper" />; + } +} diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index ba966a194d8..83cb01d4bd4 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -23,8 +23,10 @@ export const ViewStore = types })) .actions(self => { // querystring only - function updateQuery(query: any) { - self.query.clear(); + function updateQuery(query: any, clear = true) { + if (clear) { + self.query.clear(); + } for (let key of Object.keys(query)) { if (query[key]) { self.query.set(key, query[key]); From 70c808130f2b95e4a49989aaf266a4cd42fa9676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 Jun 2018 04:31:55 -0700 Subject: [PATCH 0022/2611] react panels wip --- .../dashboard/dashgrid/PanelEditor.tsx | 13 +++++++---- public/app/features/panel/DataPanel.tsx | 23 +++++++++++++++++++ .../app/features/plugins/plugin_component.ts | 1 - 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 public/app/features/panel/DataPanel.tsx diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index c1350729ddd..ebcde802b7f 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -19,8 +19,15 @@ interface PanelEditorTab { @observer export class PanelEditor extends React.Component { + tabs: PanelEditorTab[]; + constructor(props) { super(props); + + this.tabs = [ + { id: 'queries', text: 'Queries', icon: 'fa fa-database' }, + { id: 'viz', text: 'Visualization', icon: 'fa fa-line-chart' }, + ]; } renderQueriesTab() { @@ -37,16 +44,12 @@ export class PanelEditor extends React.Component { render() { const activeTab: string = store.view.query.get('tab') || 'queries'; - const tabs: PanelEditorTab[] = [ - { id: 'queries', text: 'Queries', icon: 'fa fa-database' }, - { id: 'viz', text: 'Visualization', icon: 'fa fa-line-chart' }, - ]; return (
      - {tabs.map(tab => { + {this.tabs.map(tab => { return ; })}
    diff --git a/public/app/features/panel/DataPanel.tsx b/public/app/features/panel/DataPanel.tsx new file mode 100644 index 00000000000..5ea490e497d --- /dev/null +++ b/public/app/features/panel/DataPanel.tsx @@ -0,0 +1,23 @@ +import React, { Component, ComponentClass } from 'react'; +import _ from 'lodash'; + +export interface Props { + type: string; + queries: Query[]; +} + +interface State { + isLoading: boolean; + timeSeries: TimeSeriesServerResponse[]; +} + +export interface OriginalProps { + data: TimeSeriesServerResponse[]; + isLoading: boolean; +} + +const DataPanel = (ComposedComponent: ComponentClass) => { + class Wrapper extends Component {} + + return Wrapper; +}; diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 2ca3dd6bd0b..1936e57f558 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -110,7 +110,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ let datasource = scope.target.datasource || scope.ctrl.panel.datasource; return datasourceSrv.get(datasource).then(ds => { scope.datasource = ds; - console.log('scope', scope); return importPluginModule(ds.meta.module).then(dsModule => { return { From ab9e1b35cdf48ccb4d303d4960b7fca9aaea85c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 1 Jul 2018 17:34:42 +0200 Subject: [PATCH 0023/2611] wip: minor progress on DataPanel --- .../dashboard/dashgrid/PanelChrome.tsx | 4 +- public/app/features/panel/DataPanel.tsx | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 public/app/features/panel/DataPanel.tsx diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index bf5f8044a37..3fd9df799af 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -9,13 +9,13 @@ import { PanelEditor } from './PanelEditor'; const TITLE_HEIGHT = 27; const PANEL_BORDER = 2; -export interface PanelChromeProps { +export interface Props { panel: PanelModel; dashboard: DashboardModel; component: any; } -export class PanelChrome extends React.Component { +export class PanelChrome extends React.Component { constructor(props) { super(props); diff --git a/public/app/features/panel/DataPanel.tsx b/public/app/features/panel/DataPanel.tsx new file mode 100644 index 00000000000..46418516a26 --- /dev/null +++ b/public/app/features/panel/DataPanel.tsx @@ -0,0 +1,78 @@ +import React, { Component, ComponentClass } from 'react'; + +export interface OuterProps { + type: string; + queries: any[]; + isVisible: boolean; +} + +export interface AddedProps { + data: any[]; +} + +interface State { + isLoading: boolean; + data: any[]; +} + +const DataPanel = (ComposedComponent: ComponentClass) => { + class Wrapper extends Component { + public static defaultProps = { + isVisible: true, + }; + + constructor(props: OuterProps) { + super(props); + + this.state = { + isLoading: false, + data: [], + }; + } + + public componentDidMount() { + this.issueQueries(); + } + + public issueQueries = () => { + const { queries, isVisible } = this.props; + + if (!isVisible) { + return; + } + + if (!queries.length) { + this.setState({ data: [{ message: 'no queries' }] }); + return; + } + + this.setState({ isLoading: true }); + }; + + public render() { + const { data, isLoading } = this.state; + + if (!data.length) { + return ( +
    +

    No Data

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

    Loading

    +
    + ); + } + + return ; + } + } + + return Wrapper; +}; + +export default DataPanel; From c86fc6fb47f9dead091ef0e2b6f048c6e292e716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Jul 2018 13:10:39 -0700 Subject: [PATCH 0024/2611] react-panels: minor progress on data flow --- .../dashboard/dashgrid/DashboardPanel.tsx | 25 ++++++----- .../dashgrid}/DataPanel.tsx | 28 +++++++----- .../dashboard/dashgrid/PanelChrome.tsx | 44 ++++++++++++------- public/app/features/plugins/plugin_loader.ts | 13 +++++- public/app/plugins/panel/text2/module.tsx | 14 ++++-- 5 files changed, 84 insertions(+), 40 deletions(-) rename public/app/features/{panel => dashboard/dashgrid}/DataPanel.tsx (67%) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index c45d3c15bf0..3d2542d63c8 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -5,24 +5,27 @@ import { DashboardModel } from '../dashboard_model'; import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader'; import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; -import { importPluginModule } from 'app/features/plugins/plugin_loader'; +import { importPluginModule, PluginExports } from 'app/features/plugins/plugin_loader'; import { PanelChrome } from './PanelChrome'; -export interface DashboardPanelProps { +export interface Props { panel: PanelModel; dashboard: DashboardModel; } -export class DashboardPanel extends React.Component { +export interface State { + pluginExports: PluginExports; +} + +export class DashboardPanel extends React.Component { element: any; angularPanel: AngularComponent; pluginInfo: any; - pluginExports: any; specialPanels = {}; constructor(props) { super(props); - this.state = {}; + this.state = { pluginExports: null }; this.specialPanels['row'] = this.renderRow.bind(this); this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); @@ -32,8 +35,7 @@ export class DashboardPanel extends React.Component { // load panel plugin importPluginModule(this.pluginInfo.module).then(pluginExports => { - this.pluginExports = pluginExports; - this.forceUpdate(); + this.setState({ pluginExports: pluginExports }); }); } } @@ -70,18 +72,21 @@ export class DashboardPanel extends React.Component { } render() { + const { pluginExports } = this.state; + if (this.isSpecial()) { return this.specialPanels[this.props.panel.type](); } - if (!this.pluginExports) { + if (!pluginExports) { return null; } - if (this.pluginExports.PanelComponent) { + if (pluginExports.PanelComponent) { return ( diff --git a/public/app/features/panel/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx similarity index 67% rename from public/app/features/panel/DataPanel.tsx rename to public/app/features/dashboard/dashgrid/DataPanel.tsx index a1669f6603d..251bd243b90 100644 --- a/public/app/features/panel/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,5 +1,4 @@ import React, { Component, ComponentClass } from 'react'; -import _ from 'lodash'; export interface OuterProps { type: string; @@ -7,16 +6,19 @@ export interface OuterProps { isVisible: boolean; } -export interface AddedProps { +export interface PanelProps extends OuterProps { data: any[]; } +export interface DataPanel extends ComponentClass { +} + interface State { isLoading: boolean; data: any[]; } -const DataPanel = (ComposedComponent: ComponentClass) => { +export const DataPanelWrapper = (ComposedComponent: ComponentClass) => { class Wrapper extends Component { public static defaultProps = { isVisible: true, @@ -32,26 +34,31 @@ const DataPanel = (ComposedComponent: ComponentClass) = } public componentDidMount() { + console.log('data panel mount'); this.issueQueries(); } - public issueQueries = () => { - const { queries, isVisible } = this.props; + public issueQueries = async () => { + const { isVisible } = this.props; if (!isVisible) { return; } - if (!queries.length) { - this.setState({ data: [{ message: 'no queries' }] }); - return; - } - this.setState({ isLoading: true }); + + await new Promise(resolve => { + setTimeout(() => { + + this.setState({ isLoading: false, data: [{value: 10}] }); + + }, 500); + }); }; public render() { const { data, isLoading } = this.state; + console.log('data panel render'); if (!data.length) { return ( @@ -76,4 +83,3 @@ const DataPanel = (ComposedComponent: ComponentClass) = return Wrapper; }; -export default DataPanel; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 3fd9df799af..ebe25379c2d 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { ComponentClass } from 'react'; import $ from 'jquery'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { PanelHeader } from './PanelHeader'; import { PanelEditor } from './PanelEditor'; +import { DataPanel, PanelProps, DataPanelWrapper } from './DataPanel'; const TITLE_HEIGHT = 27; const PANEL_BORDER = 2; @@ -12,33 +13,46 @@ const PANEL_BORDER = 2; export interface Props { panel: PanelModel; dashboard: DashboardModel; - component: any; + component: ComponentClass; } -export class PanelChrome extends React.Component { +interface State { + height: number; +} + +export class PanelChrome extends React.Component { + panelComponent: DataPanel; + constructor(props) { super(props); - this.props.panel.events.on('panel-size-changed', this.triggerForceUpdate.bind(this)); - } - - triggerForceUpdate() { - this.forceUpdate(); - } - - render() { - let panelContentStyle = { + this.state = { height: this.getPanelHeight(), }; - let PanelComponent = this.props.component; + this.panelComponent = DataPanelWrapper(this.props.component); + this.props.panel.events.on('panel-size-changed', this.onPanelSizeChanged); + } + + onPanelSizeChanged = () => { + this.setState({ + height: this.getPanelHeight(), + }); + }; + + componentDidMount() { + console.log('panel chrome mounted'); + } + + render() { + let PanelComponent = this.panelComponent; return (
    -
    - {} +
    + {}
    {this.props.panel.isEditing && } diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index f999ee7e2ff..ffcd36312fb 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -138,11 +138,22 @@ const flotDeps = [ 'jquery.flot.stackpercent', 'jquery.flot.events', ]; + for (let flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } -export function importPluginModule(path: string): Promise { +export interface PluginExports { + PanelCtrl?; + any; + PanelComponent?: any; + Datasource?: any; + QueryCtrl?: any; + ConfigCtrl?: any; + AnnotationsQueryCtrl?: any; +} + +export function importPluginModule(path: string): Promise { let builtIn = builtInPlugins[path]; if (builtIn) { return Promise.resolve(builtIn); diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 8987b738f0b..019cf912340 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,12 +1,20 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; -export class ReactTestPanel extends React.Component { +export class ReactTestPanel extends PureComponent { constructor(props) { super(props); } render() { - return

    I am a react panel, haha!

    ; + const { data } = this.props; + let value = 0; + + if (data.length) { + value = data[0].value; + } + + return

    I am a react value: {value}

    ; } } From dec62d73401ddce4ecda6b3162018220ca87a7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 6 Jul 2018 04:42:59 -0700 Subject: [PATCH 0025/2611] another baby step --- .../dashboard/dashgrid/DashboardGrid.tsx | 2 +- .../dashboard/dashgrid/PanelEditor.tsx | 12 ++++- .../features/dashboard/dashgrid/VizPicker.tsx | 46 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/VizPicker.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 9ee6cdbe1f8..30d97898900 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -139,7 +139,7 @@ export class DashboardGrid extends React.Component { } onViewModeChanged(payload) { - this.setState({ animated: payload.fullscreen }); + this.setState({ animated: !payload.fullscreen }); } updateGridPos(item, layout) { diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index ebcde802b7f..1b92127c1d3 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -5,6 +5,7 @@ import { store } from 'app/stores/store'; import { observer } from 'mobx-react'; import { QueriesTab } from './QueriesTab'; import classNames from 'classnames'; +import { VizPicker } from './VizPicker'; interface PanelEditorProps { panel: PanelModel; @@ -35,7 +36,16 @@ export class PanelEditor extends React.Component { } renderVizTab() { - return

    Visualizations

    ; + return ( +
    +
    + +
    +
    +
    Options
    +
    +
    + ); } onChangeTab = (tab: PanelEditorTab) => { diff --git a/public/app/features/dashboard/dashgrid/VizPicker.tsx b/public/app/features/dashboard/dashgrid/VizPicker.tsx new file mode 100644 index 00000000000..2d879ef94d4 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VizPicker.tsx @@ -0,0 +1,46 @@ +import React, { PureComponent } from 'react'; +import config from 'app/core/config'; +import _ from 'lodash'; + +interface Props {} + +interface State { + pluginList: any[]; +} + +export class VizPicker extends PureComponent { + constructor(props) { + super(props); + + this.state = { + pluginList: this.getPanelPlugins(''), + }; + } + + getPanelPlugins(filter) { + let panels = _.chain(config.panels) + .filter({ hideFromList: false }) + .map(item => item) + .value(); + + // add sort by sort property + return _.sortBy(panels, 'sort'); + } + + onChangeVizPlugin = plugin => { + console.log('set viz'); + }; + + renderVizPlugin(plugin, index) { + return ( +
    this.onChangeVizPlugin(plugin)} title={plugin.name}> + +
    {plugin.name}
    +
    + ); + } + + render() { + return
    {this.state.pluginList.map(this.renderVizPlugin)}
    ; + } +} From dbe191fd55943eeee1e7b7fe3cb48c7df462e44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 7 Jul 2018 05:10:03 -0700 Subject: [PATCH 0026/2611] wip: viz editor started --- .../dashboard/dashgrid/DashboardPanel.tsx | 1 - .../dashboard/dashgrid/PanelEditor.tsx | 5 +- .../app/features/dashboard/dashnav/dashnav.ts | 2 + public/sass/_grafana.scss | 1 + public/sass/components/_viz_editor.scss | 50 +++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 public/sass/components/_viz_editor.scss diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 3d2542d63c8..b56e2bef39f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -85,7 +85,6 @@ export class DashboardPanel extends React.Component { if (pluginExports.PanelComponent) { return ( { renderVizTab() { return (
    -
    +
    +
    Visualization Type
    -
    +
    Options
    diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 628f09349d3..73e06877472 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -38,6 +38,8 @@ export class DashNavCtrl { } else if (search.fullscreen) { delete search.fullscreen; delete search.edit; + delete search.tab; + delete search.panelId; } this.$location.search(search); } diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index afc869f8b15..0c758abf9e0 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -92,6 +92,7 @@ @import 'components/form_select_box'; @import 'components/user-picker'; @import 'components/description-picker'; +@import 'components/viz_editor'; // PAGES @import 'pages/login'; diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss new file mode 100644 index 00000000000..0dc368b4ed0 --- /dev/null +++ b/public/sass/components/_viz_editor.scss @@ -0,0 +1,50 @@ +.viz-editor { + display: flex; +} + +.viz-editor-col1 { + width: 150px; + background: $panel-bg; +} + +.viz-editor-col2 { + flex-grow: 1; +} + +.viz-picker { + padding: 3px 8px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow: auto; + height: 100%; +} + +.viz-picker__item { + background: $card-background; + box-shadow: $card-shadow; + + border-radius: 3px; + padding: $spacer/3 $spacer; + width: 31%; + height: 60px; + text-align: center; + margin: $gf-form-margin; + cursor: pointer; + + &.active, + &:hover { + background: $card-background-hover; + } +} + +.viz-picker__item-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: $font-size-sm; +} + +.viz-picker__item-img { + height: calc(100% - 15px); +} From 6e4b199bc20431589ece37785fa0730e1a0e3db1 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 8 Jul 2018 11:07:01 +0200 Subject: [PATCH 0027/2611] 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 0028/2611] 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 50f24c98f7e735a75a732a372209a8f2b013484b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 8 Jul 2018 07:39:25 -0700 Subject: [PATCH 0029/2611] wip: minor progres on react panels edit mode --- public/app/core/config.ts | 13 +++- .../dashboard/dashgrid/PanelChrome.tsx | 4 +- .../dashboard/dashgrid/PanelEditor.tsx | 19 ++++-- .../features/dashboard/dashgrid/VizPicker.tsx | 46 -------------- .../dashboard/dashgrid/VizTypePicker.tsx | 61 +++++++++++++++++++ .../dashboard/specs/AddPanelPanel.jest.tsx | 15 +++++ public/app/features/panel/panel_directive.ts | 2 +- .../app/features/plugins/plugin_component.ts | 2 +- public/sass/components/_panel_add_panel.scss | 4 -- public/sass/components/_tabbed_view.scss | 23 ++----- public/sass/components/_viz_editor.scss | 43 ++++++++++--- public/sass/pages/_dashboard.scss | 12 +++- 12 files changed, 154 insertions(+), 90 deletions(-) delete mode 100644 public/app/features/dashboard/dashgrid/VizPicker.tsx create mode 100644 public/app/features/dashboard/dashgrid/VizTypePicker.tsx diff --git a/public/app/core/config.ts b/public/app/core/config.ts index e065ddb22fb..eb2eee999ab 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -7,9 +7,20 @@ export interface BuildInfo { env: string; } +export interface PanelPlugin { + id: string; + name: string; + meta: any; + hideFromList: boolean; + module: string; + baseUrl: string; + info: any; + sort: number; +} + export class Settings { datasources: any; - panels: any; + panels: PanelPlugin[]; appSubUrl: string; window_title_prefix: string; buildInfo: BuildInfo; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index ebe25379c2d..b4584af63f8 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -48,7 +48,7 @@ export class PanelChrome extends React.Component { let PanelComponent = this.panelComponent; return ( -
    +
    @@ -73,6 +73,6 @@ export class PanelChrome extends React.Component { height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN; } - return height - PANEL_BORDER + TITLE_HEIGHT; + return height - (PANEL_BORDER + TITLE_HEIGHT); } } diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 9296293f6de..55babf4b19d 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -1,11 +1,12 @@ import React from 'react'; +import classNames from 'classnames'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { store } from 'app/stores/store'; import { observer } from 'mobx-react'; import { QueriesTab } from './QueriesTab'; -import classNames from 'classnames'; -import { VizPicker } from './VizPicker'; +import { PanelPlugin } from 'app/core/config'; +import { VizTypePicker } from './VizTypePicker'; interface PanelEditorProps { panel: PanelModel; @@ -39,16 +40,22 @@ export class PanelEditor extends React.Component { return (
    -
    Visualization Type
    - +
    -
    Options
    +
    Options
    ); } + onVizTypeChanged = (plugin: PanelPlugin) => { + this.props.panel.type = plugin.id; + this.forceUpdate(); + + console.log('panel type changed', plugin); + }; + onChangeTab = (tab: PanelEditorTab) => { store.view.updateQuery({ tab: tab.id }, false); }; @@ -57,7 +64,7 @@ export class PanelEditor extends React.Component { const activeTab: string = store.view.query.get('tab') || 'queries'; return ( -
    +
      {this.tabs.map(tab => { diff --git a/public/app/features/dashboard/dashgrid/VizPicker.tsx b/public/app/features/dashboard/dashgrid/VizPicker.tsx deleted file mode 100644 index 2d879ef94d4..00000000000 --- a/public/app/features/dashboard/dashgrid/VizPicker.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { PureComponent } from 'react'; -import config from 'app/core/config'; -import _ from 'lodash'; - -interface Props {} - -interface State { - pluginList: any[]; -} - -export class VizPicker extends PureComponent { - constructor(props) { - super(props); - - this.state = { - pluginList: this.getPanelPlugins(''), - }; - } - - getPanelPlugins(filter) { - let panels = _.chain(config.panels) - .filter({ hideFromList: false }) - .map(item => item) - .value(); - - // add sort by sort property - return _.sortBy(panels, 'sort'); - } - - onChangeVizPlugin = plugin => { - console.log('set viz'); - }; - - renderVizPlugin(plugin, index) { - return ( -
      this.onChangeVizPlugin(plugin)} title={plugin.name}> - -
      {plugin.name}
      -
      - ); - } - - render() { - return
      {this.state.pluginList.map(this.renderVizPlugin)}
      ; - } -} diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx new file mode 100644 index 00000000000..197892090b5 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import config, { PanelPlugin } from 'app/core/config'; +import _ from 'lodash'; + +interface Props { + currentType: string; + onTypeChanged: (newType: PanelPlugin) => void; +} + +interface State { + pluginList: PanelPlugin[]; +} + +export class VizTypePicker extends PureComponent { + constructor(props) { + super(props); + + this.state = { + pluginList: this.getPanelPlugins(''), + }; + } + + getPanelPlugins(filter) { + let panels = _.chain(config.panels) + .filter({ hideFromList: false }) + .map(item => item) + .value(); + + // add sort by sort property + return _.sortBy(panels, 'sort'); + } + + renderVizPlugin = (plugin, index) => { + const cssClass = classNames({ + 'viz-picker__item': true, + 'viz-picker__item--selected': plugin.id === this.props.currentType, + }); + + return ( +
      this.props.onTypeChanged(plugin)} title={plugin.name}> + +
      {plugin.name}
      +
      + ); + }; + + render() { + return ( +
      +
      + +
      +
      {this.state.pluginList.map(this.renderVizPlugin)}
      +
      + ); + } +} diff --git a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx b/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx index 9bf99b5720c..c5f66fed32a 100644 --- a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx +++ b/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx @@ -23,6 +23,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Singlestat', sort: 2, + module: '', + baseUrl: '', + meta: {}, info: { logos: { small: '', @@ -34,6 +37,9 @@ describe('AddPanelPanel', () => { hideFromList: true, name: 'Hidden', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -45,6 +51,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Graph', sort: 1, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -56,6 +65,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Zabbix', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -67,6 +79,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Piechart', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 685e527c944..67cad9c5594 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -26,7 +26,7 @@ var panelTemplate = `
    -
    +

    {{ctrl.pluginName}} diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 1936e57f558..5ef4019c24d 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => { PanelCtrl.templateUrl = null; - PanelCtrl.template = `${template}`; + PanelCtrl.template = `${template}`; return componentInfo; }); diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index 5bfff31a108..263b181262e 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -85,10 +85,6 @@ height: calc(100% - 15px); } -.add-panel__item-icon { - padding: 2px; -} - .add-panel__searchbar { width: 100%; margin-bottom: 10px; diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index a5d38d292a1..d577fa4777b 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,28 +1,16 @@ .tabbed-view { - padding: $spacer*3; margin-bottom: $dashboard-padding; + display: flex; + flex-direction: column; + height: 100%; - &.tabbed-view--panel-edit { - padding: 0; - - .tabbed-view-header { - padding: 0px 25px; - background: none; - } - } - - &.tabbed-view--panel-edit-new { + &.tabbed-view--new { padding: 10px 0 0 0; - - .tabbed-view-header { - padding: 0px; - background: none; - } + height: 100%; } } .tabbed-view-header { - background: $page-header-bg; box-shadow: $page-header-shadow; border-bottom: 1px solid $page-header-border-color; @include clearfix(); @@ -58,6 +46,7 @@ .tabbed-view-body { padding: $spacer*2 $spacer; + height: 100%; &--small { min-height: 0px; diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss index 0dc368b4ed0..b377a7d5c28 100644 --- a/public/sass/components/_viz_editor.scss +++ b/public/sass/components/_viz_editor.scss @@ -1,10 +1,12 @@ .viz-editor { display: flex; + height: 100%; } .viz-editor-col1 { - width: 150px; - background: $panel-bg; + width: 240px; + height: 100%; + margin-right: 40px; } .viz-editor-col2 { @@ -12,11 +14,15 @@ } .viz-picker { + display: flex; + flex-direction: column; +} + +.viz-picker-list { padding: 3px 8px; display: flex; - flex-direction: row; - flex-wrap: wrap; - overflow: auto; + flex-direction: column; + overflow: hidden; height: 100%; } @@ -25,17 +31,29 @@ box-shadow: $card-shadow; border-radius: 3px; - padding: $spacer/3 $spacer; - width: 31%; + padding: $spacer; + width: 100%; height: 60px; text-align: center; margin: $gf-form-margin; cursor: pointer; + display: flex; - &.active, &:hover { background: $card-background-hover; } + + &--selected { + border: 1px solid $orange; + + .viz-picker__item-name { + color: $text-color; + } + + .viz-picker__item-img { + filter: saturate(100%); + } + } } .viz-picker__item-name { @@ -43,8 +61,15 @@ overflow: hidden; white-space: nowrap; font-size: $font-size-sm; + display: flex; + flex-direction: column; + align-self: center; + padding-left: $spacer; + font-size: $font-size-md; + color: $text-muted; } .viz-picker__item-img { - height: calc(100% - 15px); + height: 100%; + filter: saturate(30%); } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 2e8097e3fb7..1ff4f2078e7 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -1,7 +1,8 @@ .dashboard-container { padding: $dashboard-padding; width: 100%; - min-height: 100%; + height: 100%; + box-sizing: border-box; } .template-variable { @@ -28,12 +29,17 @@ div.flot-text { height: 100%; } +.panel-editor-container { + display: flex; + flex-direction: column; + height: 100%; +} + .panel-container { background-color: $panel-bg; border: $panel-border; position: relative; border-radius: 3px; - height: 100%; &.panel-transparent { background-color: transparent; @@ -233,5 +239,5 @@ div.flot-text { } .panel-full-edit { - margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding); + padding-top: $dashboard-padding; } From 8036c49ffed416a210f1d8859b7e5f619410cbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 8 Jul 2018 12:29:23 -0700 Subject: [PATCH 0030/2611] wip: minopr progress on react panel edit infra --- .../features/dashboard/dashgrid/DashboardPanel.tsx | 3 +-- .../app/features/dashboard/dashgrid/PanelEditor.tsx | 2 -- public/sass/components/_tabbed_view.scss | 7 ++++--- public/sass/components/_viz_editor.scss | 12 +++++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index b56e2bef39f..0591d90cde2 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -53,8 +53,7 @@ export class DashboardPanel extends React.Component { } componentDidUpdate() { - // skip loading angular component if we have no element - // or we have already loaded it + // skip loading angular component if we have no element or we have already loaded it if (!this.element || this.angularPanel) { return; } diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 55babf4b19d..3ddf7d2f81b 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -52,8 +52,6 @@ export class PanelEditor extends React.Component { onVizTypeChanged = (plugin: PanelPlugin) => { this.props.panel.type = plugin.id; this.forceUpdate(); - - console.log('panel type changed', plugin); }; onChangeTab = (tab: PanelEditorTab) => { diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index d577fa4777b..80e76b5fbf4 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,5 +1,4 @@ .tabbed-view { - margin-bottom: $dashboard-padding; display: flex; flex-direction: column; height: 100%; @@ -45,8 +44,10 @@ } .tabbed-view-body { - padding: $spacer*2 $spacer; - height: 100%; + padding: $spacer*2 $spacer $spacer $spacer; + display: flex; + flex-direction: column; + flex: 1; &--small { min-height: 0px; diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss index b377a7d5c28..f0c4dceaee0 100644 --- a/public/sass/components/_viz_editor.scss +++ b/public/sass/components/_viz_editor.scss @@ -4,7 +4,7 @@ } .viz-editor-col1 { - width: 240px; + width: 210px; height: 100%; margin-right: 40px; } @@ -16,14 +16,15 @@ .viz-picker { display: flex; flex-direction: column; + height: 100%; } .viz-picker-list { - padding: 3px 8px; + padding-top: $spacer; display: flex; flex-direction: column; overflow: hidden; - height: 100%; + flex-grow: 1; } .viz-picker__item { @@ -35,9 +36,10 @@ width: 100%; height: 60px; text-align: center; - margin: $gf-form-margin; + margin-bottom: 6px; cursor: pointer; display: flex; + flex-shrink: 0; &:hover { background: $card-background-hover; @@ -60,7 +62,7 @@ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - font-size: $font-size-sm; + font-size: $font-size-h5; display: flex; flex-direction: column; align-self: center; From 51f8d3ca420c93c9ad0ea6c0830ba5b1797a6607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 8 Jul 2018 13:03:22 -0700 Subject: [PATCH 0031/2611] fix: minor css change --- public/sass/components/_viz_editor.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss index f0c4dceaee0..7d69813ebd5 100644 --- a/public/sass/components/_viz_editor.scss +++ b/public/sass/components/_viz_editor.scss @@ -40,6 +40,7 @@ cursor: pointer; display: flex; flex-shrink: 0; + border: 1px solid transparent; &:hover { background: $card-background-hover; 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 0032/2611] 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 0033/2611] 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 761283231c4c1d3e98342db826985c64c5b042ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Jul 2018 18:17:51 +0200 Subject: [PATCH 0034/2611] react panels: working on changing type --- public/app/core/config.ts | 12 +- .../dashboard/dashgrid/DashboardGrid.tsx | 1 + .../dashboard/dashgrid/DashboardPanel.tsx | 81 ++++++-- .../dashboard/dashgrid/PanelChrome.tsx | 47 +---- .../dashboard/dashgrid/PanelEditor.tsx | 6 +- .../dashboard/dashgrid/PanelHeader.tsx | 14 +- .../dashboard/dashgrid/VizTypePicker.tsx | 3 +- public/app/features/dashboard/panel_model.ts | 5 + .../app/features/plugins/built_in_plugins.ts | 2 + public/app/features/plugins/plugin_loader.ts | 11 +- public/app/plugins/panel/graph2/README.md | 5 + .../panel/graph2/img/icn-graph-panel.svg | 186 ++++++++++++++++++ public/app/plugins/panel/graph2/module.tsx | 21 ++ public/app/plugins/panel/graph2/plugin.json | 17 ++ public/app/plugins/panel/text2/module.tsx | 2 +- public/app/types/plugins.ts | 20 ++ public/sass/pages/_dashboard.scss | 9 + 17 files changed, 352 insertions(+), 90 deletions(-) create mode 100644 public/app/plugins/panel/graph2/README.md create mode 100644 public/app/plugins/panel/graph2/img/icn-graph-panel.svg create mode 100644 public/app/plugins/panel/graph2/module.tsx create mode 100644 public/app/plugins/panel/graph2/plugin.json create mode 100644 public/app/types/plugins.ts diff --git a/public/app/core/config.ts b/public/app/core/config.ts index eb2eee999ab..249a274ce5d 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { PanelPlugin } from 'app/types/plugins'; export interface BuildInfo { version: string; @@ -7,17 +8,6 @@ export interface BuildInfo { env: string; } -export interface PanelPlugin { - id: string; - name: string; - meta: any; - hideFromList: boolean; - module: string; - baseUrl: string; - info: any; - sort: number; -} - export class Settings { datasources: any; panels: PanelPlugin[]; diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 30d97898900..77b55a2130f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -171,6 +171,7 @@ export class DashboardGrid extends React.Component { renderPanels() { const panelElements = []; + console.log('render panels'); for (let panel of this.props.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 0591d90cde2..781e7186a7e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -5,8 +5,10 @@ import { DashboardModel } from '../dashboard_model'; import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader'; import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; -import { importPluginModule, PluginExports } from 'app/features/plugins/plugin_loader'; +import { importPluginModule } from 'app/features/plugins/plugin_loader'; +import { PluginExports } from 'app/types/plugins'; import { PanelChrome } from './PanelChrome'; +import { PanelEditor } from './PanelEditor'; export interface Props { panel: PanelModel; @@ -29,15 +31,11 @@ export class DashboardPanel extends React.Component { this.specialPanels['row'] = this.renderRow.bind(this); this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); + this.props.panel.events.on('panel-size-changed', this.triggerForceUpdate.bind(this)); + } - if (!this.isSpecial()) { - this.pluginInfo = config.panels[this.props.panel.type]; - - // load panel plugin - importPluginModule(this.pluginInfo.module).then(pluginExports => { - this.setState({ pluginExports: pluginExports }); - }); - } + triggerForceUpdate() { + this.forceUpdate(); } isSpecial() { @@ -52,8 +50,33 @@ export class DashboardPanel extends React.Component { return ; } + loadPlugin() { + if (this.isSpecial()) { + return; + } + + // handle plugin loading & changing of plugin type + if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) { + this.pluginInfo = config.panels[this.props.panel.type]; + + if (this.pluginInfo.exports) { + this.setState({ pluginExports: this.pluginInfo.exports }); + } else { + importPluginModule(this.pluginInfo.module).then(pluginExports => { + this.setState({ pluginExports: pluginExports }); + }); + } + } + } + + componentDidMount() { + this.loadPlugin(); + } + componentDidUpdate() { - // skip loading angular component if we have no element or we have already loaded it + this.loadPlugin(); + + // handle angular plugin loading if (!this.element || this.angularPanel) { return; } @@ -70,25 +93,43 @@ export class DashboardPanel extends React.Component { } } - render() { + renderReactPanel() { const { pluginExports } = this.state; + const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper'; + const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper'; + // this might look strange with these classes that change when edit, but + // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel + // plugin component + return ( +
    +
    + +
    + {this.props.panel.isEditing && ( +
    + +
    + )} +
    + ); + } + + render() { if (this.isSpecial()) { return this.specialPanels[this.props.panel.type](); } - if (!pluginExports) { + if (!this.state.pluginExports) { return null; } - if (pluginExports.PanelComponent) { - return ( - - ); + if (this.state.pluginExports.PanelComponent) { + return this.renderReactPanel(); } // legacy angular rendering diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b4584af63f8..eb0b34c3b06 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -2,23 +2,16 @@ import React, { ComponentClass } from 'react'; import $ from 'jquery'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { PanelHeader } from './PanelHeader'; -import { PanelEditor } from './PanelEditor'; import { DataPanel, PanelProps, DataPanelWrapper } from './DataPanel'; -const TITLE_HEIGHT = 27; -const PANEL_BORDER = 2; - export interface Props { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; } -interface State { - height: number; -} +interface State {} export class PanelChrome extends React.Component { panelComponent: DataPanel; @@ -26,20 +19,9 @@ export class PanelChrome extends React.Component { constructor(props) { super(props); - this.state = { - height: this.getPanelHeight(), - }; - this.panelComponent = DataPanelWrapper(this.props.component); - this.props.panel.events.on('panel-size-changed', this.onPanelSizeChanged); } - onPanelSizeChanged = () => { - this.setState({ - height: this.getPanelHeight(), - }); - }; - componentDidMount() { console.log('panel chrome mounted'); } @@ -48,31 +30,10 @@ export class PanelChrome extends React.Component { let PanelComponent = this.panelComponent; return ( -
    -
    - -
    - {} -
    -
    - {this.props.panel.isEditing && } +
    + +
    {}
    ); } - - getPanelHeight() { - const panel = this.props.panel; - let height = 0; - - if (panel.fullscreen) { - var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.3); - var fullscreenHeight = Math.floor(docHeight * 0.8); - height = panel.isEditing ? editHeight : fullscreenHeight; - } else { - height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN; - } - - return height - (PANEL_BORDER + TITLE_HEIGHT); - } } diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 3ddf7d2f81b..1a8b1190928 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -5,7 +5,7 @@ import { DashboardModel } from '../dashboard_model'; import { store } from 'app/stores/store'; import { observer } from 'mobx-react'; import { QueriesTab } from './QueriesTab'; -import { PanelPlugin } from 'app/core/config'; +import { PanelPlugin } from 'app/types/plugins'; import { VizTypePicker } from './VizTypePicker'; interface PanelEditorProps { @@ -50,8 +50,8 @@ export class PanelEditor extends React.Component { } onVizTypeChanged = (plugin: PanelPlugin) => { - this.props.panel.type = plugin.id; - this.forceUpdate(); + console.log('changing type to ', plugin.id); + this.props.panel.changeType(plugin.id); }; onChangeTab = (tab: PanelEditorTab) => { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx index 97d41e15a0c..c4c169ceb88 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -21,6 +21,16 @@ export class PanelHeader extends React.Component { ); }; + onViewPanel = () => { + store.view.updateQuery( + { + panelId: this.props.panel.id, + fullscreen: true, + }, + false + ); + }; + render() { let isFullscreen = false; let isLoading = false; @@ -52,7 +62,9 @@ export class PanelHeader extends React.Component {
  • - asd + + View +
diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index 197892090b5..5a77ab6bfcf 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; -import config, { PanelPlugin } from 'app/core/config'; +import config from 'app/core/config'; +import { PanelPlugin } from 'app/types/plugins'; import _ from 'lodash'; interface Props { diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 6ee49886a06..8c9270ad1ab 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -97,6 +97,11 @@ export class PanelModel { this.events.emit('panel-init-edit-mode'); } + changeType(newType: string) { + this.type = newType; + this.events.emit('panel-size-changed'); + } + destroy() { this.events.removeAllListeners(); } diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 5f1f56b29bd..79f699d2291 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -12,6 +12,7 @@ import * as mssqlPlugin from 'app/plugins/datasource/mssql/module'; import * as textPanel from 'app/plugins/panel/text/module'; import * as text2Panel from 'app/plugins/panel/text2/module'; +import * as graph2Panel from 'app/plugins/panel/graph2/module'; import * as graphPanel from 'app/plugins/panel/graph/module'; import * as dashListPanel from 'app/plugins/panel/dashlist/module'; import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; @@ -41,6 +42,7 @@ const builtInPlugins = { 'app/plugins/panel/text/module': textPanel, 'app/plugins/panel/text2/module': text2Panel, + 'app/plugins/panel/graph2/module': graph2Panel, 'app/plugins/panel/graph/module': graphPanel, 'app/plugins/panel/dashlist/module': dashListPanel, 'app/plugins/panel/pluginlist/module': pluginsListPanel, diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index ffcd36312fb..4564e5d4fd9 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -18,6 +18,7 @@ import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import TableModel from 'app/core/table_model'; import { coreModule, appEvents, contextSrv } from 'app/core/core'; +import { PluginExports } from 'app/types/plugins'; import * as datemath from 'app/core/utils/datemath'; import * as fileExport from 'app/core/utils/file_export'; import * as flatten from 'app/core/utils/flatten'; @@ -143,16 +144,6 @@ for (let flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } -export interface PluginExports { - PanelCtrl?; - any; - PanelComponent?: any; - Datasource?: any; - QueryCtrl?: any; - ConfigCtrl?: any; - AnnotationsQueryCtrl?: any; -} - export function importPluginModule(path: string): Promise { let builtIn = builtInPlugins[path]; if (builtIn) { diff --git a/public/app/plugins/panel/graph2/README.md b/public/app/plugins/panel/graph2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/graph2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/graph2/img/icn-graph-panel.svg b/public/app/plugins/panel/graph2/img/icn-graph-panel.svg new file mode 100644 index 00000000000..463b3d5770b --- /dev/null +++ b/public/app/plugins/panel/graph2/img/icn-graph-panel.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx new file mode 100644 index 00000000000..7ae24bd7d40 --- /dev/null +++ b/public/app/plugins/panel/graph2/module.tsx @@ -0,0 +1,21 @@ +import React, { PureComponent } from 'react'; +import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; + +export class ReactTestPanel extends PureComponent { + constructor(props) { + super(props); + } + + render() { + const { data } = this.props; + let value = 0; + + if (data.length) { + value = data[0].value; + } + + return

Graph Panel! {value}

; + } +} + +export { ReactTestPanel as PanelComponent }; diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json new file mode 100644 index 00000000000..bc60d6ad2d7 --- /dev/null +++ b/public/app/plugins/panel/graph2/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "React Graph", + "id": "graph2", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-graph-panel.svg", + "large": "img/icn-graph-panel.svg" + } + } +} + diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 019cf912340..20b6be4ca72 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -14,7 +14,7 @@ export class ReactTestPanel extends PureComponent { value = data[0].value; } - return

I am a react value: {value}

; + return

Text Panel {value}

; } } diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts new file mode 100644 index 00000000000..9f0208463b9 --- /dev/null +++ b/public/app/types/plugins.ts @@ -0,0 +1,20 @@ +export interface PluginExports { + PanelCtrl?; + PanelComponent?: any; + Datasource?: any; + QueryCtrl?: any; + ConfigCtrl?: any; + AnnotationsQueryCtrl?: any; +} + +export interface PanelPlugin { + id: string; + name: string; + meta: any; + hideFromList: boolean; + module: string; + baseUrl: string; + info: any; + sort: number; + exports?: PluginExports; +} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 1ff4f2078e7..dad8bfa1c49 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -35,11 +35,20 @@ div.flot-text { height: 100%; } +.panel-editor-container__panel { + height: 35%; +} + +.panel-editor-container__editor { + height: 65%; +} + .panel-container { background-color: $panel-bg; border: $panel-border; position: relative; border-radius: 3px; + height: 100%; &.panel-transparent { background-color: transparent; From dc3a81200b1cd366bdf237ada152ae3d79bdaf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Jul 2018 13:24:15 -0700 Subject: [PATCH 0035/2611] wip: you can now change panel type in edit mode --- .../dashboard/dashgrid/DashboardPanel.tsx | 28 +++++++++++++------ .../dashboard/dashgrid/PanelChrome.tsx | 20 +++++++------ .../dashboard/dashgrid/PanelEditor.tsx | 9 ++---- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 781e7186a7e..bec0a6e6dbe 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -6,7 +6,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/angular_lo import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; import { importPluginModule } from 'app/features/plugins/plugin_loader'; -import { PluginExports } from 'app/types/plugins'; +import { PluginExports, PanelPlugin } from 'app/types/plugins'; import { PanelChrome } from './PanelChrome'; import { PanelEditor } from './PanelEditor'; @@ -27,15 +27,13 @@ export class DashboardPanel extends React.Component { constructor(props) { super(props); - this.state = { pluginExports: null }; + + this.state = { + pluginExports: null, + }; this.specialPanels['row'] = this.renderRow.bind(this); this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); - this.props.panel.events.on('panel-size-changed', this.triggerForceUpdate.bind(this)); - } - - triggerForceUpdate() { - this.forceUpdate(); } isSpecial() { @@ -50,6 +48,11 @@ export class DashboardPanel extends React.Component { return ; } + onPluginTypeChanged = (plugin: PanelPlugin) => { + this.props.panel.changeType(plugin.id); + this.loadPlugin(); + }; + loadPlugin() { if (this.isSpecial()) { return; @@ -63,6 +66,9 @@ export class DashboardPanel extends React.Component { this.setState({ pluginExports: this.pluginInfo.exports }); } else { importPluginModule(this.pluginInfo.module).then(pluginExports => { + // cache plugin exports (saves a promise async cycle next time) + this.pluginInfo.exports = pluginExports; + // update panel state this.setState({ pluginExports: pluginExports }); }); } @@ -74,6 +80,7 @@ export class DashboardPanel extends React.Component { } componentDidUpdate() { + console.log('componentDidUpdate'); this.loadPlugin(); // handle angular plugin loading @@ -112,7 +119,12 @@ export class DashboardPanel extends React.Component {
{this.props.panel.isEditing && (
- +
)}
diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index eb0b34c3b06..73208c34130 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -1,5 +1,4 @@ import React, { ComponentClass } from 'react'; -import $ from 'jquery'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelHeader } from './PanelHeader'; @@ -13,21 +12,26 @@ export interface Props { interface State {} +// cache DataPanel wrapper components +const dataPanels: { [s: string]: DataPanel } = {}; + export class PanelChrome extends React.Component { panelComponent: DataPanel; constructor(props) { super(props); - - this.panelComponent = DataPanelWrapper(this.props.component); - } - - componentDidMount() { - console.log('panel chrome mounted'); } render() { - let PanelComponent = this.panelComponent; + const { type } = this.props.panel; + + let PanelComponent = dataPanels[type]; + + if (!PanelComponent) { + PanelComponent = dataPanels[type] = DataPanelWrapper(this.props.component); + } + + console.log('PanelChrome render', PanelComponent); return (
diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 1a8b1190928..f2b71837822 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -11,6 +11,8 @@ import { VizTypePicker } from './VizTypePicker'; interface PanelEditorProps { panel: PanelModel; dashboard: DashboardModel; + panelType: string; + onTypeChanged: (newType: PanelPlugin) => void; } interface PanelEditorTab { @@ -40,7 +42,7 @@ export class PanelEditor extends React.Component { return (
- +
Options
@@ -49,11 +51,6 @@ export class PanelEditor extends React.Component { ); } - onVizTypeChanged = (plugin: PanelPlugin) => { - console.log('changing type to ', plugin.id); - this.props.panel.changeType(plugin.id); - }; - onChangeTab = (tab: PanelEditorTab) => { store.view.updateQuery({ tab: tab.id }, false); }; 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 0036/2611] 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 0037/2611] 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 0038/2611] 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 0039/2611] 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 211e0f2199f67184f7d010c4999f2e6fb6f671a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Jul 2018 12:11:21 -0700 Subject: [PATCH 0040/2611] wip: another baby step, another million to go --- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../features/dashboard/dashgrid/PanelEditor.tsx | 15 ++++++++++++++- public/app/plugins/panel/text2/module.tsx | 8 +++++++- public/app/types/plugins.ts | 1 + public/sass/components/_viz_editor.scss | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index bec0a6e6dbe..4b2e81b969c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -107,7 +107,6 @@ export class DashboardPanel extends React.Component { // this might look strange with these classes that change when edit, but // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel - // plugin component return (
@@ -124,6 +123,7 @@ export class DashboardPanel extends React.Component { panelType={this.props.panel.type} dashboard={this.props.dashboard} onTypeChanged={this.onPluginTypeChanged} + pluginExports={pluginExports} />
)} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index f2b71837822..70b5bf11815 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -5,13 +5,14 @@ import { DashboardModel } from '../dashboard_model'; import { store } from 'app/stores/store'; import { observer } from 'mobx-react'; import { QueriesTab } from './QueriesTab'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPlugin, PluginExports } from 'app/types/plugins'; import { VizTypePicker } from './VizTypePicker'; interface PanelEditorProps { panel: PanelModel; dashboard: DashboardModel; panelType: string; + pluginExports: PluginExports; onTypeChanged: (newType: PanelPlugin) => void; } @@ -38,6 +39,17 @@ export class PanelEditor extends React.Component { return ; } + renderPanelOptions() { + const { pluginExports } = this.props; + + if (pluginExports.PanelOptions) { + const PanelOptions = pluginExports.PanelOptions; + return ; + } else { + return

Visualization has no options

; + } + } + renderVizTab() { return (
@@ -46,6 +58,7 @@ export class PanelEditor extends React.Component {
Options
+ {this.renderPanelOptions()}
); diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 20b6be4ca72..703a9897c6f 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -18,4 +18,10 @@ export class ReactTestPanel extends PureComponent { } } -export { ReactTestPanel as PanelComponent }; +export class TextOptions extends PureComponent { + render() { + return

Text2 Options component

; + } +} + +export { ReactTestPanel as PanelComponent, TextOptions as PanelOptions }; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 9f0208463b9..53d7b2c51c9 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -5,6 +5,7 @@ export interface PluginExports { QueryCtrl?: any; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; + PanelOptions?: any; } export interface PanelPlugin { diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss index 7d69813ebd5..3658c869d6d 100644 --- a/public/sass/components/_viz_editor.scss +++ b/public/sass/components/_viz_editor.scss @@ -23,7 +23,7 @@ padding-top: $spacer; display: flex; flex-direction: column; - overflow: hidden; + overflow: auto; flex-grow: 1; } From 4e089229fb4c134a918b42a10414882661f35727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Jul 2018 14:56:40 +0200 Subject: [PATCH 0041/2611] minor fix for legacy panels --- public/app/features/dashboard/dashgrid/PanelHeader.tsx | 1 + public/app/features/panel/panel_directive.ts | 2 +- public/sass/pages/_dashboard.scss | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx index c4c169ceb88..8c3dc6a324e 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -26,6 +26,7 @@ export class PanelHeader extends React.Component { { panelId: this.props.panel.id, fullscreen: true, + edit: null, }, false ); diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 67cad9c5594..f4ed06007a5 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -6,7 +6,7 @@ import baron from 'baron'; var module = angular.module('grafana.directives'); var panelTemplate = ` -
+
diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index dad8bfa1c49..ba4fa93b37b 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -62,6 +62,10 @@ div.flot-text { opacity: 1; } } + + &--is-editing { + height: auto; + } } .panel-content { From c4308fedea8ee48973d39284e60562db1228fe6b Mon Sep 17 00:00:00 2001 From: Josh Dadak Date: Wed, 25 Jul 2018 14:02:36 +0100 Subject: [PATCH 0042/2611] 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 0043/2611] 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 76bd173a365068ecad00c1e5777e2d7eff83bf32 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 13 Aug 2018 14:28:41 +0200 Subject: [PATCH 0044/2611] created a section under administration for authentication, moved ldap guide here, created pages for auth-proxy, oauth, anonymous auth, ldap sync with grafana ee, and overview, moved authentication guides from configuration to, added linksin configuration page to guides --- .../authentication/anonymous-auth.md | 30 ++ .../authentication/auth-proxy.md | 282 ++++++++++++ .../administration/authentication/index.md | 10 + .../authentication/ldap-sync-grafana-ee.md | 13 + .../authentication}/ldap.md | 20 +- .../administration/authentication/oauth.md | 399 ++++++++++++++++ .../administration/authentication/overview.md | 28 ++ docs/sources/http_api/auth.md | 2 +- docs/sources/installation/configuration.md | 426 +----------------- 9 files changed, 790 insertions(+), 420 deletions(-) create mode 100644 docs/sources/administration/authentication/anonymous-auth.md create mode 100644 docs/sources/administration/authentication/auth-proxy.md create mode 100644 docs/sources/administration/authentication/index.md create mode 100644 docs/sources/administration/authentication/ldap-sync-grafana-ee.md rename docs/sources/{installation => administration/authentication}/ldap.md (90%) create mode 100644 docs/sources/administration/authentication/oauth.md create mode 100644 docs/sources/administration/authentication/overview.md diff --git a/docs/sources/administration/authentication/anonymous-auth.md b/docs/sources/administration/authentication/anonymous-auth.md new file mode 100644 index 00000000000..f2cde75cacb --- /dev/null +++ b/docs/sources/administration/authentication/anonymous-auth.md @@ -0,0 +1,30 @@ ++++ +title = "Anonymous Authentication" +description = "Anonymous authentication " +keywords = ["grafana", "configuration", "documentation", "anonymous"] +type = "docs" +[menu.docs] +name = "Anonymous Auth" +identifier = "anonymous-auth" +parent = "authentication" +weight = 4 ++++ + +# Anonymous Authentication + +## [auth.anonymous] + +### enabled + +Set to `true` to enable anonymous access. Defaults to `false` + +### org_name + +Set the organization name that should be used for anonymous users. If +you change your organization name in the Grafana UI this setting needs +to be updated to match the new name. + +### org_role + +Specify role for anonymous users. Defaults to `Viewer`, other valid +options are `Editor` and `Admin`. diff --git a/docs/sources/administration/authentication/auth-proxy.md b/docs/sources/administration/authentication/auth-proxy.md new file mode 100644 index 00000000000..8ff61a8c40b --- /dev/null +++ b/docs/sources/administration/authentication/auth-proxy.md @@ -0,0 +1,282 @@ ++++ +title = "Auth Proxy" +description = "Grafana Auth Proxy Guide " +keywords = ["grafana", "configuration", "documentation", "proxy"] +type = "docs" +[menu.docs] +name = "Auth Proxy" +identifier = "auth-proxy" +parent = "authentication" +weight = 2 ++++ + +# Auth Proxy Authentication + +## [auth.proxy] + +This feature allows you to handle authentication in a http reverse proxy. + +### enabled + +Defaults to `false` + +### header_name + +Defaults to X-WEBAUTH-USER + +#### header_property + +Defaults to username but can also be set to email + +### auto_sign_up + +Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`. + +### whitelist + +Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. + +### headers + +Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. + +
+ +# Grafana Authproxy + +AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet). + +Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature. + +The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful. + +## Interacting with Grafana’s AuthProxy via curl + +The AuthProxy feature can be configured through the Grafana configuration file with the following options: + +```js +[auth.proxy] +enabled = true +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true +ldap_sync_ttl = 60 +whitelist = +``` + +* **enabled**: this is to toggle the feature on or off +* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in. +* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email) +* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid. +* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes. +* **whitelist**: Comma separated list of trusted authentication proxies IP. + +With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header. + +```bash +curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users +[ + { + "id":1, + "name":"", + "login":"admin", + "email":"admin@localhost", + "isAdmin":true + } +] +``` + +We can then send a second request to the `/api/user` method which will return the details of the logged in user. We will use this request to show how Grafana automatically adds the new user we specify to the system. Here we create a new user called “anthony”. + +```bash +curl -H "X-WEBAUTH-USER: anthony" http://localhost:3000/api/user +{ + "email":"anthony", + "name":"", + "login":"anthony", + "theme":"", + "orgId":1, + "isGrafanaAdmin":false +} +``` + +## Making Apache’s auth work together with Grafana’s AuthProxy + +I’ll demonstrate how to use Apache for authenticating users. In this example we use BasicAuth with Apache’s text file based authentication handler, i.e. htpasswd files. However, any available Apache authentication capabilities could be used. + +### Apache BasicAuth + +In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service. + +#### Apache configuration + +```bash + + ServerAdmin webmaster@authproxy + ServerName authproxy + ErrorLog "logs/authproxy-error_log" + CustomLog "logs/authproxy-access_log" common + + + AuthType Basic + AuthName GrafanaAuthProxy + AuthBasicProvider file + AuthUserFile /etc/apache2/grafana_htpasswd + Require valid-user + + RewriteEngine On + RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] + RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" + + + RequestHeader unset Authorization + + ProxyRequests Off + ProxyPass / http://localhost:3000/ + ProxyPassReverse / http://localhost:3000/ + +``` + +* The first 4 lines of the virtualhost configuration are standard, so we won’t go into detail on what they do. + +* We use a **\** configuration block for applying our authentication rules to every proxied request. These rules include requiring basic authentication where user:password credentials are stored in the **/etc/apache2/grafana_htpasswd** file. This file can be created with the `htpasswd` command. + + * The next part of the configuration is the tricky part. We use Apache’s rewrite engine to create our **X-WEBAUTH-USER header**, populated with the authenticated user. + + * **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is necessary as the REMOTE_USER variable is not available to the RequestHeader function. + + * **RequestHeader set X-WEBAUTH-USER “%{PROXY_USER}e”**: With the authenticated username now stored in the PROXY_USER variable, we create a new HTTP request header that will be sent to our backend Grafana containing the username. + +* The **RequestHeader unset Authorization** removes the Authorization header from the HTTP request before it is forwarded to Grafana. This ensures that Grafana does not try to authenticate the user using these credentials (BasicAuth is a supported authentication handler in Grafana). + +* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000. + +#### Grafana configuration + +```bash +############# Users ################ +[users] + # disable user signup / registration +allow_sign_up = false + +# Set to true to automatically assign new users to the default organization (id 1) +auto_assign_org = true + +# Default role new users will be automatically assigned (if auto_assign_org above is set to true) + auto_assign_org_role = Editor + + +############ Auth Proxy ######## +[auth.proxy] +enabled = true + +# the Header name that contains the authenticated user. +header_name = X-WEBAUTH-USER + +# does the user authenticate against the proxy using a 'username' or an 'email' +header_property = username + +# automatically add the user to the system if they don't already exist. +auto_sign_up = true +``` + +#### Full walk through using Docker. + +##### Grafana Container + +For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/) + +* Create a file `grafana.ini` with the following contents + +```bash +[users] +allow_sign_up = false +auto_assign_org = true +auto_assign_org_role = Editor + +[auth.proxy] +enabled = true +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true +``` + +* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container. + +```bash +docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana +``` + +### Apache Container + +For this example we use the official Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/) + +* Create a file `httpd.conf` with the following contents + +```bash +ServerRoot "/usr/local/apache2" +Listen 80 +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule env_module modules/mod_env.so +LoadModule headers_module modules/mod_headers.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so + +User daemon +Group daemon + +ServerAdmin you@example.com + + AllowOverride none + Require all denied + +DocumentRoot "/usr/local/apache2/htdocs" +ErrorLog /proc/self/fd/2 +LogLevel error + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + CustomLog /proc/self/fd/1 common + + + AuthType Basic + AuthName GrafanaAuthProxy + AuthBasicProvider file + AuthUserFile /tmp/htpasswd + Require valid-user + RewriteEngine On + RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] + RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" + +RequestHeader unset Authorization +ProxyRequests Off +ProxyPass / http://grafana:3000/ +ProxyPassReverse / http://grafana:3000/ +``` + +* Create a htpasswd file. We create a new user **anthony** with the password **password** + + ```bash + htpasswd -bc htpasswd anthony password + ``` + +* Launch the httpd container using our custom httpd.conf and our htpasswd file. The container will listen on port 80, and we create a link to the **grafana** container so that this container can resolve the hostname **grafana** to the grafana container’s ip address. + + ```bash + docker run -i -p 80:80 --link grafana:grafana -v $(pwd)/httpd.conf:/usr/local/apache2/conf/httpd.conf -v $(pwd)/htpasswd:/tmp/htpasswd httpd:2.4 + ``` + +### Use grafana. + +With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file. diff --git a/docs/sources/administration/authentication/index.md b/docs/sources/administration/authentication/index.md new file mode 100644 index 00000000000..f9bc9e5f13c --- /dev/null +++ b/docs/sources/administration/authentication/index.md @@ -0,0 +1,10 @@ ++++ +title = "Authentication" +description = "Authentication" +type = "docs" +[menu.docs] +name = "Authentication" +identifier = "authentication" +parent = "admin" +weight = 1 ++++ \ No newline at end of file diff --git a/docs/sources/administration/authentication/ldap-sync-grafana-ee.md b/docs/sources/administration/authentication/ldap-sync-grafana-ee.md new file mode 100644 index 00000000000..c60b21b0320 --- /dev/null +++ b/docs/sources/administration/authentication/ldap-sync-grafana-ee.md @@ -0,0 +1,13 @@ ++++ +title = "LDAP Sync with Grafana EE" +description = "LDAP Sync with Grafana EE Guide " +keywords = ["grafana", "configuration", "documentation", "ldap", "enterprise"] +type = "docs" +[menu.docs] +name = "LDAP Sync with Grafana EE" +identifier = "ldap-sync" +parent = "authentication" +weight = 2 ++++ + +# LDAP Sync with Grafana EE \ No newline at end of file diff --git a/docs/sources/installation/ldap.md b/docs/sources/administration/authentication/ldap.md similarity index 90% rename from docs/sources/installation/ldap.md rename to docs/sources/administration/authentication/ldap.md index 88cf40632db..f9208213aef 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/administration/authentication/ldap.md @@ -4,12 +4,28 @@ description = "Grafana LDAP Authentication Guide " keywords = ["grafana", "configuration", "documentation", "ldap"] type = "docs" [menu.docs] -name = "LDAP Authentication" +name = "LDAP Auth" identifier = "ldap" -parent = "admin" +parent = "authentication" weight = 2 +++ +## [auth.ldap] +### enabled +Set to `true` to enable LDAP integration (default: `false`) + +### config_file +Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) + +### allow_sign_up + +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). + +> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page. + +
+ # LDAP Authentication Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your diff --git a/docs/sources/administration/authentication/oauth.md b/docs/sources/administration/authentication/oauth.md new file mode 100644 index 00000000000..0fe60196ffa --- /dev/null +++ b/docs/sources/administration/authentication/oauth.md @@ -0,0 +1,399 @@ ++++ +title = "OAuth authentication" +description = "Grafana OAuthentication Guide " +keywords = ["grafana", "configuration", "documentation", "oauth"] +type = "docs" +[menu.docs] +name = "OAuth" +identifier = "oauth" +parent = "authentication" +weight = 2 ++++ + +# OAuth Authentication + +## [auth.generic_oauth] + +This option could be used if have your own oauth service. + +This callback URL must match the full HTTP address that you use in your +browser to access Grafana, but with the prefix path of `/login/generic_oauth`. + +```bash +[auth.generic_oauth] +enabled = true +client_id = YOUR_APP_CLIENT_ID +client_secret = YOUR_APP_CLIENT_SECRET +scopes = +auth_url = +token_url = +api_url = +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` + +Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. + +### Set up oauth2 with Okta + +First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. + +Finally set up the generic oauth module like this: +```bash +[auth.generic_oauth] +name = Okta +enabled = true +scopes = openid profile email +client_id = +client_secret = +auth_url = https:///oauth2/v1/authorize +token_url = https:///oauth2/v1/token +api_url = https:///oauth2/v1/userinfo +``` + +### Set up oauth2 with Bitbucket + +```bash +[auth.generic_oauth] +name = BitBucket +enabled = true +allow_sign_up = true +client_id = +client_secret = +scopes = account email +auth_url = https://bitbucket.org/site/oauth2/authorize +token_url = https://bitbucket.org/site/oauth2/access_token +api_url = https://api.bitbucket.org/2.0/user +team_ids = +allowed_organizations = +``` + +### Set up oauth2 with OneLogin + +1. Create a new Custom Connector with the following settings: + - Name: Grafana + - Sign On Method: OpenID Connect + - Redirect URI: `https:///login/generic_oauth` + - Signing Algorithm: RS256 + - Login URL: `https:///login/generic_oauth` + + then: +2. Add an App to the Grafana Connector: + - Display Name: Grafana + + then: +3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. + + Your OneLogin Domain will match the url you use to access OneLogin. + + Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = OneLogin + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://.onelogin.com/oidc/auth + token_url = https://.onelogin.com/oidc/token + api_url = https://.onelogin.com/oidc/me + team_ids = + allowed_organizations = + ``` + +### Set up oauth2 with Auth0 + +1. Create a new Client in Auth0 + - Name: Grafana + - Type: Regular Web Application + +2. Go to the Settings tab and set: + - Allowed Callback URLs: `https:///login/generic_oauth` + +3. Click Save Changes, then use the values at the top of the page to configure Grafana: + + ```bash + [auth.generic_oauth] + enabled = true + allow_sign_up = true + team_ids = + allowed_organizations = + name = Auth0 + client_id = + client_secret = + scopes = openid profile email + auth_url = https:///authorize + token_url = https:///oauth/token + api_url = https:///userinfo + ``` + +### Set up oauth2 with Azure Active Directory + +1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item. + +2. Copy the "Directory ID", this is needed for setting URLs later + +3. Click "App Registrations" and add a new application registration: + - Name: Grafana + - Application type: Web app / API + - Sign-on URL: `https:///login/generic_oauth` + +4. Click the name of the new application to open the application details page. + +5. Note down the "Application ID", this will be the OAuth client id. + +6. Click "Settings", then click "Keys" and add a new entry under Passwords + - Key Description: Grafana OAuth + - Duration: Never Expires + +7. Click Save then copy the key value, this will be the OAuth client secret. + +8. Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = Azure AD + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://login.microsoftonline.com//oauth2/authorize + token_url = https://login.microsoftonline.com//oauth2/token + api_url = + team_ids = + allowed_organizations = + ``` + +
+ +## [auth.github] + +You need to create a GitHub OAuth application (you find this under the GitHub +settings page). When you create the application you will need to specify +a callback URL. Specify this as callback: + +```bash +http://:/login/github +``` + +This callback URL must match the full HTTP address that you use in your +browser to access Grafana, but with the prefix path of `/login/github`. +When the GitHub OAuth application is created you will get a Client ID and a +Client Secret. Specify these in the Grafana configuration file. For +example: + +```bash +[auth.github] +enabled = true +allow_sign_up = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +team_ids = +allowed_organizations = +``` + +Restart the Grafana back-end. You should now see a GitHub login button +on the login page. You can now login or sign up with your GitHub +accounts. + +You may allow users to sign-up via GitHub authentication by setting the +`allow_sign_up` option to `true`. When this option is set to `true`, any +user successfully authenticating via GitHub authentication will be +automatically signed up. + +### team_ids + +Require an active team membership for at least one of the given teams on +GitHub. If the authenticated user isn't a member of at least one of the +teams they will not be able to register or authenticate with your +Grafana instance. For example: + +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +team_ids = 150,300 +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +allow_sign_up = true +``` + +### allowed_organizations + +Require an active organization membership for at least one of the given +organizations on GitHub. If the authenticated user isn't a member of at least +one of the organizations they will not be able to register or authenticate with +your Grafana instance. For example + +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +allow_sign_up = true +# space-delimited organization names +allowed_organizations = github google +``` + +
+ +## [auth.gitlab] + +> Only available in Grafana v5.3+. + +You need to [create a GitLab OAuth +application](https://docs.gitlab.com/ce/integration/oauth_provider.html). +Choose a descriptive *Name*, and use the following *Redirect URI*: + +``` +https://grafana.example.com/login/gitlab +``` + +where `https://grafana.example.com` is the URL you use to connect to Grafana. +Adjust it as needed if you don't use HTTPS or if you use a different port; for +instance, if you access Grafana at `http://203.0.113.31:3000`, you should use + +``` +http://203.0.113.31:3000/login/gitlab +``` + +Finally, select *api* as the *Scope* and submit the form. Note that if you're +not going to use GitLab groups for authorization (i.e. not setting +`allowed_groups`, see below), you can select *read_user* instead of *api* as +the *Scope*, thus giving a more restricted access to your GitLab API. + +You'll get an *Application Id* and a *Secret* in return; we'll call them +`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this +section. + +Add the following to your Grafana configuration file to enable GitLab +authentication: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = false +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = +``` + +Restart the Grafana backend for your changes to take effect. + +If you use your own instance of GitLab instead of `gitlab.com`, adjust +`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` +hostname with your own. + +With `allow_sign_up` set to `false`, only existing users will be able to login +using their GitLab account, but with `allow_sign_up` set to `true`, *any* user +who can authenticate on GitLab will be able to login on your Grafana instance; +if you use the public `gitlab.com`, it means anyone in the world would be able +to login on your Grafana instance. + +You can can however limit access to only members of a given group or list of +groups by setting the `allowed_groups` option. + +### allowed_groups + +To limit access to authenticated users that are members of one or more [GitLab +groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` +to a comma- or space-separated list of groups. For instance, if you want to +only give access to members of the `example` group, set + + +```ini +allowed_groups = example +``` + +If you want to also give access to members of the subgroup `bar`, which is in +the group `foo`, set + +```ini +allowed_groups = example, foo/bar +``` + +Note that in GitLab, the group or subgroup name doesn't always match its +display name, especially if the display name contains spaces or special +characters. Make sure you always use the group or subgroup name as it appears +in the URL of the group or subgroup. + +Here's a complete example with `alloed_sign_up` enabled, and access limited to +the `example` and `foo/bar` groups: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = example, foo/bar +``` + +
+ +## [auth.google] + +First, you need to create a Google OAuth Client: + +1. Go to https://console.developers.google.com/apis/credentials + +2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the +menu that drops down + +3. Enter the following: + + - Application Type: Web Application + - Name: Grafana + - Authorized Javascript Origins: https://grafana.mycompany.com + - Authorized Redirect URLs: https://grafana.mycompany.com/login/google + + Replace https://grafana.mycompany.com with the URL of your Grafana instance. + +4. Click Create + +5. Copy the Client ID and Client Secret from the 'OAuth Client' modal + +Specify the Client ID and Secret in the Grafana configuration file. For example: + +```bash +[auth.google] +enabled = true +client_id = CLIENT_ID +client_secret = CLIENT_SECRET +scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +auth_url = https://accounts.google.com/o/oauth2/auth +token_url = https://accounts.google.com/o/oauth2/token +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` + +Restart the Grafana back-end. You should now see a Google login button +on the login page. You can now login or sign up with your Google +accounts. The `allowed_domains` option is optional, and domains were separated by space. + +You may allow users to sign-up via Google authentication by setting the +`allow_sign_up` option to `true`. When this option is set to `true`, any +user successfully authenticating via Google authentication will be +automatically signed up. \ No newline at end of file diff --git a/docs/sources/administration/authentication/overview.md b/docs/sources/administration/authentication/overview.md new file mode 100644 index 00000000000..e7daf581abb --- /dev/null +++ b/docs/sources/administration/authentication/overview.md @@ -0,0 +1,28 @@ ++++ +title = "Overview" +description = "Overview for auth" +type = "docs" +[menu.docs] +name = "Overview" +identifier = "overview-auth" +parent = "authentication" +weight = 1 ++++ + +## [auth] + +### disable_login_form + +Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false. + +### disable_signout_menu + +Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false. + +
+ +## [auth.basic] +### enabled +When enabled is `true` (default) the http api will accept basic authentication. + +
\ No newline at end of file diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index 8ff40b5ef04..e87d3571322 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -5,7 +5,7 @@ keywords = ["grafana", "http", "documentation", "api", "authentication"] aliases = ["/http_api/authentication/"] type = "docs" [menu.docs] -name = "Authentication" +name = "Authentication HTTP API" parent = "http_api" +++ diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4b14829b689..f61274e36fa 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -333,405 +333,31 @@ Set to true to disable the signout link in the side menu. useful if you use auth ## [auth.anonymous] -### enabled +[Read guide here.](/administration/authentication/anonymous-auth) -Set to `true` to enable anonymous access. Defaults to `false` - -### org_name - -Set the organization name that should be used for anonymous users. If -you change your organization name in the Grafana UI this setting needs -to be updated to match the new name. - -### org_role - -Specify role for anonymous users. Defaults to `Viewer`, other valid -options are `Editor` and `Admin`. +
## [auth.github] -You need to create a GitHub OAuth application (you find this under the GitHub -settings page). When you create the application you will need to specify -a callback URL. Specify this as callback: - -```bash -http://:/login/github -``` - -This callback URL must match the full HTTP address that you use in your -browser to access Grafana, but with the prefix path of `/login/github`. -When the GitHub OAuth application is created you will get a Client ID and a -Client Secret. Specify these in the Grafana configuration file. For -example: - -```bash -[auth.github] -enabled = true -allow_sign_up = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -team_ids = -allowed_organizations = -``` - -Restart the Grafana back-end. You should now see a GitHub login button -on the login page. You can now login or sign up with your GitHub -accounts. - -You may allow users to sign-up via GitHub authentication by setting the -`allow_sign_up` option to `true`. When this option is set to `true`, any -user successfully authenticating via GitHub authentication will be -automatically signed up. - -### team_ids - -Require an active team membership for at least one of the given teams on -GitHub. If the authenticated user isn't a member of at least one of the -teams they will not be able to register or authenticate with your -Grafana instance. For example: - -```bash -[auth.github] -enabled = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -team_ids = 150,300 -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -allow_sign_up = true -``` - -### allowed_organizations - -Require an active organization membership for at least one of the given -organizations on GitHub. If the authenticated user isn't a member of at least -one of the organizations they will not be able to register or authenticate with -your Grafana instance. For example - -```bash -[auth.github] -enabled = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -allow_sign_up = true -# space-delimited organization names -allowed_organizations = github google -``` +[Read guide here.](/administration/authentication/oauth/#auth-github)
## [auth.gitlab] -> Only available in Grafana v5.3+. - -You need to [create a GitLab OAuth -application](https://docs.gitlab.com/ce/integration/oauth_provider.html). -Choose a descriptive *Name*, and use the following *Redirect URI*: - -``` -https://grafana.example.com/login/gitlab -``` - -where `https://grafana.example.com` is the URL you use to connect to Grafana. -Adjust it as needed if you don't use HTTPS or if you use a different port; for -instance, if you access Grafana at `http://203.0.113.31:3000`, you should use - -``` -http://203.0.113.31:3000/login/gitlab -``` - -Finally, select *api* as the *Scope* and submit the form. Note that if you're -not going to use GitLab groups for authorization (i.e. not setting -`allowed_groups`, see below), you can select *read_user* instead of *api* as -the *Scope*, thus giving a more restricted access to your GitLab API. - -You'll get an *Application Id* and a *Secret* in return; we'll call them -`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this -section. - -Add the following to your Grafana configuration file to enable GitLab -authentication: - -```ini -[auth.gitlab] -enabled = false -allow_sign_up = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = api -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -api_url = https://gitlab.com/api/v4 -allowed_groups = -``` - -Restart the Grafana backend for your changes to take effect. - -If you use your own instance of GitLab instead of `gitlab.com`, adjust -`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` -hostname with your own. - -With `allow_sign_up` set to `false`, only existing users will be able to login -using their GitLab account, but with `allow_sign_up` set to `true`, *any* user -who can authenticate on GitLab will be able to login on your Grafana instance; -if you use the public `gitlab.com`, it means anyone in the world would be able -to login on your Grafana instance. - -You can can however limit access to only members of a given group or list of -groups by setting the `allowed_groups` option. - -### allowed_groups - -To limit access to authenticated users that are members of one or more [GitLab -groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` -to a comma- or space-separated list of groups. For instance, if you want to -only give access to members of the `example` group, set - - -```ini -allowed_groups = example -``` - -If you want to also give access to members of the subgroup `bar`, which is in -the group `foo`, set - -```ini -allowed_groups = example, foo/bar -``` - -Note that in GitLab, the group or subgroup name doesn't always match its -display name, especially if the display name contains spaces or special -characters. Make sure you always use the group or subgroup name as it appears -in the URL of the group or subgroup. - -Here's a complete example with `alloed_sign_up` enabled, and access limited to -the `example` and `foo/bar` groups: - -```ini -[auth.gitlab] -enabled = false -allow_sign_up = true -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = api -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -api_url = https://gitlab.com/api/v4 -allowed_groups = example, foo/bar -``` +[Read guide here.](/administration/authentication/oauth/#auth-gitlab)
## [auth.google] -First, you need to create a Google OAuth Client: +[Read guide here.](/administration/authentication/oauth/#auth-google) -1. Go to https://console.developers.google.com/apis/credentials - -2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the -menu that drops down - -3. Enter the following: - - - Application Type: Web Application - - Name: Grafana - - Authorized Javascript Origins: https://grafana.mycompany.com - - Authorized Redirect URLs: https://grafana.mycompany.com/login/google - - Replace https://grafana.mycompany.com with the URL of your Grafana instance. - -4. Click Create - -5. Copy the Client ID and Client Secret from the 'OAuth Client' modal - -Specify the Client ID and Secret in the Grafana configuration file. For example: - -```bash -[auth.google] -enabled = true -client_id = CLIENT_ID -client_secret = CLIENT_SECRET -scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email -auth_url = https://accounts.google.com/o/oauth2/auth -token_url = https://accounts.google.com/o/oauth2/token -allowed_domains = mycompany.com mycompany.org -allow_sign_up = true -``` - -Restart the Grafana back-end. You should now see a Google login button -on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains were separated by space. - -You may allow users to sign-up via Google authentication by setting the -`allow_sign_up` option to `true`. When this option is set to `true`, any -user successfully authenticating via Google authentication will be -automatically signed up. +
## [auth.generic_oauth] -This option could be used if have your own oauth service. - -This callback URL must match the full HTTP address that you use in your -browser to access Grafana, but with the prefix path of `/login/generic_oauth`. - -```bash -[auth.generic_oauth] -enabled = true -client_id = YOUR_APP_CLIENT_ID -client_secret = YOUR_APP_CLIENT_SECRET -scopes = -auth_url = -token_url = -api_url = -allowed_domains = mycompany.com mycompany.org -allow_sign_up = true -``` - -Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. - -### Set up oauth2 with Okta - -First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. - -Finally set up the generic oauth module like this: -```bash -[auth.generic_oauth] -name = Okta -enabled = true -scopes = openid profile email -client_id = -client_secret = -auth_url = https:///oauth2/v1/authorize -token_url = https:///oauth2/v1/token -api_url = https:///oauth2/v1/userinfo -``` - -### Set up oauth2 with Bitbucket - -```bash -[auth.generic_oauth] -name = BitBucket -enabled = true -allow_sign_up = true -client_id = -client_secret = -scopes = account email -auth_url = https://bitbucket.org/site/oauth2/authorize -token_url = https://bitbucket.org/site/oauth2/access_token -api_url = https://api.bitbucket.org/2.0/user -team_ids = -allowed_organizations = -``` - -### Set up oauth2 with OneLogin - -1. Create a new Custom Connector with the following settings: - - Name: Grafana - - Sign On Method: OpenID Connect - - Redirect URI: `https:///login/generic_oauth` - - Signing Algorithm: RS256 - - Login URL: `https:///login/generic_oauth` - - then: -2. Add an App to the Grafana Connector: - - Display Name: Grafana - - then: -3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. - - Your OneLogin Domain will match the url you use to access OneLogin. - - Configure Grafana as follows: - - ```bash - [auth.generic_oauth] - name = OneLogin - enabled = true - allow_sign_up = true - client_id = - client_secret = - scopes = openid email name - auth_url = https://.onelogin.com/oidc/auth - token_url = https://.onelogin.com/oidc/token - api_url = https://.onelogin.com/oidc/me - team_ids = - allowed_organizations = - ``` - -### Set up oauth2 with Auth0 - -1. Create a new Client in Auth0 - - Name: Grafana - - Type: Regular Web Application - -2. Go to the Settings tab and set: - - Allowed Callback URLs: `https:///login/generic_oauth` - -3. Click Save Changes, then use the values at the top of the page to configure Grafana: - - ```bash - [auth.generic_oauth] - enabled = true - allow_sign_up = true - team_ids = - allowed_organizations = - name = Auth0 - client_id = - client_secret = - scopes = openid profile email - auth_url = https:///authorize - token_url = https:///oauth/token - api_url = https:///userinfo - ``` - -### Set up oauth2 with Azure Active Directory - -1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item. - -2. Copy the "Directory ID", this is needed for setting URLs later - -3. Click "App Registrations" and add a new application registration: - - Name: Grafana - - Application type: Web app / API - - Sign-on URL: `https:///login/generic_oauth` - -4. Click the name of the new application to open the application details page. - -5. Note down the "Application ID", this will be the OAuth client id. - -6. Click "Settings", then click "Keys" and add a new entry under Passwords - - Key Description: Grafana OAuth - - Duration: Never Expires - -7. Click Save then copy the key value, this will be the OAuth client secret. - -8. Configure Grafana as follows: - - ```bash - [auth.generic_oauth] - name = Azure AD - enabled = true - allow_sign_up = true - client_id = - client_secret = - scopes = openid email name - auth_url = https://login.microsoftonline.com//oauth2/authorize - token_url = https://login.microsoftonline.com//oauth2/token - api_url = - team_ids = - allowed_organizations = - ``` - +[Read guide here.](/administration/authentication/oauth/#auth-generic-oauth)
## [auth.basic] @@ -741,48 +367,14 @@ When enabled is `true` (default) the http api will accept basic authentication.
## [auth.ldap] -### enabled -Set to `true` to enable LDAP integration (default: `false`) -### config_file -Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) - -### allow_sign_up - -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). - -> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page. +[Read guide here.](/administration/authentication/ldap/)
## [auth.proxy] -This feature allows you to handle authentication in a http reverse proxy. - -### enabled - -Defaults to `false` - -### header_name - -Defaults to X-WEBAUTH-USER - -#### header_property - -Defaults to username but can also be set to email - -### auto_sign_up - -Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`. - -### whitelist - -Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. - -### headers - -Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. +[Read guide here.](/administration/authentication/auth-proxy/)
From 1c9781627523a4005cdf61be684c72f7c81f7364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 24 Aug 2018 18:46:17 +0200 Subject: [PATCH 0045/2611] wip: trying to align react & angular edit modes --- public/app/core/directives/dash_class.ts | 4 +- .../dashboard/dashgrid/DashboardGrid.tsx | 2 +- .../dashboard/dashgrid/PanelHeader.tsx | 2 +- public/app/features/panel/panel_directive.ts | 82 ++++++++++--------- public/app/stores/ViewStore/ViewStore.ts | 2 + 5 files changed, 51 insertions(+), 41 deletions(-) diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index c164acf7bfc..53d5a83a256 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -5,7 +5,9 @@ coreModule.directive('dashClass', function($timeout) { return { link: function($scope, elem) { $scope.ctrl.dashboard.events.on('view-mode-changed', function(panel) { - elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); + $timeout(() => { + elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); + }, 10); }); elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 77b55a2130f..3b2615787ea 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -165,7 +165,7 @@ export class DashboardGrid extends React.Component { componentDidMount() { setTimeout(() => { - this.setState({ animated: true }); + this.setState({ animated: false }); }); } diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx index 8c3dc6a324e..d8fd5f9aa89 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -26,7 +26,7 @@ export class PanelHeader extends React.Component { { panelId: this.props.panel.id, fullscreen: true, - edit: null, + edit: false, }, false ); diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index f4ed06007a5..1d256927a8a 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -6,48 +6,53 @@ import baron from 'baron'; var module = angular.module('grafana.directives'); var panelTemplate = ` -
-
- - - - +
+
+
+
+ + + + - - - + + + - -
+ +
-
- -
-
- -
-
-
-

- {{ctrl.pluginName}} -

- - - - +
+ +
+
-
-
- +
+
+
+

+ {{ctrl.pluginName}} +

+ + + + +
+ +
+
+ +
@@ -86,7 +91,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { } function panelHeightUpdated() { - panelContent.css({ height: ctrl.height + 'px' }); + // panelContent.css({ height: ctrl.height + 'px' }); } function resizeScrollableContent() { @@ -135,6 +140,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { ctrl.calculatePanelHeight(); panelHeightUpdated(); $timeout(() => { + console.log('panel directive panel size changed, render'); resizeScrollableContent(); ctrl.render(); }); diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index 83cb01d4bd4..6529cc954d9 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -30,6 +30,8 @@ export const ViewStore = types for (let key of Object.keys(query)) { if (query[key]) { self.query.set(key, query[key]); + } else { + self.query.delete(key); } } } From 91b343403c28caa03b4a74afb23c9bbf72b88bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 25 Aug 2018 07:21:00 -0700 Subject: [PATCH 0046/2611] wip: minor fixes --- public/app/core/components/scroll/scroll.ts | 1 + public/app/core/directives/dash_class.ts | 4 +--- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 2 +- public/sass/components/_dashboard_grid.scss | 5 +++++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index 3f9865e6dce..b45f7ca8bd7 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -19,6 +19,7 @@ export function geminiScrollbar() { let scrollRoot = elem.parent(); let scroller = elem; + console.log('scroll'); if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { scrollRoot = scroller; } diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 53d5a83a256..c164acf7bfc 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -5,9 +5,7 @@ coreModule.directive('dashClass', function($timeout) { return { link: function($scope, elem) { $scope.ctrl.dashboard.events.on('view-mode-changed', function(panel) { - $timeout(() => { - elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); - }, 10); + elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); }); elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 3b2615787ea..77b55a2130f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -165,7 +165,7 @@ export class DashboardGrid extends React.Component { componentDidMount() { setTimeout(() => { - this.setState({ animated: false }); + this.setState({ animated: true }); }); } diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 26326013dab..da1f140d252 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -31,6 +31,11 @@ .react-resizable-handle { display: none; } + + // the react-grid has a height transition + .react-grid-layout { + transition-property: none; + } } @include media-breakpoint-down(sm) { From 4424bdd1b1fd4a2ed50cc0ec0fb138f5bdb7d6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 25 Aug 2018 07:37:37 -0700 Subject: [PATCH 0047/2611] fix: going from fullscreen fix --- public/app/core/directives/dash_class.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index c164acf7bfc..2a8db641f3f 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -5,7 +5,13 @@ coreModule.directive('dashClass', function($timeout) { return { link: function($scope, elem) { $scope.ctrl.dashboard.events.on('view-mode-changed', function(panel) { - elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); + if (panel.fullscreen) { + elem.addClass('panel-in-fullscreen'); + } else { + $timeout(() => { + elem.removeClass('panel-in-fullscreen'); + }); + } }); elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); From 6ba8f6c5ab6e4c42eb8b6ce066d14e603a8e2fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 25 Aug 2018 08:49:39 -0700 Subject: [PATCH 0048/2611] wip: major change for refresh and render events flow --- .../app/features/dashboard/dashboard_model.ts | 28 ++++++++++++++++++ public/app/features/dashboard/panel_model.ts | 19 ++++++++++++ public/app/features/dashboard/time_srv.ts | 3 +- .../dashboard/timepicker/timepicker.ts | 3 +- .../app/features/dashboard/view_state_srv.ts | 29 +++++++++---------- public/app/features/panel/panel_ctrl.ts | 6 ++-- public/app/features/panel/panel_directive.ts | 1 + 7 files changed, 66 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 92392fc80e8..af665cd8dc3 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -200,6 +200,34 @@ export class DashboardModel { this.events.emit('view-mode-changed', panel); } + startRefresh() { + this.events.emit('refresh'); + + for (const panel of this.panels) { + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + } + } + + render() { + this.events.emit('render'); + + for (const panel of this.panels) { + panel.render(); + } + } + + panelInitialized(panel: PanelModel) { + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + } + + otherPanelInFullscreen(panel: PanelModel) { + return this.meta.fullscreen && !panel.fullscreen; + } + private ensureListExist(data) { if (!data) { data = {}; diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 8c9270ad1ab..0abfaa06945 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -13,6 +13,7 @@ const notPersistedProperties: { [str: string]: boolean } = { events: true, fullscreen: true, isEditing: true, + hasRefreshed: true, }; export class PanelModel { @@ -37,6 +38,7 @@ export class PanelModel { // non persisted fullscreen: boolean; isEditing: boolean; + hasRefreshed: boolean; events: Emitter; constructor(model) { @@ -93,6 +95,23 @@ export class PanelModel { this.events.emit('panel-size-changed'); } + refresh() { + this.hasRefreshed = true; + this.events.emit('refresh'); + } + + render() { + if (!this.hasRefreshed) { + this.refresh(); + } else { + this.events.emit('render'); + } + } + + panelInitialized() { + this.events.emit('panel-initialized'); + } + initEditMode() { this.events.emit('panel-init-edit-mode'); } diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 7fd5aed7847..85eaaf6a714 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -24,7 +24,6 @@ export class TimeSrv { document.addEventListener('visibilitychange', () => { if (this.autoRefreshBlocked && document.visibilityState === 'visible') { this.autoRefreshBlocked = false; - this.refreshDashboard(); } }); @@ -136,7 +135,7 @@ export class TimeSrv { } refreshDashboard() { - this.$rootScope.$broadcast('refresh'); + this.dashboard.startRefresh(); } private startNextRefreshTimer(afterMs) { diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index 33cfff92e7f..ce8c4130973 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -30,9 +30,10 @@ export class TimePickerCtrl { $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope); $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope); - $rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope); $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope); + this.dashboard.on('refresh', this.onRefresh.bind(this), $scope); + // init options this.panel = this.dashboard.timepicker; _.defaults(this.panel, TimePickerCtrl.defaults); diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 73ec8fc0638..56b424c4afb 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -1,6 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; import config from 'app/core/config'; +import appEvents from 'app/core/app_events'; import { DashboardModel } from './dashboard_model'; // represents the transient view state @@ -132,7 +133,7 @@ export class DashboardViewState { if (this.fullscreenPanel === panel && this.editStateChanged === false) { return; } else { - this.leaveFullscreen(false); + this.leaveFullscreen(); } } @@ -140,30 +141,26 @@ export class DashboardViewState { this.enterFullscreen(panel); } } else if (this.fullscreenPanel) { - this.leaveFullscreen(true); + this.leaveFullscreen(); } } - leaveFullscreen(render) { - var panel = this.fullscreenPanel; + leaveFullscreen() { + const panel = this.fullscreenPanel; this.dashboard.setViewMode(panel, false, false); - this.$scope.appEvent('dash-scroll', { restore: true }); - if (!render) { - return false; - } + delete this.fullscreenPanel; this.$timeout(() => { - if (this.oldTimeRange !== this.dashboard.time) { - this.$rootScope.$broadcast('refresh'); - } else { - this.$rootScope.$broadcast('render'); - } - delete this.fullscreenPanel; - }); + appEvents.emit('dash-scroll', { restore: true }); - return true; + if (this.oldTimeRange !== this.dashboard.time) { + this.dashboard.startRefresh(); + } else { + this.dashboard.render(); + } + }); } enterFullscreen(panel) { diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 8f79a789e76..686072e3bcd 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -47,7 +47,6 @@ export class PanelCtrl { this.pluginName = plugin.name; } - $scope.$on('refresh', () => this.refresh()); $scope.$on('component-did-mount', () => this.panelDidMount()); $scope.$on('$destroy', () => { @@ -57,8 +56,7 @@ export class PanelCtrl { } init() { - this.events.emit('panel-initialized'); - this.publishAppEvent('panel-initialized', { scope: this.$scope }); + this.dashboard.panelInitialized(this.panel); } panelDidMount() { @@ -70,7 +68,7 @@ export class PanelCtrl { } refresh() { - this.events.emit('refresh', null); + this.panel.refresh(); } publishAppEvent(evtName, evt) { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 1d256927a8a..ea78c61f847 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -151,6 +151,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { panelHeightUpdated(); ctrl.events.on('render', () => { + console.log('panel_directive: render', ctrl.panel.id); if (transparentLastState !== ctrl.panel.transparent) { panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true); transparentLastState = ctrl.panel.transparent; From fd81f895091241aa27b004f8fa8299aab1c48900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 25 Aug 2018 12:22:50 -0700 Subject: [PATCH 0049/2611] wip: angular panels now have similar edit mode and panel type selection enabling quick changing between panel react and angular panel types --- .../core/services/dynamic_directive_srv.ts | 30 +++++------- .../app/features/dashboard/dashboard_model.ts | 5 ++ .../dashboard/dashgrid/DashboardGrid.tsx | 3 +- .../dashboard/dashgrid/DashboardPanel.tsx | 15 +++++- .../dashboard/dashgrid/PanelEditor.tsx | 7 ++- public/app/features/dashboard/panel_model.ts | 9 ++-- .../app/features/dashboard/view_state_srv.ts | 6 +-- .../app/features/panel/metrics_panel_ctrl.ts | 3 -- public/app/features/panel/panel_ctrl.ts | 9 +++- public/app/features/panel/panel_directive.ts | 6 +-- public/app/features/panel/panel_editor_tab.ts | 30 +++++++----- public/app/features/panel/viz_tab.ts | 48 +++++++++++++++++++ .../app/features/plugins/plugin_component.ts | 1 + 13 files changed, 121 insertions(+), 51 deletions(-) create mode 100644 public/app/features/panel/viz_tab.ts diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 086843b6f9a..7757c564da5 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; class DynamicDirectiveSrv { /** @ngInject */ - constructor(private $compile, private $rootScope) {} + constructor(private $compile) {} addDirective(element, name, scope) { var child = angular.element(document.createElement(name)); @@ -14,25 +14,19 @@ class DynamicDirectiveSrv { } link(scope, elem, attrs, options) { - options - .directive(scope) - .then(directiveInfo => { - if (!directiveInfo || !directiveInfo.fn) { - elem.empty(); - return; - } + const directiveInfo = options.directive(scope); + if (!directiveInfo || !directiveInfo.fn) { + elem.empty(); + return; + } - if (!directiveInfo.fn.registered) { - coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); - directiveInfo.fn.registered = true; - } + if (!directiveInfo.fn.registered) { + console.log('register panel tab'); + coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); + directiveInfo.fn.registered = true; + } - this.addDirective(elem, directiveInfo.name, scope); - }) - .catch(err => { - console.log('Plugin load:', err); - this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]); - }); + this.addDirective(elem, directiveInfo.name, scope); } create(options) { diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index af665cd8dc3..d1c52f168be 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -228,6 +228,11 @@ export class DashboardModel { return this.meta.fullscreen && !panel.fullscreen; } + changePanelType(panel: PanelModel, pluginId: string) { + panel.changeType(pluginId); + this.events.emit('panel-type-changed', panel); + } + private ensureListExist(data) { if (!data) { data = {}; diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 77b55a2130f..6f9f3a83ef9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -84,6 +84,7 @@ export class DashboardGrid extends React.Component { dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this)); dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); + dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this)); } buildLayout() { @@ -177,7 +178,7 @@ export class DashboardGrid extends React.Component { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
- +
); } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 4b2e81b969c..6958b43b05d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -11,6 +11,7 @@ import { PanelChrome } from './PanelChrome'; import { PanelEditor } from './PanelEditor'; export interface Props { + panelType: string; panel: PanelModel; dashboard: DashboardModel; } @@ -53,6 +54,10 @@ export class DashboardPanel extends React.Component { this.loadPlugin(); }; + onAngularPluginTypeChanged = () => { + this.loadPlugin(); + }; + loadPlugin() { if (this.isSpecial()) { return; @@ -63,9 +68,11 @@ export class DashboardPanel extends React.Component { this.pluginInfo = config.panels[this.props.panel.type]; if (this.pluginInfo.exports) { + this.cleanUpAngularPanel(); this.setState({ pluginExports: this.pluginInfo.exports }); } else { importPluginModule(this.pluginInfo.module).then(pluginExports => { + this.cleanUpAngularPanel(); // cache plugin exports (saves a promise async cycle next time) this.pluginInfo.exports = pluginExports; // update panel state @@ -80,7 +87,6 @@ export class DashboardPanel extends React.Component { } componentDidUpdate() { - console.log('componentDidUpdate'); this.loadPlugin(); // handle angular plugin loading @@ -94,12 +100,17 @@ export class DashboardPanel extends React.Component { this.angularPanel = loader.load(this.element, scopeProps, template); } - componentWillUnmount() { + cleanUpAngularPanel() { if (this.angularPanel) { this.angularPanel.destroy(); + this.angularPanel = null; } } + componentWillUnmount() { + this.cleanUpAngularPanel(); + } + renderReactPanel() { const { pluginExports } = this.state; const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper'; diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 70b5bf11815..a9b7802fb4b 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -31,7 +31,7 @@ export class PanelEditor extends React.Component { this.tabs = [ { id: 'queries', text: 'Queries', icon: 'fa fa-database' }, - { id: 'viz', text: 'Visualization', icon: 'fa fa-line-chart' }, + { id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' }, ]; } @@ -87,7 +87,7 @@ export class PanelEditor extends React.Component {
{activeTab === 'queries' && this.renderQueriesTab()} - {activeTab === 'viz' && this.renderVizTab()} + {activeTab === 'visualization' && this.renderVizTab()}
); @@ -109,8 +109,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) { return (
  • onClick(tab)}> - - {tab.text} + {tab.text}
  • ); diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 0abfaa06945..6192047c84e 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -34,6 +34,7 @@ export class PanelModel { soloMode?: boolean; targets: any[]; datasource: string; + thresholds?: any; // non persisted fullscreen: boolean; @@ -116,9 +117,11 @@ export class PanelModel { this.events.emit('panel-init-edit-mode'); } - changeType(newType: string) { - this.type = newType; - this.events.emit('panel-size-changed'); + changeType(pluginId: string) { + this.type = pluginId; + + delete this.thresholds; + delete this.alert; } destroy() { diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 56b424c4afb..266fdb16d13 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -16,7 +16,7 @@ export class DashboardViewState { oldTimeRange: any; /** @ngInject */ - constructor($scope, private $location, private $timeout, private $rootScope) { + constructor($scope, private $location, private $timeout) { var self = this; self.state = {}; self.panelScopes = []; @@ -176,10 +176,10 @@ export class DashboardViewState { } /** @ngInject */ -export function dashboardViewStateSrv($location, $timeout, $rootScope) { +export function dashboardViewStateSrv($location, $timeout) { return { create: function($scope) { - return new DashboardViewState($scope, $location, $timeout, $rootScope); + return new DashboardViewState($scope, $location, $timeout); }, }; } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 432ca847a33..9abda3e8b4d 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -8,8 +8,6 @@ import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import { encodePathComponent } from 'app/core/utils/location_util'; -import { metricsTabDirective } from './metrics_tab'; - class MetricsPanelCtrl extends PanelCtrl { scope: any; datasource: any; @@ -58,7 +56,6 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - this.addEditorTab('Metrics', metricsTabDirective); this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 686072e3bcd..4b57c15dfd5 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -6,6 +6,8 @@ import { PanelModel } from 'app/features/dashboard/panel_model'; import Remarkable from 'remarkable'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants'; import store from 'app/core/store'; +import { metricsTabDirective } from './metrics_tab'; +import { vizTabDirective } from './viz_tab'; const TITLE_HEIGHT = 27; const PANEL_BORDER = 2; @@ -97,7 +99,10 @@ export class PanelCtrl { initEditMode() { this.editorTabs = []; + this.addEditorTab('Queries', metricsTabDirective, 0, 'fa fa-database'); + this.addEditorTab('Visualization', vizTabDirective, 1, 'fa fa-line-chart'); this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); + this.editModeInitiated = true; this.events.emit('init-edit-mode', null); @@ -118,8 +123,8 @@ export class PanelCtrl { route.updateParams(); } - addEditorTab(title, directiveFn, index?) { - var editorTab = { title, directiveFn }; + addEditorTab(title, directiveFn, index?, icon?) { + var editorTab = { title, directiveFn, icon }; if (_.isString(directiveFn)) { editorTab.directiveFn = function() { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index ea78c61f847..191396bf97d 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -32,13 +32,11 @@ var panelTemplate = ` 'panel-height-helper': !ctrl.panel.isEditing}">
    -

    - {{ctrl.pluginName}} -

    -
    +
    diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts index 89eaba88299..dd7f5fdce5f 100644 --- a/public/app/features/panel/panel_editor_tab.ts +++ b/public/app/features/panel/panel_editor_tab.ts @@ -1,6 +1,7 @@ import angular from 'angular'; -var directiveModule = angular.module('grafana.directives'); +const directiveModule = angular.module('grafana.directives'); +const directiveCache = {}; /** @ngInject */ function panelEditorTab(dynamicDirectiveSrv) { @@ -11,18 +12,25 @@ function panelEditorTab(dynamicDirectiveSrv) { index: '=', }, directive: scope => { - var pluginId = scope.ctrl.pluginId; - var tabIndex = scope.index; - // create a wrapper for directiveFn - // required for metrics tab directive - // that is the same for many panels but - // given different names in this function - var fn = () => scope.editorTab.directiveFn(); + const pluginId = scope.ctrl.pluginId; + const tabIndex = scope.index; - return Promise.resolve({ + if (directiveCache[pluginId]) { + if (directiveCache[pluginId][tabIndex]) { + return directiveCache[pluginId][tabIndex]; + } + } else { + directiveCache[pluginId] = []; + } + + let result = { + fn: () => scope.editorTab.directiveFn(), name: `panel-editor-tab-${pluginId}${tabIndex}`, - fn: fn, - }); + }; + + directiveCache[pluginId][tabIndex] = result; + + return result; }, }); } diff --git a/public/app/features/panel/viz_tab.ts b/public/app/features/panel/viz_tab.ts new file mode 100644 index 00000000000..db3c7921475 --- /dev/null +++ b/public/app/features/panel/viz_tab.ts @@ -0,0 +1,48 @@ +import coreModule from 'app/core/core_module'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import { VizTypePicker } from '../dashboard/dashgrid/VizTypePicker'; +import { react2AngularDirective } from 'app/core/utils/react2angular'; +import { PanelPlugin } from 'app/types/plugins'; + +export class VizTabCtrl { + panelCtrl: any; + dashboard: DashboardModel; + + /** @ngInject */ + constructor($scope) { + this.panelCtrl = $scope.ctrl; + this.dashboard = this.panelCtrl.dashboard; + + $scope.ctrl = this; + } + + onTypeChanged = (plugin: PanelPlugin) => { + this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id); + }; +} + +let template = ` +
    +
    + +
    +
    +
    Options
    +
    +
    +`; + +/** @ngInject **/ +export function vizTabDirective() { + 'use strict'; + return { + restrict: 'E', + scope: true, + template: template, + controller: VizTabCtrl, + }; +} + +react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]); + +coreModule.directive('vizTab', vizTabDirective); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 5ef4019c24d..5375d09d2f5 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -211,6 +211,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ scope.$applyAsync(function() { scope.$broadcast('component-did-mount'); scope.$broadcast('refresh'); + console.log('appendAndCompile', scope.panel); }); }); } From fda9790ba5b1a143eea59187d7c651ec00b7de53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 25 Aug 2018 21:23:20 +0200 Subject: [PATCH 0050/2611] upgrades to golang 1.11 --- .circleci/config.yml | 8 ++++---- Dockerfile | 2 +- README.md | 2 +- appveyor.yml | 2 +- docs/sources/project/building_from_source.md | 2 +- scripts/build/Dockerfile | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e046aec34d..b4480b4bade 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ version: 2 jobs: mysql-integration-test: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 - image: circleci/mysql:5.6-ram environment: MYSQL_ROOT_PASSWORD: rootpass @@ -39,7 +39,7 @@ jobs: postgres-integration-test: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 - image: circleci/postgres:9.3-ram environment: POSTGRES_USER: grafanatest @@ -74,7 +74,7 @@ jobs: gometalinter: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 environment: # we need CGO because of go-sqlite3 CGO_ENABLED: 1 @@ -115,7 +115,7 @@ jobs: test-backend: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/Dockerfile b/Dockerfile index f7e45893c38..28dd71952af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Golang build container -FROM golang:1.10 +FROM golang:1.11 WORKDIR $GOPATH/src/github.com/grafana/grafana diff --git a/README.md b/README.md index 74fb10c8066..133d9e50d07 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies -- Go 1.10 +- Go 1.11 - NodeJS LTS ### Building the backend diff --git a/appveyor.yml b/appveyor.yml index 5cdec1b8bf5..52f23162033 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "6" GOPATH: C:\gopath - GOVERSION: 1.10 + GOVERSION: 1.11 install: - rmdir c:\go /s /q diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 08673404572..e83c62ca800 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple ## Dependencies -- [Go 1.10](https://golang.org/dl/) +- [Go 1.11](https://golang.org/dl/) - [Git](https://git-scm.com/downloads) - [NodeJS LTS](https://nodejs.org/download/) - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details. diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 808e7f141e9..c7f4fecc649 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -21,7 +21,7 @@ RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A170311380 RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ yum install -y nodejs --nogpgcheck -ENV GOLANG_VERSION 1.10 +ENV GOLANG_VERSION 1.11 RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \ From 864c4691da82ba1b62854a821d312c5581365565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 25 Aug 2018 12:38:25 -0700 Subject: [PATCH 0051/2611] fix: minor fix to changing type --- public/app/features/plugins/plugin_component.ts | 1 - public/sass/components/_tabbed_view.scss | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 5375d09d2f5..aafffc77fa0 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -246,7 +246,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ registerPluginComponent(scope, elem, attrs, componentInfo); }) .catch(err => { - $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]); console.log('Plugin component error', err); }); }, diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index 80e76b5fbf4..87b43a31142 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -4,7 +4,7 @@ height: 100%; &.tabbed-view--new { - padding: 10px 0 0 0; + padding: 25px 0 0 0; height: 100%; } } From e62c083cf0da76d995ef71e51b5b1e68364ef6e2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Jun 2018 14:03:52 +0900 Subject: [PATCH 0052/2611] 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 0053/2611] 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 ff7b0d4f6347366bcc6827d49359fba568e81f63 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 28 Aug 2018 22:14:15 +0200 Subject: [PATCH 0054/2611] go fmt fixes --- 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 12c98608826250ed481197fa1eeeb2aae2457c3d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 28 Aug 2018 22:26:47 +0200 Subject: [PATCH 0055/2611] string formating fixes --- pkg/api/live/conn.go | 2 +- pkg/cmd/grafana-cli/services/services.go | 4 ++-- pkg/components/imguploader/s3uploader.go | 2 +- pkg/log/log.go | 2 +- pkg/login/ext_user.go | 4 ++-- pkg/middleware/auth_proxy.go | 6 +++--- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/sqlstore/sqlstore.go | 2 +- pkg/services/sqlstore/transactions.go | 2 +- pkg/setting/setting.go | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go index f2a041d7631..0fae7f75b73 100644 --- a/pkg/api/live/conn.go +++ b/pkg/api/live/conn.go @@ -70,7 +70,7 @@ func (c *connection) readPump() { func (c *connection) handleMessage(message []byte) { json, err := simplejson.NewJson(message) if err != nil { - log.Error(3, "Unreadable message on websocket channel:", err) + log.Error(3, "Unreadable message on websocket channel. error: %v", err) } msgType := json.Get("action").MustString() diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index e743d42022c..b4e50ac84df 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: %v", err) + logger.Info("Failed to unmarshal graphite 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: %v", err) + logger.Info("Failed to unmarshal graphite response error:", err) return m.Plugin{}, err } diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index 62196357c61..a1e4aed0f47 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -60,7 +60,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region) key := u.path + util.GetRandomString(20) + ".png" image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key - log.Debug("Uploading image to s3", "url = ", image_url) + log.Debug("Uploading image to s3. url = %s", image_url) file, err := os.Open(imageDiskPath) if err != nil { diff --git a/pkg/log/log.go b/pkg/log/log.go index 0e6874e1b4b..8154b9b7f07 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -103,7 +103,7 @@ func Critical(skip int, format string, v ...interface{}) { } func Fatal(skip int, format string, v ...interface{}) { - Root.Crit(fmt.Sprintf(format, v)) + Root.Crit(fmt.Sprintf(format, v...)) Close() os.Exit(1) } diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index a421e3ebe0a..1262c1cc44f 100644 --- a/pkg/login/ext_user.go +++ b/pkg/login/ext_user.go @@ -35,7 +35,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { limitReached, err := quota.QuotaReached(cmd.ReqContext, "user") if err != nil { - log.Warn("Error getting user quota", "err", err) + log.Warn("Error getting user quota. error: %v", err) return ErrGettingUserQuota } if limitReached { @@ -135,7 +135,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error { return nil } - log.Debug("Syncing user info", "id", user.Id, "update", updateCmd) + log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd) return bus.Dispatch(updateCmd) } diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 144a0ae3a69..29bd305b336 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -36,7 +36,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { // initialize session if err := ctx.Session.Start(ctx.Context); err != nil { - log.Error(3, "Failed to start session", err) + log.Error(3, "Failed to start session. error %v", err) return false } @@ -146,12 +146,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId { // remove session if err := ctx.Session.Destory(ctx.Context); err != nil { - log.Error(3, "Failed to destroy session, err") + log.Error(3, "Failed to destroy session. error: %v", err) } // initialize a new session if err := ctx.Session.Start(ctx.Context); err != nil { - log.Error(3, "Failed to start session", err) + log.Error(3, "Failed to start session. error: %v", err) } } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index ca24c996914..d79552079d5 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -216,7 +216,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string { if len(extra)+len(message) <= sizeLimit { return message + extra } - log.Debug("Line too long for image caption.", "value", extra) + log.Debug("Line too long for image caption. value: %s", extra) return message } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 13d706b6198..5477bc7b2d1 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -106,7 +106,7 @@ func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTr if len(sess.events) > 0 { for _, e := range sess.events { if err = bus.Publish(e); err != nil { - log.Error(3, "Failed to publish event after commit", err) + log.Error(3, "Failed to publish event after commit. error: %v", err) } } } diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go index eccd37f9a43..edf29fffb8f 100644 --- a/pkg/services/sqlstore/transactions.go +++ b/pkg/services/sqlstore/transactions.go @@ -89,7 +89,7 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, if len(sess.events) > 0 { for _, e := range sess.events { if err = bus.Publish(e); err != nil { - log.Error(3, "Failed to publish event after commit", err) + log.Error(3, "Failed to publish event after commit. error: %v", err) } } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index eb61568261d..aee9c00b526 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -324,7 +324,7 @@ func getCommandLineProperties(args []string) map[string]string { trimmed := strings.TrimPrefix(arg, "cfg:") parts := strings.Split(trimmed, "=") if len(parts) != 2 { - log.Fatal(3, "Invalid command line argument", arg) + log.Fatal(3, "Invalid command line argument. argument: %v", arg) return nil } From 41b5dae606b834b29f218be5ab727e7985897c9d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 30 Aug 2018 16:52:12 +0200 Subject: [PATCH 0056/2611] start implementing mysql query editor as a copy of postgres query editor --- .../plugins/datasource/mysql/meta_query.ts | 139 +++++ .../plugins/datasource/mysql/mysql_query.ts | 285 +++++++++ .../mysql/partials/query.editor.html | 106 +++- .../plugins/datasource/mysql/query_ctrl.ts | 570 +++++++++++++++++- .../app/plugins/datasource/mysql/sql_part.ts | 86 +++ 5 files changed, 1168 insertions(+), 18 deletions(-) create mode 100644 public/app/plugins/datasource/mysql/meta_query.ts create mode 100644 public/app/plugins/datasource/mysql/mysql_query.ts create mode 100644 public/app/plugins/datasource/mysql/sql_part.ts diff --git a/public/app/plugins/datasource/mysql/meta_query.ts b/public/app/plugins/datasource/mysql/meta_query.ts new file mode 100644 index 00000000000..94e3e8fc3d6 --- /dev/null +++ b/public/app/plugins/datasource/mysql/meta_query.ts @@ -0,0 +1,139 @@ +export class MysqlMetaQuery { + constructor(private target, private queryModel) {} + + getOperators(datatype: string) { + switch (datatype) { + case 'float4': + case 'float8': { + return ['=', '!=', '<', '<=', '>', '>=']; + } + case 'text': + case 'varchar': + case 'char': { + return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE', '~', '~*', '!~', '!~*']; + } + default: { + return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN']; + } + } + } + + // quote identifier as literal to use in metadata queries + quoteIdentAsLiteral(value) { + return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value)); + } + + findMetricTable() { + // query that returns first table found that has a timestamp(tz) column and a float column + let query = ` + SELECT + table_name as table_name, + ( SELECT + column_name as column_name + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN ('timestamp', 'datetime') + ORDER BY ordinal_position LIMIT 1 + ) AS time_column, + ( SELECT + column_name AS column_name + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN('float', 'int', 'bigint') + ORDER BY ordinal_position LIMIT 1 + ) AS value_column + FROM information_schema.tables t + WHERE + EXISTS + ( SELECT 1 + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN ('timestamp', 'datetime') + ) AND + EXISTS + ( SELECT 1 + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN('float', 'int', 'bigint') + ) + LIMIT 1 +;`; + return query; + } + + buildTableConstraint(table: string) { + let query = ''; + + // check for schema qualified table + if (table.includes('.')) { + let parts = table.split('.'); + query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]); + query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]); + return query; + } else { + query = ' table_name = ' + this.quoteIdentAsLiteral(table); + + return query; + } + } + + buildTableQuery() { + return 'SELECT table_name FROM information_schema.tables ORDER BY table_name'; + } + + buildColumnQuery(type?: string) { + let query = 'SELECT column_name FROM information_schema.columns WHERE '; + query += this.buildTableConstraint(this.target.table); + + switch (type) { + case 'time': { + query += " AND data_type IN ('timestamp','datetime','bigint','int','float')"; + break; + } + case 'metric': { + query += " AND data_type IN ('text' 'tinytext','mediumtext', 'longtext', 'varchar')"; + break; + } + case 'value': { + query += + " AND data_type IN ('bigint','int','float','smallint', 'mediumint', 'tinyint', 'double', 'decimal', 'float')"; + query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn); + break; + } + case 'group': { + query += " AND data_type IN ('text' 'tinytext','mediumtext', 'longtext', 'varchar')"; + break; + } + } + + query += ' ORDER BY column_name'; + + return query; + } + + buildValueQuery(column: string) { + let query = 'SELECT DISTINCT QUOTE(' + column + ')'; + query += ' FROM ' + this.target.table; + query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')'; + query += ' ORDER BY 1 LIMIT 100'; + return query; + } + + buildDatatypeQuery(column: string) { + let query = ` +SELECT data_type +FROM information_schema.columns +WHERE `; + query += ' table_name = ' + this.quoteIdentAsLiteral(this.target.table); + query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); + return query; + } +} diff --git a/public/app/plugins/datasource/mysql/mysql_query.ts b/public/app/plugins/datasource/mysql/mysql_query.ts new file mode 100644 index 00000000000..1c4b927ceea --- /dev/null +++ b/public/app/plugins/datasource/mysql/mysql_query.ts @@ -0,0 +1,285 @@ +import _ from 'lodash'; + +export default class MysqlQuery { + target: any; + templateSrv: any; + scopedVars: any; + + /** @ngInject */ + constructor(target, templateSrv?, scopedVars?) { + this.target = target; + this.templateSrv = templateSrv; + this.scopedVars = scopedVars; + + target.format = target.format || 'time_series'; + target.timeColumn = target.timeColumn || 'time'; + target.metricColumn = target.metricColumn || 'none'; + + target.group = target.group || []; + target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }]; + target.select = target.select || [[{ type: 'column', params: ['value'] }]]; + + // handle pre query gui panels gracefully + if (!('rawQuery' in this.target)) { + if ('rawSql' in target) { + // pre query gui panel + target.rawQuery = true; + } else { + // new panel + target.rawQuery = false; + } + } + + // give interpolateQueryStr access to this + this.interpolateQueryStr = this.interpolateQueryStr.bind(this); + } + + // remove identifier quoting from identifier to use in metadata queries + unquoteIdentifier(value) { + if (value[0] === '"' && value[value.length - 1] === '"') { + return value.substring(1, value.length - 1).replace(/""/g, '"'); + } else { + return value; + } + } + + quoteIdentifier(value) { + return '"' + value.replace(/"/g, '""') + '"'; + } + + quoteLiteral(value) { + return "'" + value.replace(/'/g, "''") + "'"; + } + + escapeLiteral(value) { + return value.replace(/'/g, "''"); + } + + hasTimeGroup() { + return _.find(this.target.group, (g: any) => g.type === 'time'); + } + + hasMetricColumn() { + return this.target.metricColumn !== 'none'; + } + + interpolateQueryStr(value, variable, defaultFormatFn) { + // if no multi or include all do not regexEscape + if (!variable.multi && !variable.includeAll) { + return this.escapeLiteral(value); + } + + if (typeof value === 'string') { + return this.quoteLiteral(value); + } + + let escapedValues = _.map(value, this.quoteLiteral); + return escapedValues.join(','); + } + + render(interpolate?) { + let target = this.target; + + // new query with no table set yet + if (!this.target.rawQuery && !('table' in this.target)) { + return ''; + } + + if (!target.rawQuery) { + target.rawSql = this.buildQuery(); + } + + if (interpolate) { + return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr); + } else { + return target.rawSql; + } + } + + hasUnixEpochTimecolumn() { + return ['int4', 'int8', 'float4', 'float8', 'numeric'].indexOf(this.target.timeColumnType) > -1; + } + + buildTimeColumn(alias = true) { + let timeGroup = this.hasTimeGroup(); + let query; + let macro = '$__timeGroup'; + + if (timeGroup) { + let args; + if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') { + args = timeGroup.params.join(','); + } else { + args = timeGroup.params[0]; + } + if (this.hasUnixEpochTimecolumn()) { + macro = '$__unixEpochGroup'; + } + if (alias) { + macro += 'Alias'; + } + query = macro + '(' + this.target.timeColumn + ',' + args + ')'; + } else { + query = this.target.timeColumn; + if (alias) { + query += ' AS "time"'; + } + } + + return query; + } + + buildMetricColumn() { + if (this.hasMetricColumn()) { + return this.target.metricColumn + ' AS metric'; + } + + return ''; + } + + buildValueColumns() { + let query = ''; + for (let column of this.target.select) { + query += ',\n ' + this.buildValueColumn(column); + } + + return query; + } + + buildValueColumn(column) { + let query = ''; + + let columnName = _.find(column, (g: any) => g.type === 'column'); + query = columnName.params[0]; + + let aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); + let windows = _.find(column, (g: any) => g.type === 'window' || g.type === 'moving_window'); + + if (aggregate) { + let func = aggregate.params[0]; + switch (aggregate.type) { + case 'aggregate': + if (func === 'first' || func === 'last') { + query = func + '(' + query + ',' + this.target.timeColumn + ')'; + } else { + query = func + '(' + query + ')'; + } + break; + case 'percentile': + query = func + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')'; + break; + } + } + + if (windows) { + let overParts = []; + if (this.hasMetricColumn()) { + overParts.push('PARTITION BY ' + this.target.metricColumn); + } + overParts.push('ORDER BY ' + this.buildTimeColumn(false)); + + let over = overParts.join(' '); + let curr: string; + let prev: string; + switch (windows.type) { + case 'window': + switch (windows.params[0]) { + case 'increase': + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + break; + case 'rate': + let timeColumn = this.target.timeColumn; + if (aggregate) { + timeColumn = 'min(' + timeColumn + ')'; + } + + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; + break; + default: + query = windows.params[0] + '(' + query + ') OVER (' + over + ')'; + break; + } + break; + case 'moving_window': + query = windows.params[0] + '(' + query + ') OVER (' + over + ' ROWS ' + windows.params[1] + ' PRECEDING)'; + break; + } + } + + let alias = _.find(column, (g: any) => g.type === 'alias'); + if (alias) { + query += ' AS ' + this.quoteIdentifier(alias.params[0]); + } + + return query; + } + + buildWhereClause() { + let query = ''; + let conditions = _.map(this.target.where, (tag, index) => { + switch (tag.type) { + case 'macro': + return tag.name + '(' + this.target.timeColumn + ')'; + break; + case 'expression': + return tag.params.join(' '); + break; + } + }); + + if (conditions.length > 0) { + query = '\nWHERE\n ' + conditions.join(' AND\n '); + } + + return query; + } + + buildGroupClause() { + let query = ''; + let groupSection = ''; + + for (let i = 0; i < this.target.group.length; i++) { + let part = this.target.group[i]; + if (i > 0) { + groupSection += ', '; + } + if (part.type === 'time') { + groupSection += '1'; + } else { + groupSection += part.params[0]; + } + } + + if (groupSection.length) { + query = '\nGROUP BY ' + groupSection; + if (this.hasMetricColumn()) { + query += ',2'; + } + } + return query; + } + + buildQuery() { + let query = 'SELECT'; + + query += '\n ' + this.buildTimeColumn(); + if (this.hasMetricColumn()) { + query += ',\n ' + this.buildMetricColumn(); + } + query += this.buildValueColumns(); + + query += '\nFROM ' + this.target.table; + + query += this.buildWhereClause(); + query += this.buildGroupClause(); + + query += '\nORDER BY 1'; + + return query; + } +} diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 1e829a1175d..0c630947657 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -1,10 +1,102 @@ - -
    -
    - - -
    -
    + + +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + + + + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + + +
    + +
    + +
    + +
    +
    +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + + + + +
    + +
    + +
    + +
    +
    +
    +
    + +
    diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 1de1fb768ad..1c911368ed8 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -1,12 +1,10 @@ import _ from 'lodash'; +import appEvents from 'app/core/app_events'; +import { MysqlMetaQuery } from './meta_query'; import { QueryCtrl } from 'app/plugins/sdk'; - -export interface MysqlQuery { - refId: string; - format: string; - alias: string; - rawSql: string; -} +import { SqlPart } from 'app/core/components/sql_part/sql_part'; +import MysqlQuery from './mysql_query'; +import sqlPart from './sql_part'; export interface QueryMeta { sql: string; @@ -26,17 +24,31 @@ export class MysqlQueryCtrl extends QueryCtrl { showLastQuerySQL: boolean; formats: any[]; - target: MysqlQuery; lastQueryMeta: QueryMeta; lastQueryError: string; showHelp: boolean; + queryModel: MysqlQuery; + metaBuilder: MysqlMetaQuery; + tableSegment: any; + whereAdd: any; + timeColumnSegment: any; + metricColumnSegment: any; + selectMenu: any[]; + selectParts: SqlPart[][]; + groupParts: SqlPart[]; + whereParts: SqlPart[]; + groupAdd: any; + /** @ngInject **/ - constructor($scope, $injector) { + constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); - this.target.format = this.target.format || 'time_series'; - this.target.alias = ''; + this.target = this.target; + this.queryModel = new MysqlQuery(this.target, templateSrv, this.panel.scopedVars); + this.metaBuilder = new MysqlMetaQuery(this.target, this.queryModel); + this.updateProjection(); + this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; if (!this.target.rawSql) { @@ -44,15 +56,199 @@ export class MysqlQueryCtrl extends QueryCtrl { if (this.panelCtrl.panel.type === 'table') { this.target.format = 'table'; this.target.rawSql = 'SELECT 1'; + this.target.rawQuery = true; } else { this.target.rawSql = defaultQuery; + this.datasource.metricFindQuery(this.metaBuilder.findMetricTable()).then(result => { + if (result.length > 0) { + this.target.table = result[0].text; + let segment = this.uiSegmentSrv.newSegment(this.target.table); + this.tableSegment.html = segment.html; + this.tableSegment.value = segment.value; + + this.target.timeColumn = result[1].text; + segment = this.uiSegmentSrv.newSegment(this.target.timeColumn); + this.timeColumnSegment.html = segment.html; + this.timeColumnSegment.value = segment.value; + + this.target.timeColumnType = 'timestamp'; + this.target.select = [[{ type: 'column', params: [result[2].text] }]]; + this.updateProjection(); + this.panelCtrl.refresh(); + } + }); } } + if (!this.target.table) { + this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true }); + } else { + this.tableSegment = uiSegmentSrv.newSegment(this.target.table); + } + + this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); + this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); + + this.buildSelectMenu(); + this.whereAdd = this.uiSegmentSrv.newPlusButton(); + this.groupAdd = this.uiSegmentSrv.newPlusButton(); + this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } + updateProjection() { + this.selectParts = _.map(this.target.select, function(parts: any) { + return _.map(parts, sqlPart.create).filter(n => n); + }); + this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n); + this.groupParts = _.map(this.target.group, sqlPart.create).filter(n => n); + } + + updatePersistedParts() { + this.target.select = _.map(this.selectParts, function(selectParts) { + return _.map(selectParts, function(part: any) { + return { type: part.def.type, datatype: part.datatype, params: part.params }; + }); + }); + this.target.where = _.map(this.whereParts, function(part: any) { + return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params }; + }); + this.target.group = _.map(this.groupParts, function(part: any) { + return { type: part.def.type, datatype: part.datatype, params: part.params }; + }); + } + + buildSelectMenu() { + this.selectMenu = []; + let aggregates = { + text: 'Aggregate Functions', + value: 'aggregate', + submenu: [ + { text: 'Average', value: 'avg' }, + { text: 'Count', value: 'count' }, + { text: 'Maximum', value: 'max' }, + { text: 'Minimum', value: 'min' }, + { text: 'Sum', value: 'sum' }, + { text: 'Standard deviation', value: 'stddev' }, + { text: 'Variance', value: 'variance' }, + ], + }; + + this.selectMenu.push(aggregates); + this.selectMenu.push({ text: 'Alias', value: 'alias' }); + this.selectMenu.push({ text: 'Column', value: 'column' }); + } + + toggleEditorMode() { + if (this.target.rawQuery) { + appEvents.emit('confirm-modal', { + title: 'Warning', + text2: 'Switching to query builder may overwrite your raw SQL.', + icon: 'fa-exclamation', + yesText: 'Switch', + onConfirm: () => { + this.target.rawQuery = !this.target.rawQuery; + }, + }); + } else { + this.target.rawQuery = !this.target.rawQuery; + } + } + + resetPlusButton(button) { + let plusButton = this.uiSegmentSrv.newPlusButton(); + button.html = plusButton.html; + button.value = plusButton.value; + } + + getTableSegments() { + return this.datasource + .metricFindQuery(this.metaBuilder.buildTableQuery()) + .then(this.transformToSegments({})) + .catch(this.handleQueryError.bind(this)); + } + + tableChanged() { + this.target.table = this.tableSegment.value; + this.target.where = []; + this.target.group = []; + this.updateProjection(); + + let segment = this.uiSegmentSrv.newSegment('none'); + this.metricColumnSegment.html = segment.html; + this.metricColumnSegment.value = segment.value; + this.target.metricColumn = 'none'; + + let task1 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then(result => { + // check if time column is still valid + if (result.length > 0 && !_.find(result, (r: any) => r.text === this.target.timeColumn)) { + let segment = this.uiSegmentSrv.newSegment(result[0].text); + this.timeColumnSegment.html = segment.html; + this.timeColumnSegment.value = segment.value; + } + return this.timeColumnChanged(false); + }); + let task2 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('value')).then(result => { + if (result.length > 0) { + this.target.select = [[{ type: 'column', params: [result[0].text] }]]; + this.updateProjection(); + } + }); + + this.$q.all([task1, task2]).then(() => { + this.panelCtrl.refresh(); + }); + } + + getTimeColumnSegments() { + return this.datasource + .metricFindQuery(this.metaBuilder.buildColumnQuery('time')) + .then(this.transformToSegments({})) + .catch(this.handleQueryError.bind(this)); + } + + timeColumnChanged(refresh?: boolean) { + this.target.timeColumn = this.timeColumnSegment.value; + return this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn)).then(result => { + if (result.length === 1) { + if (this.target.timeColumnType !== result[0].text) { + this.target.timeColumnType = result[0].text; + } + let partModel; + if (this.queryModel.hasUnixEpochTimecolumn()) { + partModel = sqlPart.create({ type: 'macro', name: '$__unixEpochFilter', params: [] }); + } else { + partModel = sqlPart.create({ type: 'macro', name: '$__timeFilter', params: [] }); + } + + if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') { + // replace current macro + this.whereParts[0] = partModel; + } else { + this.whereParts.splice(0, 0, partModel); + } + } + + this.updatePersistedParts(); + if (refresh !== false) { + this.panelCtrl.refresh(); + } + }); + } + + getMetricColumnSegments() { + return this.datasource + .metricFindQuery(this.metaBuilder.buildColumnQuery('metric')) + .then(this.transformToSegments({ addNone: true })) + .catch(this.handleQueryError.bind(this)); + } + + metricColumnChanged() { + this.target.metricColumn = this.metricColumnSegment.value; + this.panelCtrl.refresh(); + } + onDataReceived(dataList) { this.lastQueryMeta = null; this.lastQueryError = null; @@ -72,4 +268,356 @@ export class MysqlQueryCtrl extends QueryCtrl { } } } + + transformToSegments(config) { + return results => { + let segments = _.map(results, segment => { + return this.uiSegmentSrv.newSegment({ + value: segment.text, + expandable: segment.expandable, + }); + }); + + if (config.addTemplateVars) { + for (let variable of this.templateSrv.variables) { + let value; + value = '$' + variable.name; + if (config.templateQuoter && variable.multi === false) { + value = config.templateQuoter(value); + } + + segments.unshift( + this.uiSegmentSrv.newSegment({ + type: 'template', + value: value, + expandable: true, + }) + ); + } + } + + if (config.addNone) { + segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'none', expandable: true })); + } + + return segments; + }; + } + + findAggregateIndex(selectParts) { + return _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile'); + } + + findWindowIndex(selectParts) { + return _.findIndex(selectParts, (p: any) => p.def.type === 'window' || p.def.type === 'moving_window'); + } + + addSelectPart(selectParts, item, subItem) { + let partType = item.value; + if (subItem && subItem.type) { + partType = subItem.type; + } + let partModel = sqlPart.create({ type: partType }); + if (subItem) { + partModel.params[0] = subItem.value; + } + let addAlias = false; + + switch (partType) { + case 'column': + let parts = _.map(selectParts, function(part: any) { + return sqlPart.create({ type: part.def.type, params: _.clone(part.params) }); + }); + this.selectParts.push(parts); + break; + case 'percentile': + case 'aggregate': + // add group by if no group by yet + if (this.target.group.length === 0) { + this.addGroup('time', '$__interval'); + } + let aggIndex = this.findAggregateIndex(selectParts); + if (aggIndex !== -1) { + // replace current aggregation + selectParts[aggIndex] = partModel; + } else { + selectParts.splice(1, 0, partModel); + } + if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) { + addAlias = true; + } + break; + case 'moving_window': + case 'window': + let windowIndex = this.findWindowIndex(selectParts); + if (windowIndex !== -1) { + // replace current window function + selectParts[windowIndex] = partModel; + } else { + let aggIndex = this.findAggregateIndex(selectParts); + if (aggIndex !== -1) { + selectParts.splice(aggIndex + 1, 0, partModel); + } else { + selectParts.splice(1, 0, partModel); + } + } + if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) { + addAlias = true; + } + break; + case 'alias': + addAlias = true; + break; + } + + if (addAlias) { + // set initial alias name to column name + partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace(/"/g, '')] }); + if (selectParts[selectParts.length - 1].def.type === 'alias') { + selectParts[selectParts.length - 1] = partModel; + } else { + selectParts.push(partModel); + } + } + + this.updatePersistedParts(); + this.panelCtrl.refresh(); + } + + removeSelectPart(selectParts, part) { + if (part.def.type === 'column') { + // remove all parts of column unless its last column + if (this.selectParts.length > 1) { + let modelsIndex = _.indexOf(this.selectParts, selectParts); + this.selectParts.splice(modelsIndex, 1); + } + } else { + let partIndex = _.indexOf(selectParts, part); + selectParts.splice(partIndex, 1); + } + + this.updatePersistedParts(); + } + + handleSelectPartEvent(selectParts, part, evt) { + switch (evt.name) { + case 'get-param-options': { + switch (part.def.type) { + // case 'aggregate': + // return this.datasource + // .metricFindQuery(this.metaBuilder.buildAggregateQuery()) + // .then(this.transformToSegments({})) + // .catch(this.handleQueryError.bind(this)); + case 'column': + return this.datasource + .metricFindQuery(this.metaBuilder.buildColumnQuery('value')) + .then(this.transformToSegments({})) + .catch(this.handleQueryError.bind(this)); + } + } + case 'part-param-changed': { + this.updatePersistedParts(); + this.panelCtrl.refresh(); + break; + } + case 'action': { + this.removeSelectPart(selectParts, part); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + handleGroupPartEvent(part, index, evt) { + switch (evt.name) { + case 'get-param-options': { + return this.datasource + .metricFindQuery(this.metaBuilder.buildColumnQuery()) + .then(this.transformToSegments({})) + .catch(this.handleQueryError.bind(this)); + } + case 'part-param-changed': { + this.updatePersistedParts(); + this.panelCtrl.refresh(); + break; + } + case 'action': { + this.removeGroup(part, index); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + addGroup(partType, value) { + let params = [value]; + if (partType === 'time') { + params = ['$__interval', 'none']; + } + let partModel = sqlPart.create({ type: partType, params: params }); + + if (partType === 'time') { + // put timeGroup at start + this.groupParts.splice(0, 0, partModel); + } else { + this.groupParts.push(partModel); + } + + // add aggregates when adding group by + for (let selectParts of this.selectParts) { + if (!selectParts.some(part => part.def.type === 'aggregate')) { + let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); + selectParts.splice(1, 0, aggregate); + if (!selectParts.some(part => part.def.type === 'alias')) { + let alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] }); + selectParts.push(alias); + } + } + } + + this.updatePersistedParts(); + } + + removeGroup(part, index) { + if (part.def.type === 'time') { + // remove aggregations + this.selectParts = _.map(this.selectParts, (s: any) => { + return _.filter(s, (part: any) => { + if (part.def.type === 'aggregate' || part.def.type === 'percentile') { + return false; + } + return true; + }); + }); + } + + this.groupParts.splice(index, 1); + this.updatePersistedParts(); + } + + handleWherePartEvent(whereParts, part, evt, index) { + switch (evt.name) { + case 'get-param-options': { + switch (evt.param.name) { + case 'left': + return this.datasource + .metricFindQuery(this.metaBuilder.buildColumnQuery()) + .then(this.transformToSegments({})) + .catch(this.handleQueryError.bind(this)); + case 'right': + if (['int4', 'int8', 'float4', 'float8', 'timestamp', 'timestamptz'].indexOf(part.datatype) > -1) { + // don't do value lookups for numerical fields + return this.$q.when([]); + } else { + return this.datasource + .metricFindQuery(this.metaBuilder.buildValueQuery(part.params[0])) + .then( + this.transformToSegments({ + addTemplateVars: true, + templateQuoter: (v: string) => { + return this.queryModel.quoteLiteral(v); + }, + }) + ) + .catch(this.handleQueryError.bind(this)); + } + case 'op': + return this.$q.when(this.uiSegmentSrv.newOperators(this.metaBuilder.getOperators(part.datatype))); + default: + return this.$q.when([]); + } + } + case 'part-param-changed': { + this.updatePersistedParts(); + this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(part.params[0])).then((d: any) => { + if (d.length === 1) { + part.datatype = d[0].text; + } + }); + this.panelCtrl.refresh(); + break; + } + case 'action': { + // remove element + whereParts.splice(index, 1); + this.updatePersistedParts(); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + getWhereOptions() { + var options = []; + if (this.queryModel.hasUnixEpochTimecolumn()) { + options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__unixEpochFilter' })); + } else { + options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__timeFilter' })); + } + options.push(this.uiSegmentSrv.newSegment({ type: 'expression', value: 'Expression' })); + return this.$q.when(options); + } + + addWhereAction(part, index) { + switch (this.whereAdd.type) { + case 'macro': { + let partModel = sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] }); + if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') { + // replace current macro + this.whereParts[0] = partModel; + } else { + this.whereParts.splice(0, 0, partModel); + } + break; + } + default: { + this.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); + } + } + + this.updatePersistedParts(); + this.resetPlusButton(this.whereAdd); + this.panelCtrl.refresh(); + } + + getGroupOptions() { + return this.datasource + .metricFindQuery(this.metaBuilder.buildColumnQuery('group')) + .then(tags => { + var options = []; + if (!this.queryModel.hasTimeGroup()) { + options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' })); + } + for (let tag of tags) { + options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text })); + } + return options; + }) + .catch(this.handleQueryError.bind(this)); + } + + addGroupAction() { + switch (this.groupAdd.value) { + default: { + this.addGroup(this.groupAdd.type, this.groupAdd.value); + } + } + + this.resetPlusButton(this.groupAdd); + this.panelCtrl.refresh(); + } + + handleQueryError(err) { + this.error = err.message || 'Failed to issue metric query'; + return []; + } } diff --git a/public/app/plugins/datasource/mysql/sql_part.ts b/public/app/plugins/datasource/mysql/sql_part.ts new file mode 100644 index 00000000000..25cdd09baa6 --- /dev/null +++ b/public/app/plugins/datasource/mysql/sql_part.ts @@ -0,0 +1,86 @@ +import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part'; + +let index = []; + +function createPart(part): any { + let def = index[part.type]; + if (!def) { + return null; + } + + return new SqlPart(part, def); +} + +function register(options: any) { + index[options.type] = new SqlPartDef(options); +} + +register({ + type: 'column', + style: 'label', + params: [{ type: 'column', dynamicLookup: true }], + defaultParams: ['value'], +}); + +register({ + type: 'expression', + style: 'expression', + label: 'Expr:', + params: [ + { name: 'left', type: 'string', dynamicLookup: true }, + { name: 'op', type: 'string', dynamicLookup: true }, + { name: 'right', type: 'string', dynamicLookup: true }, + ], + defaultParams: ['value', '=', 'value'], +}); + +register({ + type: 'macro', + style: 'label', + label: 'Macro:', + params: [], + defaultParams: [], +}); + +register({ + type: 'aggregate', + style: 'label', + params: [ + { + name: 'name', + type: 'string', + options: ['avg', 'count', 'min', 'max', 'sum', 'stddev', 'variance'], + }, + ], + defaultParams: ['avg'], +}); + +register({ + type: 'alias', + style: 'label', + params: [{ name: 'name', type: 'string', quote: 'double' }], + defaultParams: ['alias'], +}); + +register({ + type: 'time', + style: 'function', + label: 'time', + params: [ + { + name: 'interval', + type: 'interval', + options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + { + name: 'fill', + type: 'string', + options: ['none', 'NULL', 'previous', '0'], + }, + ], + defaultParams: ['$__interval', 'none'], +}); + +export default { + create: createPart, +}; From 4f91087d9a458b21f791a97674451416e5007839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 07:15:07 +0200 Subject: [PATCH 0057/2611] docs: minor updates, more work to do --- .../authentication/ldap-sync-grafana-ee.md | 13 ----------- docs/sources/administration/permissions.md | 2 -- .../anonymous-auth.md => auth/anonymous.md} | 2 +- .../authentication => auth}/auth-proxy.md | 0 .../authentication => auth}/index.md | 4 ++-- .../authentication => auth}/ldap.md | 0 .../authentication => auth}/oauth.md | 0 .../authentication => auth}/overview.md | 22 ++++++++++++++----- 8 files changed, 19 insertions(+), 24 deletions(-) delete mode 100644 docs/sources/administration/authentication/ldap-sync-grafana-ee.md rename docs/sources/{administration/authentication/anonymous-auth.md => auth/anonymous.md} (96%) rename docs/sources/{administration/authentication => auth}/auth-proxy.md (100%) rename docs/sources/{administration/authentication => auth}/index.md (91%) rename docs/sources/{administration/authentication => auth}/ldap.md (100%) rename docs/sources/{administration/authentication => auth}/oauth.md (100%) rename docs/sources/{administration/authentication => auth}/overview.md (52%) diff --git a/docs/sources/administration/authentication/ldap-sync-grafana-ee.md b/docs/sources/administration/authentication/ldap-sync-grafana-ee.md deleted file mode 100644 index c60b21b0320..00000000000 --- a/docs/sources/administration/authentication/ldap-sync-grafana-ee.md +++ /dev/null @@ -1,13 +0,0 @@ -+++ -title = "LDAP Sync with Grafana EE" -description = "LDAP Sync with Grafana EE Guide " -keywords = ["grafana", "configuration", "documentation", "ldap", "enterprise"] -type = "docs" -[menu.docs] -name = "LDAP Sync with Grafana EE" -identifier = "ldap-sync" -parent = "authentication" -weight = 2 -+++ - -# LDAP Sync with Grafana EE \ No newline at end of file diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md index e7b84a417c0..1d1a70607c8 100644 --- a/docs/sources/administration/permissions.md +++ b/docs/sources/administration/permissions.md @@ -52,8 +52,6 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se ### Dashboard & Folder Permissions -> Introduced in Grafana v5.0 - {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} For dashboards and dashboard folders there is a **Permissions** page that make it possible to diff --git a/docs/sources/administration/authentication/anonymous-auth.md b/docs/sources/auth/anonymous.md similarity index 96% rename from docs/sources/administration/authentication/anonymous-auth.md rename to docs/sources/auth/anonymous.md index f2cde75cacb..39d1059e92e 100644 --- a/docs/sources/administration/authentication/anonymous-auth.md +++ b/docs/sources/auth/anonymous.md @@ -4,7 +4,7 @@ description = "Anonymous authentication " keywords = ["grafana", "configuration", "documentation", "anonymous"] type = "docs" [menu.docs] -name = "Anonymous Auth" +name = "Anonymous" identifier = "anonymous-auth" parent = "authentication" weight = 4 diff --git a/docs/sources/administration/authentication/auth-proxy.md b/docs/sources/auth/auth-proxy.md similarity index 100% rename from docs/sources/administration/authentication/auth-proxy.md rename to docs/sources/auth/auth-proxy.md diff --git a/docs/sources/administration/authentication/index.md b/docs/sources/auth/index.md similarity index 91% rename from docs/sources/administration/authentication/index.md rename to docs/sources/auth/index.md index f9bc9e5f13c..455c361369a 100644 --- a/docs/sources/administration/authentication/index.md +++ b/docs/sources/auth/index.md @@ -6,5 +6,5 @@ type = "docs" name = "Authentication" identifier = "authentication" parent = "admin" -weight = 1 -+++ \ No newline at end of file +weight = 3 ++++ diff --git a/docs/sources/administration/authentication/ldap.md b/docs/sources/auth/ldap.md similarity index 100% rename from docs/sources/administration/authentication/ldap.md rename to docs/sources/auth/ldap.md diff --git a/docs/sources/administration/authentication/oauth.md b/docs/sources/auth/oauth.md similarity index 100% rename from docs/sources/administration/authentication/oauth.md rename to docs/sources/auth/oauth.md diff --git a/docs/sources/administration/authentication/overview.md b/docs/sources/auth/overview.md similarity index 52% rename from docs/sources/administration/authentication/overview.md rename to docs/sources/auth/overview.md index e7daf581abb..03a7a0e9fe4 100644 --- a/docs/sources/administration/authentication/overview.md +++ b/docs/sources/auth/overview.md @@ -9,20 +9,30 @@ parent = "authentication" weight = 1 +++ -## [auth] +# Authentication -### disable_login_form +Grafana provides many ways to authenticate users. By default it will use local users & passwords stored in the Grafana +database. + +## Settings + +Via the [server ini config file]({{< relref "installation/debian.md" >}}) you can setup many different authentication methods. Auth settings +are documented below. + +### [auth] + +#### disable_login_form Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false. -### disable_signout_menu +#### disable_signout_menu Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false.
    -## [auth.basic] -### enabled +### [auth.basic] +#### enabled When enabled is `true` (default) the http api will accept basic authentication. -
    \ No newline at end of file +
    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 0058/2611] 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 0059/2611] 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 0e10fdb4150fbaf8f845c6273791b62af4defb45 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 13:24:49 +0300 Subject: [PATCH 0060/2611] graph: legend as React component --- public/app/core/angular_wrappers.ts | 2 + public/app/plugins/panel/graph/Legend.tsx | 189 ++++++++++++++++++++++ public/app/plugins/panel/graph/graph.ts | 19 ++- 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/graph/Legend.tsx diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index a4439509f8e..b1105268543 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -6,6 +6,7 @@ import LoginBackground from './components/Login/LoginBackground'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import DashboardPermissions from './components/Permissions/DashboardPermissions'; +import { GraphLegend } from 'app/plugins/panel/graph/Legend'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -19,4 +20,5 @@ export function registerAngularDirectives() { ['tagOptions', { watchDepth: 'reference' }], ]); react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); + react2AngularDirective('graphLegendReact', GraphLegend, ['seriesList', 'className']); } diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx new file mode 100644 index 00000000000..a4bfbefd541 --- /dev/null +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -0,0 +1,189 @@ +import _ from 'lodash'; +import React from 'react'; + +const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; + +export interface GraphLegendProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; + alignAsTable?: boolean; + rightSide?: boolean; + sideWidth?: number; + sort?: 'min' | 'max' | 'avg' | 'current' | 'total'; + sortDesc?: boolean; + className?: string; +} + +export interface GraphLegendState {} + +export class GraphLegend extends React.PureComponent { + sortLegend() { + let seriesList = this.props.seriesList || []; + if (this.props.sort) { + seriesList = _.sortBy(seriesList, function(series) { + let sort = series.stats[this.props.sort]; + if (sort === null) { + sort = -Infinity; + } + return sort; + }); + if (this.props.sortDesc) { + seriesList = seriesList.reverse(); + } + } + return seriesList; + } + + render() { + const { className = '', hiddenSeries } = this.props; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + const seriesList = this.sortLegend(); + return ( +
    +
    +
    + {this.props.alignAsTable ? ( + + ) : ( + seriesList.map((series, i) => ( + + )) + )} +
    +
    +
    + ); + } +} + +interface LegendTableProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendTable extends React.PureComponent { + render() { + const seriesList = this.props.seriesList; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + const headerStyle: React.CSSProperties = { + textAlign: 'left', + }; + + return ( + + + + {LEGEND_STATS.map( + statName => seriesValuesProps[statName] && + )} + + {seriesList.map((series, i) => ( + + ))} + + ); + } +} + +interface LegendTableHeaderProps { + statName: string; + sortDesc?: boolean; +} + +function LegendTableHeader(props: LegendTableHeaderProps) { + return ( + + {props.statName} + + + ); +} + +interface LegendSeriesItemProps { + series: any; + index: number; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendSeriesItem extends React.Component { + constructor(props) { + super(props); + } + + render() { + const { series, index, hiddenSeries } = this.props; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; + return ( +
    +
    + +
    + + {series.aliasEscaped} + + {valueItems} +
    + ); + } +} + +function LegendValue(props) { + const value = props.value; + const valueName = props.valueName; + return
    {value}
    ; +} + +function renderLegendValues(props: LegendSeriesItemProps, series) { + const legendValueItems = []; + for (const valueName of LEGEND_STATS) { + if (props[valueName]) { + const valueFormatted = series.formatValue(series.stats[valueName]); + legendValueItems.push(); + } + } + return legendValueItems; +} + +function getOptionSeriesCSSClasses(series, hiddenSeries) { + const classes = []; + if (series.yaxis === 2) { + classes.push('graph-legend-series--right-y'); + } + if (hiddenSeries[series.alias]) { + classes.push('graph-legend-series-hidden'); + } + return classes.join(' '); +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 7f9fa0e1693..37841313c82 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -20,6 +20,9 @@ import { EventManager } from 'app/features/annotations/all'; import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { GraphLegend, GraphLegendProps } from './Legend'; import { GraphCtrl } from './module'; @@ -82,7 +85,21 @@ class GraphElement { const graphHeight = this.elem.height(); updateLegendValues(this.data, this.panel, graphHeight); - this.ctrl.events.emit('render-legend'); + // this.ctrl.events.emit('render-legend'); + const { values, min, max, avg, current, total } = this.panel.legend; + const { alignAsTable, rightSide, sideWidth } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth }; + const valueOptions = { values, min, max, avg, current, total }; + const legendProps: GraphLegendProps = { + seriesList: this.data, + hiddenSeries: this.ctrl.hiddenSeries, + ...legendOptions, + ...valueOptions, + }; + const legendReactElem = React.createElement(GraphLegend, legendProps); + const legendElem = this.elem.parent().find('.graph-legend'); + ReactDOM.render(legendReactElem, legendElem[0]); + this.onLegendRenderingComplete(); } onGraphHover(evt) { From 329f39e4d796a255057ee9e768474fcdd1bbea18 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 16:34:22 +0300 Subject: [PATCH 0061/2611] graph: make table markup corresponding to standards --- public/app/plugins/panel/graph/Legend.tsx | 186 +++++++++++++--------- 1 file changed, 110 insertions(+), 76 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index a4bfbefd541..ba11ba988f2 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -41,90 +41,39 @@ export class GraphLegend extends React.PureComponent -
    -
    - {this.props.alignAsTable ? ( - - ) : ( - seriesList.map((series, i) => ( - - )) - )} -
    +
    +
    + {this.props.alignAsTable ? ( + + ) : ( + seriesList.map((series, i) => ( + + )) + )}
    ); } } -interface LegendTableProps { - seriesList: any[]; - hiddenSeries: any; - values?: boolean; - min?: boolean; - max?: boolean; - avg?: boolean; - current?: boolean; - total?: boolean; -} - -class LegendTable extends React.PureComponent { - render() { - const seriesList = this.props.seriesList; - const { values, min, max, avg, current, total } = this.props; - const seriesValuesProps = { values, min, max, avg, current, total }; - const headerStyle: React.CSSProperties = { - textAlign: 'left', - }; - - return ( - - - - {LEGEND_STATS.map( - statName => seriesValuesProps[statName] && - )} - - {seriesList.map((series, i) => ( - - ))} - - ); - } -} - -interface LegendTableHeaderProps { - statName: string; - sortDesc?: boolean; -} - -function LegendTableHeader(props: LegendTableHeaderProps) { - return ( - - {props.statName} - - - ); -} - interface LegendSeriesItemProps { series: any; index: number; @@ -163,20 +112,105 @@ class LegendSeriesItem extends React.Component { function LegendValue(props) { const value = props.value; const valueName = props.valueName; + if (props.asTable) { + return {value}; + } return
    {value}
    ; } -function renderLegendValues(props: LegendSeriesItemProps, series) { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { const valueFormatted = series.formatValue(series.stats[valueName]); - legendValueItems.push(); + legendValueItems.push( + + ); } } return legendValueItems; } +interface LegendTableProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendTable extends React.PureComponent { + render() { + const seriesList = this.props.seriesList; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + + return ( + + + + + {seriesList.map((series, i) => ( + + ))} + +
    + {LEGEND_STATS.map( + statName => seriesValuesProps[statName] && + )} +
    + ); + } +} + +class LegendSeriesItemAsTable extends React.Component { + constructor(props) { + super(props); + } + + render() { + const { series, index, hiddenSeries } = this.props; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series, true) : []; + return ( + + +
    + +
    + + {series.aliasEscaped} + + + {valueItems} + + ); + } +} + +interface LegendTableHeaderProps { + statName: string; + sortDesc?: boolean; +} + +function LegendTableHeader(props: LegendTableHeaderProps) { + return ( + + {props.statName} + + + ); +} + function getOptionSeriesCSSClasses(series, hiddenSeries) { const classes = []; if (series.yaxis === 2) { From 390472aa99ce631dc7f9cf37cb2c8dfea643a827 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 15:40:58 +0200 Subject: [PATCH 0062/2611] render query from query builder --- .../app/plugins/datasource/mysql/datasource.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index e41417e155c..612ab9adb6c 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -1,16 +1,19 @@ import _ from 'lodash'; import ResponseParser from './response_parser'; +import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query'; export class MysqlDatasource { id: any; name: any; responseParser: ResponseParser; + queryModel: MysqlQuery; /** @ngInject **/ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(this.$q); + this.queryModel = new MysqlQuery({}); } interpolateVariable(value, variable) { @@ -37,16 +40,18 @@ export class MysqlDatasource { } query(options) { - const queries = _.filter(options.targets, item => { - return item.hide !== true; - }).map(item => { + const queries = _.filter(options.targets, target => { + return target.hide !== true; + }).map(target => { + let queryModel = new MysqlQuery(target, this.templateSrv, options.scopedVars); + return { - refId: item.refId, + refId: target.refId, intervalMs: options.intervalMs, maxDataPoints: options.maxDataPoints, datasourceId: this.id, - rawSql: this.templateSrv.replace(item.rawSql, options.scopedVars, this.interpolateVariable), - format: item.format, + rawSql: queryModel.render(this.interpolateVariable), + format: target.format, }; }); From 8d73f53e973fbf0cd014c8d0965bd31f9f086140 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 16:27:48 +0200 Subject: [PATCH 0063/2611] use quoting functions from MysqlQuery in datasource --- public/app/plugins/datasource/mysql/datasource.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 612ab9adb6c..d5bd2594f24 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -19,7 +19,7 @@ export class MysqlDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value.replace(/'/g, `''`) + "'"; + return this.queryModel.quoteLiteral(value); } else { return value; } @@ -29,12 +29,8 @@ export class MysqlDatasource { return value; } - const quotedValues = _.map(value, function(val) { - if (typeof value === 'number') { - return value; - } - - return "'" + val.replace(/'/g, `''`) + "'"; + const quotedValues = _.map(value, v => { + return this.queryModel.quoteLiteral(v); }); return quotedValues.join(','); } From 60146109ab09d101879c9a9b7832b7e1ae14c2ba Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 17:27:57 +0300 Subject: [PATCH 0064/2611] graph legend: minor refactor --- public/app/plugins/panel/graph/Legend.tsx | 51 ++++++++++++++--------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index ba11ba988f2..e1748ea3655 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -41,19 +41,23 @@ export class GraphLegend extends React.PureComponent +
    {this.props.alignAsTable ? ( @@ -97,18 +101,32 @@ class LegendSeriesItem extends React.Component { const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; return (
    -
    - -
    - - {series.aliasEscaped} - + {valueItems}
    ); } } +interface LegendSeriesLabelProps { + label: string; + color: string; +} + +function LegendSeriesLabel(props: LegendSeriesLabelProps) { + const { label, color } = props; + return ( +
    +
    + +
    + + {label} + +
    + ); +} + function LegendValue(props) { const value = props.value; const valueName = props.valueName; @@ -118,7 +136,7 @@ function LegendValue(props) { return
    {value}
    ; } -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false): React.ReactElement[] { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { @@ -184,12 +202,7 @@ class LegendSeriesItemAsTable extends React.Component { return ( -
    - -
    - - {series.aliasEscaped} - + {valueItems} From cd708d6cb2100f8c76c967e9432fda36f5a9f289 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 16:52:26 +0200 Subject: [PATCH 0065/2611] ignore information_schema tables --- public/app/plugins/datasource/mysql/meta_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/mysql/meta_query.ts b/public/app/plugins/datasource/mysql/meta_query.ts index 94e3e8fc3d6..d5383e85ff7 100644 --- a/public/app/plugins/datasource/mysql/meta_query.ts +++ b/public/app/plugins/datasource/mysql/meta_query.ts @@ -86,7 +86,7 @@ export class MysqlMetaQuery { } buildTableQuery() { - return 'SELECT table_name FROM information_schema.tables ORDER BY table_name'; + return "SELECT table_name FROM information_schema.tables WHERE table_schema <> 'information_schema' ORDER BY table_name"; } buildColumnQuery(type?: string) { From bcfb841cb48178b7eb9fc9908e304d47a313a59a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 18:24:09 +0200 Subject: [PATCH 0066/2611] pass timerange in meta data queries --- public/app/plugins/datasource/mysql/datasource.ts | 5 ++++- .../datasource/mysql/specs/datasource.test.ts | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index d5bd2594f24..7b112ef4336 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -9,7 +9,7 @@ export class MysqlDatasource { queryModel: MysqlQuery; /** @ngInject **/ - constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { + constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(this.$q); @@ -108,8 +108,11 @@ export class MysqlDatasource { format: 'table', }; + const range = this.timeSrv.timeRange(); const data = { queries: [interpolatedQuery], + from: range.from.valueOf().toString(), + to: range.to.valueOf().toString(), }; if (optionalOptions && optionalOptions.range && optionalOptions.range.from) { diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index e75ba5e32ee..163f3afe671 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -9,12 +9,23 @@ describe('MySQLDatasource', function() { replace: jest.fn(text => text), }; + const raw = { + from: moment.utc('2018-04-25 10:00'), + to: moment.utc('2018-04-25 11:00'), + }; const ctx = { backendSrv, + timeSrvMock: { + timeRange: () => ({ + from: raw.from, + to: raw.to, + raw: raw, + }), + }, }; beforeEach(() => { - ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv); + ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv, ctx.timeSrvMock); }); describe('When performing annotationQuery', function() { 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 0067/2611] 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 0068/2611] 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 0069/2611] 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 0070/2611] 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 e67d3df14c5ad323a53652abeb7121e1904688c1 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sat, 21 Jul 2018 23:31:47 +0200 Subject: [PATCH 0071/2611] Fix array display from url --- .../specs/variable_srv_init.test.ts | 38 +++++++++++++++---- .../app/features/templating/variable_srv.ts | 4 +- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index 978ad824d69..ab8b20deca2 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -76,8 +76,8 @@ describe('VariableSrv init', function(this: any) { { name: 'apps', type: type, - current: { text: 'test', value: 'test' }, - options: [{ text: 'test', value: 'test' }], + current: { text: 'Test', value: 'test' }, + options: [{ text: 'Test', value: 'test' }], }, ]; scenario.urlParams['var-apps'] = 'new'; @@ -160,11 +160,11 @@ describe('VariableSrv init', function(this: any) { name: 'apps', type: 'query', multi: true, - current: { text: 'val1', value: 'val1' }, + current: { text: 'Val1', value: 'val1' }, options: [ - { text: 'val1', value: 'val1' }, - { text: 'val2', value: 'val2' }, - { text: 'val3', value: 'val3', selected: true }, + { text: 'Val1', value: 'val1' }, + { text: 'Val2', value: 'val2' }, + { text: 'Val3', value: 'val3', selected: true }, ], }, ]; @@ -176,7 +176,7 @@ describe('VariableSrv init', function(this: any) { expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); expect(variable.current.value[1]).toBe('val1'); - expect(variable.current.text).toBe('val2 + val1'); + expect(variable.current.text).toBe('Val2 + Val1'); expect(variable.options[0].selected).toBe(true); expect(variable.options[1].selected).toBe(true); }); @@ -187,6 +187,30 @@ describe('VariableSrv init', function(this: any) { }); }); + describeInitScenario( + 'when template variable is present in url multiple times and variables have no text', + scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: 'query', + multi: true, + }, + ]; + scenario.urlParams['var-apps'] = ['val1', 'val2']; + }); + + it('should display concatenated values in text', () => { + const variable = ctx.variableSrv.variables[0]; + expect(variable.current.value.length).toBe(2); + expect(variable.current.value[0]).toBe('val1'); + expect(variable.current.value[1]).toBe('val2'); + expect(variable.current.text).toBe('val1 + val2'); + }); + } + ); + describeInitScenario('when template variable is present in url multiple times using key/values', scenario => { scenario.setup(() => { scenario.variables = [ diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 22f8a909440..0530135a5ef 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -236,8 +236,10 @@ export class VariableSrv { setOptionAsCurrent(variable, option) { variable.current = _.cloneDeep(option); - if (_.isArray(variable.current.text)) { + if (_.isArray(variable.current.text) && variable.current.text.length > 0) { variable.current.text = variable.current.text.join(' + '); + } else if (_.isArray(variable.current.value) && variable.current.value[0] !== '$__all') { + variable.current.text = variable.current.value.join(' + '); } this.selectOptionsForCurrentValue(variable); 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 0072/2611] 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 0073/2611] 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 0074/2611] 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 0075/2611] 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 0076/2611] 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 0077/2611] 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 0078/2611] 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 0079/2611] 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 0080/2611] 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 0081/2611] 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 e8a52117a5f55e05579d29c773ca1b2e83cd2d76 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 3 Sep 2018 16:54:52 +0300 Subject: [PATCH 0082/2611] graph legend: react component refactor --- public/app/plugins/panel/graph/Legend.tsx | 171 ++++++++++++---------- public/app/plugins/panel/graph/graph.ts | 5 +- 2 files changed, 97 insertions(+), 79 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index e1748ea3655..becb52d1aeb 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,32 +1,62 @@ import _ from 'lodash'; import React from 'react'; +import { TimeSeries } from 'app/core/core'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; -export interface GraphLegendProps { - seriesList: any[]; +interface LegendProps { + seriesList: TimeSeries[]; + optionalClass?: string; +} + +interface LegendDisplayProps { hiddenSeries: any; + hideEmpty?: boolean; + hideZero?: boolean; + alignAsTable?: boolean; + rightSide?: boolean; + sideWidth?: number; +} + +interface LegendValuesProps { values?: boolean; min?: boolean; max?: boolean; avg?: boolean; current?: boolean; total?: boolean; - alignAsTable?: boolean; - rightSide?: boolean; - sideWidth?: number; +} + +interface LegendSortProps { sort?: 'min' | 'max' | 'avg' | 'current' | 'total'; sortDesc?: boolean; - className?: string; } +export type GraphLegendProps = LegendProps & LegendDisplayProps & LegendValuesProps & LegendSortProps; + +const defaultGraphLegendProps: Partial = { + values: false, + min: false, + max: false, + avg: false, + current: false, + total: false, + alignAsTable: false, + rightSide: false, + sort: undefined, + sortDesc: false, + optionalClass: '', +}; + export interface GraphLegendState {} export class GraphLegend extends React.PureComponent { + static defaultProps = defaultGraphLegendProps; + sortLegend() { let seriesList = this.props.seriesList || []; if (this.props.sort) { - seriesList = _.sortBy(seriesList, function(series) { + seriesList = _.sortBy(seriesList, series => { let sort = series.stats[this.props.sort]; if (sort === null) { sort = -Infinity; @@ -41,11 +71,12 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); + const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; // Set min-width if side style and there is a value, otherwise remove the CSS property // Set width so it works with IE11 @@ -62,15 +93,7 @@ export class GraphLegend extends React.PureComponent ) : ( - seriesList.map((series, i) => ( - - )) + )}
    @@ -78,23 +101,24 @@ export class GraphLegend extends React.PureComponent { + render() { + const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + return seriesList.map((series, i) => ( + + )); + } } -class LegendSeriesItem extends React.Component { - constructor(props) { - super(props); - } +interface LegendSeriesProps { + series: TimeSeries; + index: number; +} +type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; + +class LegendSeriesItem extends React.PureComponent { render() { const { series, index, hiddenSeries } = this.props; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); @@ -113,21 +137,27 @@ interface LegendSeriesLabelProps { color: string; } -function LegendSeriesLabel(props: LegendSeriesLabelProps) { - const { label, color } = props; - return ( -
    -
    +class LegendSeriesLabel extends React.PureComponent { + render() { + const { label, color } = this.props; + return [ +
    -
    - +
    , + {label} - -
    - ); + , + ]; + } } -function LegendValue(props) { +interface LegendValueProps { + value: string; + valueName: string; + asTable?: boolean; +} + +function LegendValue(props: LegendValueProps) { const value = props.value; const valueName = props.valueName; if (props.asTable) { @@ -149,30 +179,21 @@ function renderLegendValues(props: LegendSeriesItemProps, series, asTable = fals return legendValueItems; } -interface LegendTableProps { - seriesList: any[]; - hiddenSeries: any; - values?: boolean; - min?: boolean; - max?: boolean; - avg?: boolean; - current?: boolean; - total?: boolean; -} - -class LegendTable extends React.PureComponent { +class LegendTable extends React.PureComponent> { render() { const seriesList = this.props.seriesList; - const { values, min, max, avg, current, total } = this.props; + const { values, min, max, avg, current, total, sort, sortDesc } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; - return ( {seriesList.map((series, i) => ( @@ -190,11 +211,21 @@ class LegendTable extends React.PureComponent { } } -class LegendSeriesItemAsTable extends React.Component { - constructor(props) { - super(props); - } +interface LegendTableHeaderProps { + statName: string; +} +function LegendTableHeader(props: LegendTableHeaderProps & LegendSortProps) { + const { statName, sort, sortDesc } = props; + return ( + + ); +} + +class LegendSeriesItemAsTable extends React.PureComponent { render() { const { series, index, hiddenSeries } = this.props; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); @@ -210,20 +241,6 @@ class LegendSeriesItemAsTable extends React.Component { } } -interface LegendTableHeaderProps { - statName: string; - sortDesc?: boolean; -} - -function LegendTableHeader(props: LegendTableHeaderProps) { - return ( - - ); -} - function getOptionSeriesCSSClasses(series, hiddenSeries) { const classes = []; if (series.yaxis === 2) { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 37841313c82..a1066295048 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -86,9 +86,10 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); + console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; - const { alignAsTable, rightSide, sideWidth } = this.panel.legend; - const legendOptions = { alignAsTable, rightSide, sideWidth }; + const { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero }; const valueOptions = { values, min, max, avg, current, total }; const legendProps: GraphLegendProps = { seriesList: this.data, From 0e007d573d61883889fba20c50fa490b8b46a3ed Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 4 Sep 2018 09:53:07 +0200 Subject: [PATCH 0083/2611] changed functions to arrowfunctions for only-arrow-functions rule --- public/app/features/admin/admin.ts | 2 +- .../app/features/admin/admin_edit_org_ctrl.ts | 20 +-- .../features/admin/admin_edit_user_ctrl.ts | 42 ++--- .../features/admin/admin_list_orgs_ctrl.ts | 12 +- .../annotations/specs/annotations_srv.test.ts | 2 +- .../specs/dashboard_import_ctrl.test.ts | 22 +-- .../specs/dashboard_migration.test.ts | 66 +++---- .../dashboard/specs/dashboard_model.test.ts | 134 +++++++------- .../dashboard/specs/history_srv.test.ts | 20 +-- .../features/dashboard/specs/repeat.test.ts | 76 ++++---- .../dashboard/specs/save_as_modal.test.ts | 4 +- .../specs/save_provisioned_modal.test.ts | 4 +- .../dashboard/specs/share_modal_ctrl.test.ts | 2 +- .../features/dashboard/specs/time_srv.test.ts | 32 ++-- public/app/features/org/org_details_ctrl.ts | 10 +- .../panellinks/specs/link_srv.test.ts | 14 +- .../plugins/specs/datasource_srv.test.ts | 2 +- .../templating/specs/adhoc_variable.test.ts | 10 +- .../templating/specs/template_srv.test.ts | 168 +++++++++--------- .../templating/specs/variable.test.ts | 24 +-- .../templating/specs/variable_srv.test.ts | 42 ++--- .../cloudwatch/specs/datasource.test.ts | 40 ++--- .../elasticsearch/specs/datasource.test.ts | 36 ++-- .../graphite/specs/datasource.test.ts | 54 +++--- .../datasource/graphite/specs/gfunc.test.ts | 50 +++--- .../datasource/graphite/specs/lexer.test.ts | 30 ++-- .../datasource/graphite/specs/parser.test.ts | 42 ++--- .../graphite/specs/query_ctrl.test.ts | 2 +- .../influxdb/specs/datasource.test.ts | 6 +- .../influxdb/specs/influx_query.test.ts | 68 +++---- .../influxdb/specs/influx_series.test.ts | 58 +++--- .../influxdb/specs/query_builder.test.ts | 38 ++-- .../datasource/mssql/specs/datasource.test.ts | 40 ++--- .../datasource/mysql/specs/datasource.test.ts | 36 ++-- .../postgres/specs/datasource.test.ts | 34 ++-- .../prometheus/specs/completer.test.ts | 2 +- .../prometheus/specs/datasource.test.ts | 12 +- .../specs/metric_find_query.test.ts | 2 +- .../panel/graph/specs/align_yaxes.test.ts | 2 +- .../panel/graph/specs/data_processor.test.ts | 2 +- .../plugins/panel/graph/specs/graph.test.ts | 38 ++-- .../panel/graph/specs/graph_tooltip.test.ts | 48 ++--- .../panel/graph/specs/histogram.test.ts | 2 +- .../graph/specs/threshold_manager.test.ts | 26 +-- .../panel/heatmap/specs/heatmap_ctrl.test.ts | 20 +-- .../panel/singlestat/specs/singlestat.test.ts | 158 ++++++++-------- .../singlestat/specs/singlestat_panel.test.ts | 2 +- .../panel/table/specs/renderer.test.ts | 6 +- .../panel/table/specs/transformers.test.ts | 16 +- public/test/specs/helpers.ts | 58 +++--- 50 files changed, 816 insertions(+), 820 deletions(-) diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index 383b50b5d25..00e98821779 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -12,7 +12,7 @@ class AdminSettingsCtrl { constructor($scope, backendSrv, navModelSrv) { this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings', 1); - backendSrv.get('/api/admin/settings').then(function(settings) { + backendSrv.get('/api/admin/settings').then(settings => { $scope.settings = settings; }); } diff --git a/public/app/features/admin/admin_edit_org_ctrl.ts b/public/app/features/admin/admin_edit_org_ctrl.ts index d1e201dbe58..ec3f8548023 100644 --- a/public/app/features/admin/admin_edit_org_ctrl.ts +++ b/public/app/features/admin/admin_edit_org_ctrl.ts @@ -3,7 +3,7 @@ import angular from 'angular'; export class AdminEditOrgCtrl { /** @ngInject */ constructor($scope, $routeParams, backendSrv, $location, navModelSrv) { - $scope.init = function() { + $scope.init = () => { $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1); if ($routeParams.id) { @@ -12,34 +12,34 @@ export class AdminEditOrgCtrl { } }; - $scope.getOrg = function(id) { - backendSrv.get('/api/orgs/' + id).then(function(org) { + $scope.getOrg = id => { + backendSrv.get('/api/orgs/' + id).then(org => { $scope.org = org; }); }; - $scope.getOrgUsers = function(id) { - backendSrv.get('/api/orgs/' + id + '/users').then(function(orgUsers) { + $scope.getOrgUsers = id => { + backendSrv.get('/api/orgs/' + id + '/users').then(orgUsers => { $scope.orgUsers = orgUsers; }); }; - $scope.update = function() { + $scope.update = () => { if (!$scope.orgDetailsForm.$valid) { return; } - backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(function() { + backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(() => { $location.path('/admin/orgs'); }); }; - $scope.updateOrgUser = function(orgUser) { + $scope.updateOrgUser = orgUser => { backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser); }; - $scope.removeOrgUser = function(orgUser) { - backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(function() { + $scope.removeOrgUser = orgUser => { + backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(() => { $scope.getOrgUsers($scope.org.id); }); }; diff --git a/public/app/features/admin/admin_edit_user_ctrl.ts b/public/app/features/admin/admin_edit_user_ctrl.ts index b84b690d44a..c34ccdc1cad 100644 --- a/public/app/features/admin/admin_edit_user_ctrl.ts +++ b/public/app/features/admin/admin_edit_user_ctrl.ts @@ -9,72 +9,72 @@ export class AdminEditUserCtrl { $scope.permissions = {}; $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1); - $scope.init = function() { + $scope.init = () => { if ($routeParams.id) { $scope.getUser($routeParams.id); $scope.getUserOrgs($routeParams.id); } }; - $scope.getUser = function(id) { - backendSrv.get('/api/users/' + id).then(function(user) { + $scope.getUser = id => { + backendSrv.get('/api/users/' + id).then(user => { $scope.user = user; $scope.user_id = id; $scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; }); }; - $scope.setPassword = function() { + $scope.setPassword = () => { if (!$scope.passwordForm.$valid) { return; } const payload = { password: $scope.password }; - backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() { + backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(() => { $location.path('/admin/users'); }); }; - $scope.updatePermissions = function() { + $scope.updatePermissions = () => { const payload = $scope.permissions; - backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() { + backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(() => { $location.path('/admin/users'); }); }; - $scope.create = function() { + $scope.create = () => { if (!$scope.userForm.$valid) { return; } - backendSrv.post('/api/admin/users', $scope.user).then(function() { + backendSrv.post('/api/admin/users', $scope.user).then(() => { $location.path('/admin/users'); }); }; - $scope.getUserOrgs = function(id) { - backendSrv.get('/api/users/' + id + '/orgs').then(function(orgs) { + $scope.getUserOrgs = id => { + backendSrv.get('/api/users/' + id + '/orgs').then(orgs => { $scope.orgs = orgs; }); }; - $scope.update = function() { + $scope.update = () => { if (!$scope.userForm.$valid) { return; } - backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(function() { + backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(() => { $location.path('/admin/users'); }); }; - $scope.updateOrgUser = function(orgUser) { - backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(function() {}); + $scope.updateOrgUser = orgUser => { + backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(() => {}); }; - $scope.removeOrgUser = function(orgUser) { - backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() { + $scope.removeOrgUser = orgUser => { + backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(() => { $scope.getUser($scope.user_id); $scope.getUserOrgs($scope.user_id); }); @@ -82,19 +82,19 @@ export class AdminEditUserCtrl { $scope.orgsSearchCache = []; - $scope.searchOrgs = function(queryStr, callback) { + $scope.searchOrgs = (queryStr, callback) => { if ($scope.orgsSearchCache.length > 0) { callback(_.map($scope.orgsSearchCache, 'name')); return; } - backendSrv.get('/api/orgs', { query: '' }).then(function(result) { + backendSrv.get('/api/orgs', { query: '' }).then(result => { $scope.orgsSearchCache = result; callback(_.map(result, 'name')); }); }; - $scope.addOrgUser = function() { + $scope.addOrgUser = () => { if (!$scope.addOrgForm.$valid) { return; } @@ -108,7 +108,7 @@ export class AdminEditUserCtrl { $scope.newOrg.loginOrEmail = $scope.user.login; - backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() { + backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(() => { $scope.getUser($scope.user_id); $scope.getUserOrgs($scope.user_id); }); diff --git a/public/app/features/admin/admin_list_orgs_ctrl.ts b/public/app/features/admin/admin_list_orgs_ctrl.ts index d6d1b9e7dda..0513752aa3e 100644 --- a/public/app/features/admin/admin_list_orgs_ctrl.ts +++ b/public/app/features/admin/admin_list_orgs_ctrl.ts @@ -3,26 +3,26 @@ import angular from 'angular'; export class AdminListOrgsCtrl { /** @ngInject */ constructor($scope, backendSrv, navModelSrv) { - $scope.init = function() { + $scope.init = () => { $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1); $scope.getOrgs(); }; - $scope.getOrgs = function() { - backendSrv.get('/api/orgs').then(function(orgs) { + $scope.getOrgs = () => { + backendSrv.get('/api/orgs').then(orgs => { $scope.orgs = orgs; }); }; - $scope.deleteOrg = function(org) { + $scope.deleteOrg = org => { $scope.appEvent('confirm-modal', { title: 'Delete', text: 'Do you want to delete organization ' + org.name + '?', text2: 'All dashboards for this organization will be removed!', icon: 'fa-trash', yesText: 'Delete', - onConfirm: function() { - backendSrv.delete('/api/orgs/' + org.id).then(function() { + onConfirm: () => { + backendSrv.delete('/api/orgs/' + org.id).then(() => { $scope.getOrgs(); }); }, diff --git a/public/app/features/annotations/specs/annotations_srv.test.ts b/public/app/features/annotations/specs/annotations_srv.test.ts index f262544da43..a00fc9b841d 100644 --- a/public/app/features/annotations/specs/annotations_srv.test.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -2,7 +2,7 @@ import '../annotations_srv'; import 'app/features/dashboard/time_srv'; import { AnnotationsSrv } from '../annotations_srv'; -describe('AnnotationsSrv', function() { +describe('AnnotationsSrv', () => { const $rootScope = { onAppEvent: jest.fn(), }; diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts index fca857184f1..bcde009cb3a 100644 --- a/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts @@ -1,7 +1,7 @@ import { DashboardImportCtrl } from '../dashboard_import_ctrl'; import config from '../../../core/config'; -describe('DashboardImportCtrl', function() { +describe('DashboardImportCtrl', () => { const ctx: any = {}; let navModelSrv; @@ -26,8 +26,8 @@ describe('DashboardImportCtrl', function() { ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}); }); - describe('when uploading json', function() { - beforeEach(function() { + describe('when uploading json', () => { + beforeEach(() => { config.datasources = { ds: { type: 'test-db', @@ -46,19 +46,19 @@ describe('DashboardImportCtrl', function() { }); }); - it('should build input model', function() { + it('should build input model', () => { expect(ctx.ctrl.inputs.length).toBe(1); expect(ctx.ctrl.inputs[0].name).toBe('ds'); expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); }); - it('should set inputValid to false', function() { + it('should set inputValid to false', () => { expect(ctx.ctrl.inputsValid).toBe(false); }); }); - describe('when specifying grafana.com url', function() { - beforeEach(function() { + describe('when specifying grafana.com url', () => { + beforeEach(() => { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock backendSrv.get = jest.fn(() => { @@ -69,13 +69,13 @@ describe('DashboardImportCtrl', function() { return ctx.ctrl.checkGnetDashboard(); }); - it('should call gnet api with correct dashboard id', function() { + it('should call gnet api with correct dashboard id', () => { expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); - describe('when specifying dashboard id', function() { - beforeEach(function() { + describe('when specifying dashboard id', () => { + beforeEach(() => { ctx.ctrl.gnetUrl = '2342'; // setup api mock backendSrv.get = jest.fn(() => { @@ -86,7 +86,7 @@ describe('DashboardImportCtrl', function() { return ctx.ctrl.checkGnetDashboard(); }); - it('should call gnet api with correct dashboard id', function() { + it('should call gnet api with correct dashboard id', () => { expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); diff --git a/public/app/features/dashboard/specs/dashboard_migration.test.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts index d07df0e7be2..5f693c9f6d9 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.test.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.test.ts @@ -6,14 +6,14 @@ import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); -describe('DashboardModel', function() { - describe('when creating dashboard with old schema', function() { +describe('DashboardModel', () => { + describe('when creating dashboard with old schema', () => { let model; let graph; let singlestat; let table; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ services: { filter: { time: { from: 'now-1d', to: 'now' }, list: [{}] }, @@ -65,52 +65,52 @@ describe('DashboardModel', function() { table = model.panels[2]; }); - it('should have title', function() { + it('should have title', () => { expect(model.title).toBe('No Title'); }); - it('should have panel id', function() { + it('should have panel id', () => { expect(graph.id).toBe(1); }); - it('should move time and filtering list', function() { + it('should move time and filtering list', () => { expect(model.time.from).toBe('now-1d'); expect(model.templating.list[0].allFormat).toBe('glob'); }); - it('graphite panel should change name too graph', function() { + it('graphite panel should change name too graph', () => { expect(graph.type).toBe('graph'); }); - it('single stat panel should have two thresholds', function() { + it('single stat panel should have two thresholds', () => { expect(singlestat.thresholds).toBe('20,30'); }); - it('queries without refId should get it', function() { + it('queries without refId should get it', () => { expect(graph.targets[1].refId).toBe('B'); }); - it('update legend setting', function() { + it('update legend setting', () => { expect(graph.legend.show).toBe(true); }); - it('move aliasYAxis to series override', function() { + it('move aliasYAxis to series override', () => { expect(graph.seriesOverrides[0].alias).toBe('test'); expect(graph.seriesOverrides[0].yaxis).toBe(2); }); - it('should move pulldowns to new schema', function() { + it('should move pulldowns to new schema', () => { expect(model.annotations.list[1].name).toBe('old'); }); - it('table panel should only have two thresholds values', function() { + it('table panel should only have two thresholds values', () => { expect(table.styles[0].thresholds[0]).toBe('20'); expect(table.styles[0].thresholds[1]).toBe('30'); expect(table.styles[1].thresholds[0]).toBe('200'); expect(table.styles[1].thresholds[1]).toBe('300'); }); - it('graph grid to yaxes options', function() { + it('graph grid to yaxes options', () => { expect(graph.yaxes[0].min).toBe(1); expect(graph.yaxes[0].max).toBe(10); expect(graph.yaxes[0].format).toBe('kbyte'); @@ -126,11 +126,11 @@ describe('DashboardModel', function() { expect(graph.y_formats).toBe(undefined); }); - it('dashboard schema version should be set to latest', function() { + it('dashboard schema version should be set to latest', () => { expect(model.schemaVersion).toBe(16); }); - it('graph thresholds should be migrated', function() { + it('graph thresholds should be migrated', () => { expect(graph.thresholds.length).toBe(2); expect(graph.thresholds[0].op).toBe('gt'); expect(graph.thresholds[0].value).toBe(200); @@ -140,16 +140,16 @@ describe('DashboardModel', function() { }); }); - describe('when migrating to the grid layout', function() { + describe('when migrating to the grid layout', () => { let model; - beforeEach(function() { + beforeEach(() => { model = { rows: [], }; }); - it('should create proper grid', function() { + it('should create proper grid', () => { model.rows = [createRow({ collapse: false, height: 8 }, [[6], [6]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -158,7 +158,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add special "row" panel if row is collapsed', function() { + it('should add special "row" panel if row is collapsed', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -171,7 +171,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add special "row" panel if row has visible title', function() { + it('should add special "row" panel if row has visible title', () => { model.rows = [ createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]]), @@ -189,7 +189,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should not add "row" panel if row has not visible title or not collapsed', function() { + it('should not add "row" panel if row has not visible title or not collapsed', () => { model.rows = [ createRow({ collapse: true, height: 8 }, [[12]]), createRow({ height: 8 }, [[12]]), @@ -212,7 +212,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add all rows if even one collapsed or titled row is present', function() { + it('should add all rows if even one collapsed or titled row is present', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -225,7 +225,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should properly place panels with fixed height', function() { + it('should properly place panels with fixed height', () => { model.rows = [ createRow({ height: 6 }, [[6], [6, 3], [6, 3]]), createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]]), @@ -245,7 +245,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should place panel to the right side of panel having bigger height', function() { + it('should place panel to the right side of panel having bigger height', () => { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -260,7 +260,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should fill current row if it possible', function() { + it('should fill current row if it possible', () => { model.rows = [createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -276,7 +276,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should fill current row if it possible (2)', function() { + it('should fill current row if it possible (2)', () => { model.rows = [createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -292,7 +292,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should fill current row if panel height more than row height', function() { + it('should fill current row if panel height more than row height', () => { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -307,7 +307,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should wrap panels to multiple rows', function() { + it('should wrap panels to multiple rows', () => { model.rows = [createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -323,7 +323,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add repeated row if repeat set', function() { + it('should add repeated row if repeat set', () => { model.rows = [ createRow({ showTitle: true, title: 'Row', height: 8, repeat: 'server' }, [[6]]), createRow({ height: 8 }, [[12]]), @@ -344,7 +344,7 @@ describe('DashboardModel', function() { expect(dashboard.panels[3].repeat).toBeUndefined(); }); - it('should ignore repeated row', function() { + it('should ignore repeated row', () => { model.rows = [ createRow({ showTitle: true, title: 'Row1', height: 8, repeat: 'server' }, [[6]]), createRow( @@ -364,7 +364,7 @@ describe('DashboardModel', function() { expect(dashboard.panels.length).toBe(2); }); - it('minSpan should be twice', function() { + it('minSpan should be twice', () => { model.rows = [createRow({ height: 8 }, [[6]])]; model.rows[0].panels[0] = { minSpan: 12 }; @@ -372,7 +372,7 @@ describe('DashboardModel', function() { expect(dashboard.panels[0].minSpan).toBe(24); }); - it('should assign id', function() { + it('should assign id', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])]; model.rows[0].panels[0] = {}; diff --git a/public/app/features/dashboard/specs/dashboard_model.test.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts index 24d036a8233..e59d52f2410 100644 --- a/public/app/features/dashboard/specs/dashboard_model.test.ts +++ b/public/app/features/dashboard/specs/dashboard_model.test.ts @@ -4,43 +4,43 @@ import { PanelModel } from '../panel_model'; jest.mock('app/core/services/context_srv', () => ({})); -describe('DashboardModel', function() { - describe('when creating new dashboard model defaults only', function() { +describe('DashboardModel', () => { + describe('when creating new dashboard model defaults only', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({}, {}); }); - it('should have title', function() { + it('should have title', () => { expect(model.title).toBe('No Title'); }); - it('should have meta', function() { + it('should have meta', () => { expect(model.meta.canSave).toBe(true); expect(model.meta.canShare).toBe(true); }); - it('should have default properties', function() { + it('should have default properties', () => { expect(model.panels.length).toBe(0); }); }); - describe('when getting next panel id', function() { + describe('when getting next panel id', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ panels: [{ id: 5 }], }); }); - it('should return max id + 1', function() { + it('should return max id + 1', () => { expect(model.getNextPanelId()).toBe(6); }); }); - describe('getSaveModelClone', function() { + describe('getSaveModelClone', () => { it('should sort keys', () => { const model = new DashboardModel({}); const saveModel = model.getSaveModelClone(); @@ -68,20 +68,20 @@ describe('DashboardModel', function() { }); }); - describe('row and panel manipulation', function() { + describe('row and panel manipulation', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({}); }); - it('adding panel should new up panel model', function() { + it('adding panel should new up panel model', () => { dashboard.addPanel({ type: 'test', title: 'test' }); expect(dashboard.panels[0] instanceof PanelModel).toBe(true); }); - it('duplicate panel should try to add to the right if there is space', function() { + it('duplicate panel should try to add to the right if there is space', () => { const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } }; dashboard.addPanel(panel); @@ -95,7 +95,7 @@ describe('DashboardModel', function() { }); }); - it('duplicate panel should remove repeat data', function() { + it('duplicate panel should remove repeat data', () => { const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 }, @@ -111,29 +111,29 @@ describe('DashboardModel', function() { }); }); - describe('Given editable false dashboard', function() { + describe('Given editable false dashboard', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ editable: false }); }); - it('Should set meta canEdit and canSave to false', function() { + it('Should set meta canEdit and canSave to false', () => { expect(model.meta.canSave).toBe(false); expect(model.meta.canEdit).toBe(false); }); - it('getSaveModelClone should remove meta', function() { + it('getSaveModelClone should remove meta', () => { const clone = model.getSaveModelClone(); expect(clone.meta).toBe(undefined); }); }); - describe('when loading dashboard with old influxdb query schema', function() { + describe('when loading dashboard with old influxdb query schema', () => { let model; let target; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ panels: [ { @@ -185,7 +185,7 @@ describe('DashboardModel', function() { target = model.panels[0].targets[0]; }); - it('should update query schema', function() { + it('should update query schema', () => { expect(target.fields).toBe(undefined); expect(target.select.length).toBe(2); expect(target.select[0].length).toBe(4); @@ -196,10 +196,10 @@ describe('DashboardModel', function() { }); }); - describe('when creating dashboard model with missing list for annoations or templating', function() { + describe('when creating dashboard model with missing list for annoations or templating', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ annotations: { enable: true, @@ -210,54 +210,54 @@ describe('DashboardModel', function() { }); }); - it('should add empty list', function() { + it('should add empty list', () => { expect(model.annotations.list.length).toBe(1); expect(model.templating.list.length).toBe(0); }); - it('should add builtin annotation query', function() { + it('should add builtin annotation query', () => { expect(model.annotations.list[0].builtIn).toBe(1); expect(model.templating.list.length).toBe(0); }); }); - describe('Formatting epoch timestamp when timezone is set as utc', function() { + describe('Formatting epoch timestamp when timezone is set as utc', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ timezone: 'utc' }); }); - it('Should format timestamp with second resolution by default', function() { + it('Should format timestamp with second resolution by default', () => { expect(dashboard.formatDate(1234567890000)).toBe('2009-02-13 23:31:30'); }); - it('Should format timestamp with second resolution even if second format is passed as parameter', function() { + it('Should format timestamp with second resolution even if second format is passed as parameter', () => { expect(dashboard.formatDate(1234567890007, 'YYYY-MM-DD HH:mm:ss')).toBe('2009-02-13 23:31:30'); }); - it('Should format timestamp with millisecond resolution if format is passed as parameter', function() { + it('Should format timestamp with millisecond resolution if format is passed as parameter', () => { expect(dashboard.formatDate(1234567890007, 'YYYY-MM-DD HH:mm:ss.SSS')).toBe('2009-02-13 23:31:30.007'); }); }); - describe('updateSubmenuVisibility with empty lists', function() { + describe('updateSubmenuVisibility with empty lists', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({}); model.updateSubmenuVisibility(); }); - it('should not enable submmenu', function() { + it('should not enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(false); }); }); - describe('updateSubmenuVisibility with annotation', function() { + describe('updateSubmenuVisibility with annotation', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ annotations: { list: [{}], @@ -266,15 +266,15 @@ describe('DashboardModel', function() { model.updateSubmenuVisibility(); }); - it('should enable submmenu', function() { + it('should enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(true); }); }); - describe('updateSubmenuVisibility with template var', function() { + describe('updateSubmenuVisibility with template var', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ templating: { list: [{}], @@ -283,15 +283,15 @@ describe('DashboardModel', function() { model.updateSubmenuVisibility(); }); - it('should enable submmenu', function() { + it('should enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(true); }); }); - describe('updateSubmenuVisibility with hidden template var', function() { + describe('updateSubmenuVisibility with hidden template var', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ templating: { list: [{ hide: 2 }], @@ -300,15 +300,15 @@ describe('DashboardModel', function() { model.updateSubmenuVisibility(); }); - it('should not enable submmenu', function() { + it('should not enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(false); }); }); - describe('updateSubmenuVisibility with hidden annotation toggle', function() { + describe('updateSubmenuVisibility with hidden annotation toggle', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ annotations: { list: [{ hide: true }], @@ -317,15 +317,15 @@ describe('DashboardModel', function() { dashboard.updateSubmenuVisibility(); }); - it('should not enable submmenu', function() { + it('should not enable submmenu', () => { expect(dashboard.meta.submenuEnabled).toBe(false); }); }); - describe('When collapsing row', function() { + describe('When collapsing row', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 2 } }, @@ -338,36 +338,36 @@ describe('DashboardModel', function() { dashboard.toggleRow(dashboard.panels[1]); }); - it('should remove panels and put them inside collapsed row', function() { + it('should remove panels and put them inside collapsed row', () => { expect(dashboard.panels.length).toBe(3); expect(dashboard.panels[1].panels.length).toBe(2); }); - describe('and when removing row and its panels', function() { - beforeEach(function() { + describe('and when removing row and its panels', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], true); }); - it('should remove row and its panels', function() { + it('should remove row and its panels', () => { expect(dashboard.panels.length).toBe(2); }); }); - describe('and when removing only the row', function() { - beforeEach(function() { + describe('and when removing only the row', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], false); }); - it('should only remove row', function() { + it('should only remove row', () => { expect(dashboard.panels.length).toBe(4); }); }); }); - describe('When expanding row', function() { + describe('When expanding row', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 6 } }, @@ -387,16 +387,16 @@ describe('DashboardModel', function() { dashboard.toggleRow(dashboard.panels[1]); }); - it('should add panels back', function() { + it('should add panels back', () => { expect(dashboard.panels.length).toBe(5); }); - it('should add them below row in array', function() { + it('should add them below row in array', () => { expect(dashboard.panels[2].id).toBe(3); expect(dashboard.panels[3].id).toBe(4); }); - it('should position them below row', function() { + it('should position them below row', () => { expect(dashboard.panels[2].gridPos).toMatchObject({ x: 0, y: 7, @@ -405,7 +405,7 @@ describe('DashboardModel', function() { }); }); - it('should move panels below down', function() { + it('should move panels below down', () => { expect(dashboard.panels[4].gridPos).toMatchObject({ x: 0, y: 9, @@ -414,22 +414,22 @@ describe('DashboardModel', function() { }); }); - describe('and when removing row and its panels', function() { - beforeEach(function() { + describe('and when removing row and its panels', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], true); }); - it('should remove row and its panels', function() { + it('should remove row and its panels', () => { expect(dashboard.panels.length).toBe(2); }); }); - describe('and when removing only the row', function() { - beforeEach(function() { + describe('and when removing only the row', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], false); }); - it('should only remove row', function() { + it('should only remove row', () => { expect(dashboard.panels.length).toBe(4); }); }); diff --git a/public/app/features/dashboard/specs/history_srv.test.ts b/public/app/features/dashboard/specs/history_srv.test.ts index 5c8578ecf39..1e2bd57a221 100644 --- a/public/app/features/dashboard/specs/history_srv.test.ts +++ b/public/app/features/dashboard/specs/history_srv.test.ts @@ -4,7 +4,7 @@ import { HistorySrv } from '../history/history_srv'; import { DashboardModel } from '../dashboard_model'; jest.mock('app/core/store'); -describe('historySrv', function() { +describe('historySrv', () => { const versionsResponse = versions(); const restoreResponse = restore; @@ -19,35 +19,35 @@ describe('historySrv', function() { const emptyDash = new DashboardModel({}); const historyListOpts = { limit: 10, start: 0 }; - describe('getHistoryList', function() { - it('should return a versions array for the given dashboard id', function() { + describe('getHistoryList', () => { + it('should return a versions array for the given dashboard id', () => { backendSrv.get = jest.fn(() => Promise.resolve(versionsResponse)); historySrv = new HistorySrv(backendSrv); - return historySrv.getHistoryList(dash, historyListOpts).then(function(versions) { + return historySrv.getHistoryList(dash, historyListOpts).then(versions => { expect(versions).toEqual(versionsResponse); }); }); - it('should return an empty array when not given an id', function() { - return historySrv.getHistoryList(emptyDash, historyListOpts).then(function(versions) { + it('should return an empty array when not given an id', () => { + return historySrv.getHistoryList(emptyDash, historyListOpts).then(versions => { expect(versions).toEqual([]); }); }); - it('should return an empty array when not given a dashboard', function() { - return historySrv.getHistoryList(null, historyListOpts).then(function(versions) { + it('should return an empty array when not given a dashboard', () => { + return historySrv.getHistoryList(null, historyListOpts).then(versions => { expect(versions).toEqual([]); }); }); }); describe('restoreDashboard', () => { - it('should return a success response given valid parameters', function() { + it('should return a success response given valid parameters', () => { const version = 6; backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version))); historySrv = new HistorySrv(backendSrv); - return historySrv.restoreDashboard(dash, version).then(function(response) { + return historySrv.restoreDashboard(dash, version).then(response => { expect(response).toEqual(restoreResponse(version)); }); }); diff --git a/public/app/features/dashboard/specs/repeat.test.ts b/public/app/features/dashboard/specs/repeat.test.ts index 47e3590ef9e..49fb4ea9ee7 100644 --- a/public/app/features/dashboard/specs/repeat.test.ts +++ b/public/app/features/dashboard/specs/repeat.test.ts @@ -4,10 +4,10 @@ import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); -describe('given dashboard with panel repeat', function() { +describe('given dashboard with panel repeat', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { const dashboardJSON = { panels: [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } }, @@ -35,7 +35,7 @@ describe('given dashboard with panel repeat', function() { dashboard.processRepeats(); }); - it('should repeat panels when row is expanding', function() { + it('should repeat panels when row is expanding', () => { expect(dashboard.panels.length).toBe(4); // toggle row @@ -55,10 +55,10 @@ describe('given dashboard with panel repeat', function() { }); }); -describe('given dashboard with panel repeat in horizontal direction', function() { +describe('given dashboard with panel repeat in horizontal direction', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { @@ -89,22 +89,22 @@ describe('given dashboard with panel repeat in horizontal direction', function() dashboard.processRepeats(); }); - it('should repeat panel 3 times', function() { + it('should repeat panel 3 times', () => { expect(dashboard.panels.length).toBe(3); }); - it('should mark panel repeated', function() { + it('should mark panel repeated', () => { expect(dashboard.panels[0].repeat).toBe('apps'); expect(dashboard.panels[1].repeatPanelId).toBe(2); }); - it('should set scopedVars on panels', function() { + it('should set scopedVars on panels', () => { expect(dashboard.panels[0].scopedVars.apps.value).toBe('se1'); expect(dashboard.panels[1].scopedVars.apps.value).toBe('se2'); expect(dashboard.panels[2].scopedVars.apps.value).toBe('se3'); }); - it('should place on first row and adjust width so all fit', function() { + it('should place on first row and adjust width so all fit', () => { expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, @@ -125,23 +125,23 @@ describe('given dashboard with panel repeat in horizontal direction', function() }); }); - describe('After a second iteration', function() { - beforeEach(function() { + describe('After a second iteration', () => { + beforeEach(() => { dashboard.panels[0].fill = 10; dashboard.processRepeats(); }); - it('reused panel should copy properties from source', function() { + it('reused panel should copy properties from source', () => { expect(dashboard.panels[1].fill).toBe(10); }); - it('should have same panel count', function() { + it('should have same panel count', () => { expect(dashboard.panels.length).toBe(3); }); }); - describe('After a second iteration with different variable', function() { - beforeEach(function() { + describe('After a second iteration with different variable', () => { + beforeEach(() => { dashboard.templating.list.push({ name: 'server', current: { text: 'se1, se2, se3', value: ['se1'] }, @@ -151,46 +151,46 @@ describe('given dashboard with panel repeat in horizontal direction', function() dashboard.processRepeats(); }); - it('should remove scopedVars value for last variable', function() { + it('should remove scopedVars value for last variable', () => { expect(dashboard.panels[0].scopedVars.apps).toBe(undefined); }); - it('should have new variable value in scopedVars', function() { + it('should have new variable value in scopedVars', () => { expect(dashboard.panels[0].scopedVars.server.value).toBe('se1'); }); }); - describe('After a second iteration and selected values reduced', function() { - beforeEach(function() { + describe('After a second iteration and selected values reduced', () => { + beforeEach(() => { dashboard.templating.list[0].options[1].selected = false; dashboard.processRepeats(); }); - it('should clean up repeated panel', function() { + it('should clean up repeated panel', () => { expect(dashboard.panels.length).toBe(2); }); }); - describe('After a second iteration and panel repeat is turned off', function() { - beforeEach(function() { + describe('After a second iteration and panel repeat is turned off', () => { + beforeEach(() => { dashboard.panels[0].repeat = null; dashboard.processRepeats(); }); - it('should clean up repeated panel', function() { + it('should clean up repeated panel', () => { expect(dashboard.panels.length).toBe(1); }); - it('should remove scoped vars from reused panel', function() { + it('should remove scoped vars from reused panel', () => { expect(dashboard.panels[0].scopedVars).toBe(undefined); }); }); }); -describe('given dashboard with panel repeat in vertical direction', function() { +describe('given dashboard with panel repeat in vertical direction', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } }, @@ -218,7 +218,7 @@ describe('given dashboard with panel repeat in vertical direction', function() { dashboard.processRepeats(); }); - it('should place on items on top of each other and keep witdh', function() { + it('should place on items on top of each other and keep witdh', () => { expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // first row expect(dashboard.panels[1].gridPos).toMatchObject({ x: 5, y: 1, h: 2, w: 8 }); @@ -290,7 +290,7 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi ]); }); - it('should be placed in their places', function() { + it('should be placed in their places', () => { expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // 1st row expect(dashboard.panels[1].gridPos).toMatchObject({ x: 0, y: 1, h: 2, w: 6 }); @@ -311,10 +311,10 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi }); }); -describe('given dashboard with row repeat', function() { +describe('given dashboard with row repeat', () => { let dashboard, dashboardJSON; - beforeEach(function() { + beforeEach(() => { dashboardJSON = { panels: [ { @@ -349,12 +349,12 @@ describe('given dashboard with row repeat', function() { dashboard.processRepeats(); }); - it('should not repeat only row', function() { + it('should not repeat only row', () => { const panelTypes = _.map(dashboard.panels, 'type'); expect(panelTypes).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph', 'row', 'graph']); }); - it('should set scopedVars for each panel', function() { + it('should set scopedVars for each panel', () => { dashboardJSON.templating.list[0].options[2].selected = true; dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); @@ -375,12 +375,12 @@ describe('given dashboard with row repeat', function() { expect(scopedVars).toEqual(['se1', 'se1', 'se1', 'se2', 'se2', 'se2', 'se3', 'se3', 'se3']); }); - it('should repeat only configured row', function() { + it('should repeat only configured row', () => { expect(dashboard.panels[6].id).toBe(4); expect(dashboard.panels[7].id).toBe(5); }); - it('should repeat only row if it is collapsed', function() { + it('should repeat only row if it is collapsed', () => { dashboardJSON.panels = [ { id: 1, @@ -405,7 +405,7 @@ describe('given dashboard with row repeat', function() { expect(dashboard.panels[1].panels).toHaveLength(2); }); - it('should properly repeat multiple rows', function() { + it('should properly repeat multiple rows', () => { dashboardJSON.panels = [ { id: 1, @@ -469,7 +469,7 @@ describe('given dashboard with row repeat', function() { expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02'); }); - it('should assign unique ids for repeated panels', function() { + it('should assign unique ids for repeated panels', () => { dashboardJSON.panels = [ { id: 1, @@ -501,7 +501,7 @@ describe('given dashboard with row repeat', function() { expect(panelIds.length).toEqual(_.uniq(panelIds).length); }); - it('should place new panels in proper order', function() { + it('should place new panels in proper order', () => { dashboardJSON.panels = [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 }, repeat: 'apps' }, { id: 2, type: 'graph', gridPos: { x: 0, y: 1, h: 3, w: 12 } }, @@ -646,7 +646,7 @@ describe('given dashboard with row and panel repeat', () => { }); }); - it('should repeat panels when row is expanding', function() { + it('should repeat panels when row is expanding', () => { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); diff --git a/public/app/features/dashboard/specs/save_as_modal.test.ts b/public/app/features/dashboard/specs/save_as_modal.test.ts index 29ed694474b..ceb7e49c550 100644 --- a/public/app/features/dashboard/specs/save_as_modal.test.ts +++ b/public/app/features/dashboard/specs/save_as_modal.test.ts @@ -10,11 +10,11 @@ describe('saving dashboard as', () => { }; const mockDashboardSrv = { - getCurrent: function() { + getCurrent: () => { return { id: 5, meta: {}, - getSaveModelClone: function() { + getSaveModelClone: () => { return json; }, }; diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts index fb1a652a03c..a3ab27a984f 100644 --- a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts +++ b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts @@ -7,11 +7,11 @@ describe('SaveProvisionedDashboardModalCtrl', () => { }; const mockDashboardSrv = { - getCurrent: function() { + getCurrent: () => { return { id: 5, meta: {}, - getSaveModelClone: function() { + getSaveModelClone: () => { return json; }, }; diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts index 6effc504861..8a8d94fdddb 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts @@ -136,7 +136,7 @@ describe('ShareModalCtrl', () => { ctx.$location.absUrl = () => 'http://server/#!/test'; ctx.scope.options.includeTemplateVars = true; - ctx.templateSrv.fillVariableValuesForUrl = function(params) { + ctx.templateSrv.fillVariableValuesForUrl = params => { params['var-app'] = 'mupp'; params['var-server'] = 'srv-01'; }; diff --git a/public/app/features/dashboard/specs/time_srv.test.ts b/public/app/features/dashboard/specs/time_srv.test.ts index 046ac52c9bf..514e0b90792 100644 --- a/public/app/features/dashboard/specs/time_srv.test.ts +++ b/public/app/features/dashboard/specs/time_srv.test.ts @@ -2,7 +2,7 @@ import { TimeSrv } from '../time_srv'; import '../time_srv'; import moment from 'moment'; -describe('timeSrv', function() { +describe('timeSrv', () => { const rootScope = { $on: jest.fn(), onAppEvent: jest.fn(), @@ -26,20 +26,20 @@ describe('timeSrv', function() { getTimezone: jest.fn(() => 'browser'), }; - beforeEach(function() { + beforeEach(() => { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); }); - describe('timeRange', function() { - it('should return unparsed when parse is false', function() { + describe('timeRange', () => { + it('should return unparsed when parse is false', () => { timeSrv.setTime({ from: 'now', to: 'now-1h' }); const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now'); expect(time.raw.to).toBe('now-1h'); }); - it('should return parsed when parse is true', function() { + it('should return parsed when parse is true', () => { timeSrv.setTime({ from: 'now', to: 'now-1h' }); const time = timeSrv.timeRange(); expect(moment.isMoment(time.from)).toBe(true); @@ -47,8 +47,8 @@ describe('timeSrv', function() { }); }); - describe('init time from url', function() { - it('should handle relative times', function() { + describe('init time from url', () => { + it('should handle relative times', () => { location = { search: jest.fn(() => ({ from: 'now-2d', @@ -63,7 +63,7 @@ describe('timeSrv', function() { expect(time.raw.to).toBe('now'); }); - it('should handle formatted dates', function() { + it('should handle formatted dates', () => { location = { search: jest.fn(() => ({ from: '20140410T052010', @@ -79,7 +79,7 @@ describe('timeSrv', function() { expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime()); }); - it('should handle formatted dates without time', function() { + it('should handle formatted dates without time', () => { location = { search: jest.fn(() => ({ from: '20140410', @@ -95,7 +95,7 @@ describe('timeSrv', function() { expect(time.to.valueOf()).toEqual(new Date('2014-05-20T00:00:00Z').getTime()); }); - it('should handle epochs', function() { + it('should handle epochs', () => { location = { search: jest.fn(() => ({ from: '1410337646373', @@ -111,7 +111,7 @@ describe('timeSrv', function() { expect(time.to.valueOf()).toEqual(1410337665699); }); - it('should handle bad dates', function() { + it('should handle bad dates', () => { location = { search: jest.fn(() => ({ from: '20151126T00010%3C%2Fp%3E%3Cspan%20class', @@ -128,22 +128,22 @@ describe('timeSrv', function() { }); }); - describe('setTime', function() { - it('should return disable refresh if refresh is disabled for any range', function() { + describe('setTime', () => { + it('should return disable refresh if refresh is disabled for any range', () => { _dashboard.refresh = false; timeSrv.setTime({ from: '2011-01-01', to: '2015-01-01' }); expect(_dashboard.refresh).toBe(false); }); - it('should restore refresh for absolute time range', function() { + it('should restore refresh for absolute time range', () => { _dashboard.refresh = '30s'; timeSrv.setTime({ from: '2011-01-01', to: '2015-01-01' }); expect(_dashboard.refresh).toBe('30s'); }); - it('should restore refresh after relative time range is set', function() { + it('should restore refresh after relative time range is set', () => { _dashboard.refresh = '10s'; timeSrv.setTime({ from: moment([2011, 1, 1]), @@ -154,7 +154,7 @@ describe('timeSrv', function() { expect(_dashboard.refresh).toBe('10s'); }); - it('should keep refresh after relative time range is changed and now delay exists', function() { + it('should keep refresh after relative time range is changed and now delay exists', () => { _dashboard.refresh = '10s'; timeSrv.setTime({ from: 'now-1h', to: 'now-10s' }); expect(_dashboard.refresh).toBe('10s'); diff --git a/public/app/features/org/org_details_ctrl.ts b/public/app/features/org/org_details_ctrl.ts index 2ec1b57e170..1d4a92c6e8b 100644 --- a/public/app/features/org/org_details_ctrl.ts +++ b/public/app/features/org/org_details_ctrl.ts @@ -3,20 +3,20 @@ import angular from 'angular'; export class OrgDetailsCtrl { /** @ngInject */ constructor($scope, $http, backendSrv, contextSrv, navModelSrv) { - $scope.init = function() { + $scope.init = () => { $scope.getOrgInfo(); $scope.navModel = navModelSrv.getNav('cfg', 'org-settings', 0); }; - $scope.getOrgInfo = function() { - backendSrv.get('/api/org').then(function(org) { + $scope.getOrgInfo = () => { + backendSrv.get('/api/org').then(org => { $scope.org = org; $scope.address = org.address; contextSrv.user.orgName = org.name; }); }; - $scope.update = function() { + $scope.update = () => { if (!$scope.orgForm.$valid) { return; } @@ -24,7 +24,7 @@ export class OrgDetailsCtrl { backendSrv.put('/api/org', data).then($scope.getOrgInfo); }; - $scope.updateAddress = function() { + $scope.updateAddress = () => { if (!$scope.addressForm.$valid) { return; } diff --git a/public/app/features/panellinks/specs/link_srv.test.ts b/public/app/features/panellinks/specs/link_srv.test.ts index 9c6b62d4b69..e83c58f0d7c 100644 --- a/public/app/features/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panellinks/specs/link_srv.test.ts @@ -6,7 +6,7 @@ jest.mock('angular', () => { return new AngularJSMock(); }); -describe('linkSrv', function() { +describe('linkSrv', () => { let linkSrv; const templateSrvMock = {}; const timeSrvMock = {}; @@ -15,24 +15,24 @@ describe('linkSrv', function() { linkSrv = new LinkSrv(templateSrvMock, timeSrvMock); }); - describe('when appending query strings', function() { - it('add ? to URL if not present', function() { + describe('when appending query strings', () => { + it('add ? to URL if not present', () => { const url = linkSrv.appendToQueryString('http://example.com', 'foo=bar'); expect(url).toBe('http://example.com?foo=bar'); }); - it('do not add & to URL if ? is present but query string is empty', function() { + it('do not add & to URL if ? is present but query string is empty', () => { const url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar'); expect(url).toBe('http://example.com?foo=bar'); }); - it('add & to URL if query string is present', function() { + it('add & to URL if query string is present', () => { const url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world'); expect(url).toBe('http://example.com?foo=bar&hello=world'); }); - it('do not change the URL if there is nothing to append', function() { - _.each(['', undefined, null], function(toAppend) { + it('do not change the URL if there is nothing to append', () => { + _.each(['', undefined, null], toAppend => { const url1 = linkSrv.appendToQueryString('http://example.com', toAppend); expect(url1).toBe('http://example.com'); diff --git a/public/app/features/plugins/specs/datasource_srv.test.ts b/public/app/features/plugins/specs/datasource_srv.test.ts index 653e431cb9f..a8d0807c765 100644 --- a/public/app/features/plugins/specs/datasource_srv.test.ts +++ b/public/app/features/plugins/specs/datasource_srv.test.ts @@ -15,7 +15,7 @@ const templateSrv = { ], }; -describe('datasource_srv', function() { +describe('datasource_srv', () => { const _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv); describe('when loading explore sources', () => { diff --git a/public/app/features/templating/specs/adhoc_variable.test.ts b/public/app/features/templating/specs/adhoc_variable.test.ts index f85c49e73d5..6d15ce8362c 100644 --- a/public/app/features/templating/specs/adhoc_variable.test.ts +++ b/public/app/features/templating/specs/adhoc_variable.test.ts @@ -1,8 +1,8 @@ import { AdhocVariable } from '../adhoc_variable'; -describe('AdhocVariable', function() { - describe('when serializing to url', function() { - it('should set return key value and op separated by pipe', function() { +describe('AdhocVariable', () => { + describe('when serializing to url', () => { + it('should set return key value and op separated by pipe', () => { const variable = new AdhocVariable({ filters: [ { key: 'key1', operator: '=', value: 'value1' }, @@ -15,8 +15,8 @@ describe('AdhocVariable', function() { }); }); - describe('when deserializing from url', function() { - it('should restore filters', function() { + describe('when deserializing from url', () => { + it('should restore filters', () => { const variable = new AdhocVariable({}); variable.setValueFromUrl(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']); diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 984d62cb729..7f5ff959216 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -1,6 +1,6 @@ import { TemplateSrv } from '../template_srv'; -describe('templateSrv', function() { +describe('templateSrv', () => { let _templateSrv; function initTemplateSrv(variables) { @@ -8,58 +8,58 @@ describe('templateSrv', function() { _templateSrv.init(variables); } - describe('init', function() { - beforeEach(function() { + describe('init', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); - it('should initialize template data', function() { + it('should initialize template data', () => { const target = _templateSrv.replace('this.[[test]].filters'); expect(target).toBe('this.oogle.filters'); }); }); - describe('replace can pass scoped vars', function() { - beforeEach(function() { + describe('replace can pass scoped vars', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); - it('should replace $test with scoped value', function() { + it('should replace $test with scoped value', () => { const target = _templateSrv.replace('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); - it('should replace ${test} with scoped value', function() { + it('should replace ${test} with scoped value', () => { const target = _templateSrv.replace('this.${test}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); - it('should replace ${test:glob} with scoped value', function() { + it('should replace ${test:glob} with scoped value', () => { const target = _templateSrv.replace('this.${test:glob}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); - it('should replace $test with scoped text', function() { + it('should replace $test with scoped text', () => { const target = _templateSrv.replaceWithText('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); - it('should replace ${test} with scoped text', function() { + it('should replace ${test} with scoped text', () => { const target = _templateSrv.replaceWithText('this.${test}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); - it('should replace ${test:glob} with scoped text', function() { + it('should replace ${test:glob} with scoped text', () => { const target = _templateSrv.replaceWithText('this.${test:glob}.filters', { test: { value: 'mupp', text: 'asd' }, }); @@ -67,8 +67,8 @@ describe('templateSrv', function() { }); }); - describe('getAdhocFilters', function() { - beforeEach(function() { + describe('getAdhocFilters', () => { + beforeEach(() => { initTemplateSrv([ { type: 'datasource', @@ -80,24 +80,24 @@ describe('templateSrv', function() { ]); }); - it('should return filters if datasourceName match', function() { + it('should return filters if datasourceName match', () => { const filters = _templateSrv.getAdhocFilters('oogle'); expect(filters).toMatchObject([1]); }); - it('should return empty array if datasourceName does not match', function() { + it('should return empty array if datasourceName does not match', () => { const filters = _templateSrv.getAdhocFilters('oogleasdasd'); expect(filters).toMatchObject([]); }); - it('should return filters when datasourceName match via data source variable', function() { + it('should return filters when datasourceName match via data source variable', () => { const filters = _templateSrv.getAdhocFilters('logstash'); expect(filters).toMatchObject([2]); }); }); - describe('replace can pass multi / all format', function() { - beforeEach(function() { + describe('replace can pass multi / all format', () => { + beforeEach(() => { initTemplateSrv([ { type: 'query', @@ -107,44 +107,44 @@ describe('templateSrv', function() { ]); }); - it('should replace $test with globbed value', function() { + it('should replace $test with globbed value', () => { const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); - it('should replace ${test} with globbed value', function() { + it('should replace ${test} with globbed value', () => { const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); - it('should replace ${test:glob} with globbed value', function() { + it('should replace ${test:glob} with globbed value', () => { const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); - it('should replace $test with piped value', function() { + it('should replace $test with piped value', () => { const target = _templateSrv.replace('this=$test', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); - it('should replace ${test} with piped value', function() { + it('should replace ${test} with piped value', () => { const target = _templateSrv.replace('this=${test}', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); - it('should replace ${test:pipe} with piped value', function() { + it('should replace ${test:pipe} with piped value', () => { const target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); - it('should replace ${test:pipe} with piped value and $test with globbed value', function() { + it('should replace ${test:pipe} with piped value and $test with globbed value', () => { const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); expect(target).toBe('value1|value2,{value1,value2}'); }); }); - describe('variable with all option', function() { - beforeEach(function() { + describe('variable with all option', () => { + beforeEach(() => { initTemplateSrv([ { type: 'query', @@ -155,29 +155,29 @@ describe('templateSrv', function() { ]); }); - it('should replace $test with formatted all value', function() { + it('should replace $test with formatted all value', () => { const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); - it('should replace ${test} with formatted all value', function() { + it('should replace ${test} with formatted all value', () => { const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); - it('should replace ${test:glob} with formatted all value', function() { + it('should replace ${test:glob} with formatted all value', () => { const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); - it('should replace ${test:pipe} with piped value and $test with globbed value', function() { + it('should replace ${test:pipe} with piped value and $test with globbed value', () => { const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); expect(target).toBe('value1|value2,{value1,value2}'); }); }); - describe('variable with all option and custom value', function() { - beforeEach(function() { + describe('variable with all option and custom value', () => { + beforeEach(() => { initTemplateSrv([ { type: 'query', @@ -189,143 +189,143 @@ describe('templateSrv', function() { ]); }); - it('should replace $test with formatted all value', function() { + it('should replace $test with formatted all value', () => { const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.*.filters'); }); - it('should replace ${test} with formatted all value', function() { + it('should replace ${test} with formatted all value', () => { const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.*.filters'); }); - it('should replace ${test:glob} with formatted all value', function() { + it('should replace ${test:glob} with formatted all value', () => { const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.*.filters'); }); - it('should not escape custom all value', function() { + it('should not escape custom all value', () => { const target = _templateSrv.replace('this.$test', {}, 'regex'); expect(target).toBe('this.*'); }); }); - describe('lucene format', function() { - it('should properly escape $test with lucene escape sequences', function() { + describe('lucene format', () => { + it('should properly escape $test with lucene escape sequences', () => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); const target = _templateSrv.replace('this:$test', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); - it('should properly escape ${test} with lucene escape sequences', function() { + it('should properly escape ${test} with lucene escape sequences', () => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); const target = _templateSrv.replace('this:${test}', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); - it('should properly escape ${test:lucene} with lucene escape sequences', function() { + it('should properly escape ${test:lucene} with lucene escape sequences', () => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); const target = _templateSrv.replace('this:${test:lucene}', {}); expect(target).toBe('this:value\\/4'); }); }); - describe('format variable to string values', function() { - it('single value should return value', function() { + describe('format variable to string values', () => { + it('single value should return value', () => { const result = _templateSrv.formatValue('test'); expect(result).toBe('test'); }); - it('multi value and glob format should render glob string', function() { + it('multi value and glob format should render glob string', () => { const result = _templateSrv.formatValue(['test', 'test2'], 'glob'); expect(result).toBe('{test,test2}'); }); - it('multi value and lucene should render as lucene expr', function() { + it('multi value and lucene should render as lucene expr', () => { const result = _templateSrv.formatValue(['test', 'test2'], 'lucene'); expect(result).toBe('("test" OR "test2")'); }); - it('multi value and regex format should render regex string', function() { + it('multi value and regex format should render regex string', () => { const result = _templateSrv.formatValue(['test.', 'test2'], 'regex'); expect(result).toBe('(test\\.|test2)'); }); - it('multi value and pipe should render pipe string', function() { + it('multi value and pipe should render pipe string', () => { const result = _templateSrv.formatValue(['test', 'test2'], 'pipe'); expect(result).toBe('test|test2'); }); - it('multi value and distributed should render distributed string', function() { + it('multi value and distributed should render distributed string', () => { const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', { name: 'build', }); expect(result).toBe('test,build=test2'); }); - it('multi value and distributed should render when not string', function() { + it('multi value and distributed should render when not string', () => { const result = _templateSrv.formatValue(['test'], 'distributed', { name: 'build', }); expect(result).toBe('test'); }); - it('multi value and csv format should render csv string', function() { + it('multi value and csv format should render csv string', () => { const result = _templateSrv.formatValue(['test', 'test2'], 'csv'); expect(result).toBe('test,test2'); }); - it('slash should be properly escaped in regex format', function() { + it('slash should be properly escaped in regex format', () => { const result = _templateSrv.formatValue('Gi3/14', 'regex'); expect(result).toBe('Gi3\\/14'); }); }); - describe('can check if variable exists', function() { - beforeEach(function() { + describe('can check if variable exists', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); - it('should return true if exists', function() { + it('should return true if exists', () => { const result = _templateSrv.variableExists('$test'); expect(result).toBe(true); }); }); - describe('can highlight variables in string', function() { - beforeEach(function() { + describe('can highlight variables in string', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); - it('should insert html', function() { + it('should insert html', () => { const result = _templateSrv.highlightVariablesAsHtml('$test'); expect(result).toBe('$test'); }); - it('should insert html anywhere in string', function() { + it('should insert html anywhere in string', () => { const result = _templateSrv.highlightVariablesAsHtml('this $test ok'); expect(result).toBe('this $test ok'); }); - it('should ignore if variables does not exist', function() { + it('should ignore if variables does not exist', () => { const result = _templateSrv.highlightVariablesAsHtml('this $google ok'); expect(result).toBe('this $google ok'); }); }); - describe('updateTemplateData with simple value', function() { - beforeEach(function() { + describe('updateTemplateData with simple value', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]); }); - it('should set current value and update template data', function() { + it('should set current value and update template data', () => { const target = _templateSrv.replace('this.[[test]].filters'); expect(target).toBe('this.muuuu.filters'); }); }); - describe('fillVariableValuesForUrl with multi value', function() { - beforeEach(function() { + describe('fillVariableValuesForUrl with multi value', () => { + beforeEach(() => { initTemplateSrv([ { type: 'query', @@ -338,15 +338,15 @@ describe('templateSrv', function() { ]); }); - it('should set multiple url params', function() { + it('should set multiple url params', () => { const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toMatchObject(['val1', 'val2']); }); }); - describe('fillVariableValuesForUrl skip url sync', function() { - beforeEach(function() { + describe('fillVariableValuesForUrl skip url sync', () => { + beforeEach(() => { initTemplateSrv([ { name: 'test', @@ -359,15 +359,15 @@ describe('templateSrv', function() { ]); }); - it('should not include template variable value in url', function() { + it('should not include template variable value in url', () => { const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toBe(undefined); }); }); - describe('fillVariableValuesForUrl with multi value with skip url sync', function() { - beforeEach(function() { + describe('fillVariableValuesForUrl with multi value with skip url sync', () => { + beforeEach(() => { initTemplateSrv([ { type: 'query', @@ -381,19 +381,19 @@ describe('templateSrv', function() { ]); }); - it('should not include template variable value in url', function() { + it('should not include template variable value in url', () => { const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toBe(undefined); }); }); - describe('fillVariableValuesForUrl with multi value and scopedVars', function() { - beforeEach(function() { + describe('fillVariableValuesForUrl with multi value and scopedVars', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]); }); - it('should set scoped value as url params', function() { + it('should set scoped value as url params', () => { const params = {}; _templateSrv.fillVariableValuesForUrl(params, { test: { value: 'val1' }, @@ -402,12 +402,12 @@ describe('templateSrv', function() { }); }); - describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', function() { - beforeEach(function() { + describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => { + beforeEach(() => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]); }); - it('should not set scoped value as url params', function() { + it('should not set scoped value as url params', () => { const params = {}; _templateSrv.fillVariableValuesForUrl(params, { test: { name: 'test', value: 'val1', skipUrlSync: true }, @@ -416,8 +416,8 @@ describe('templateSrv', function() { }); }); - describe('replaceWithText', function() { - beforeEach(function() { + describe('replaceWithText', () => { + beforeEach(() => { initTemplateSrv([ { type: 'query', @@ -434,18 +434,18 @@ describe('templateSrv', function() { _templateSrv.updateTemplateData(); }); - it('should replace with text except for grafanaVariables', function() { + it('should replace with text except for grafanaVariables', () => { const target = _templateSrv.replaceWithText('Server: $server, period: $period'); expect(target).toBe('Server: All, period: 13m'); }); }); - describe('built in interval variables', function() { - beforeEach(function() { + describe('built in interval variables', () => { + beforeEach(() => { initTemplateSrv([]); }); - it('should replace $__interval_ms with interval milliseconds', function() { + it('should replace $__interval_ms with interval milliseconds', () => { const target = _templateSrv.replace('10 * $__interval_ms', { __interval_ms: { text: '100', value: '100' }, }); diff --git a/public/app/features/templating/specs/variable.test.ts b/public/app/features/templating/specs/variable.test.ts index 814c5fbe003..6d5e88fa4bd 100644 --- a/public/app/features/templating/specs/variable.test.ts +++ b/public/app/features/templating/specs/variable.test.ts @@ -1,53 +1,53 @@ import { containsVariable, assignModelProperties } from '../variable'; -describe('containsVariable', function() { - describe('when checking if a string contains a variable', function() { - it('should find it with $const syntax', function() { +describe('containsVariable', () => { + describe('when checking if a string contains a variable', () => { + it('should find it with $const syntax', () => { const contains = containsVariable('this.$test.filters', 'test'); expect(contains).toBe(true); }); - it('should not find it if only part matches with $const syntax', function() { + it('should not find it if only part matches with $const syntax', () => { const contains = containsVariable('this.$serverDomain.filters', 'server'); expect(contains).toBe(false); }); - it('should find it if it ends with variable and passing multiple test strings', function() { + it('should find it if it ends with variable and passing multiple test strings', () => { const contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric'); expect(contains).toBe(true); }); - it('should find it with [[var]] syntax', function() { + it('should find it with [[var]] syntax', () => { const contains = containsVariable('this.[[test]].filters', 'test'); expect(contains).toBe(true); }); - it('should find it when part of segment', function() { + it('should find it when part of segment', () => { const contains = containsVariable('metrics.$env.$group-*', 'group'); expect(contains).toBe(true); }); - it('should find it its the only thing', function() { + it('should find it its the only thing', () => { const contains = containsVariable('$env', 'env'); expect(contains).toBe(true); }); - it('should be able to pass in multiple test strings', function() { + it('should be able to pass in multiple test strings', () => { const contains = containsVariable('asd', 'asd2.$env', 'env'); expect(contains).toBe(true); }); }); }); -describe('assignModelProperties', function() { - it('only set properties defined in defaults', function() { +describe('assignModelProperties', () => { + it('only set properties defined in defaults', () => { const target: any = { test: 'asd' }; assignModelProperties(target, { propA: 1, propB: 2 }, { propB: 0 }); expect(target.propB).toBe(2); expect(target.test).toBe('asd'); }); - it('use default value if not found on source', function() { + it('use default value if not found on source', () => { const target: any = { test: 'asd' }; assignModelProperties(target, { propA: 1, propB: 2 }, { propC: 10 }); expect(target.propC).toBe(10); diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index 06317ee1b3a..359d5b79a38 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -34,7 +34,7 @@ describe('VariableSrv', function(this: any) { function describeUpdateVariable(desc, fn) { describe(desc, () => { const scenario: any = {}; - scenario.setup = function(setupFn) { + scenario.setup = setupFn => { scenario.setupFn = setupFn; }; @@ -135,7 +135,7 @@ describe('VariableSrv', function(this: any) { // // Query variable update // - describeUpdateVariable('query variable with empty current object and refresh', function(scenario) { + describeUpdateVariable('query variable with empty current object and refresh', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -154,7 +154,7 @@ describe('VariableSrv', function(this: any) { describeUpdateVariable( 'query variable with multi select and new options does not contain some selected values', - function(scenario) { + scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -177,7 +177,7 @@ describe('VariableSrv', function(this: any) { describeUpdateVariable( 'query variable with multi select and new options does not contain any selected values', - function(scenario) { + scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -198,7 +198,7 @@ describe('VariableSrv', function(this: any) { } ); - describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) { + describeUpdateVariable('query variable with multi select and $__all selected', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -219,7 +219,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('query variable with numeric results', function(scenario) { + describeUpdateVariable('query variable with numeric results', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -237,7 +237,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('basic query variable', function(scenario) { + describeUpdateVariable('basic query variable', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; @@ -255,7 +255,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('and existing value still exists in options', function(scenario) { + describeUpdateVariable('and existing value still exists in options', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.current = { value: 'backend2', text: 'backend2' }; @@ -267,7 +267,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('and regex pattern exists', function(scenario) { + describeUpdateVariable('and regex pattern exists', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/'; @@ -282,7 +282,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('and regex pattern exists and no match', function(scenario) { + describeUpdateVariable('and regex pattern exists and no match', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/'; @@ -298,7 +298,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('regex pattern without slashes', function(scenario) { + describeUpdateVariable('regex pattern without slashes', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = 'backend_01'; @@ -313,7 +313,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('regex pattern remove duplicates', function(scenario) { + describeUpdateVariable('regex pattern remove duplicates', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = '/backend_01/'; @@ -328,7 +328,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('with include All', function(scenario) { + describeUpdateVariable('with include All', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -345,7 +345,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('with include all and custom value', function(scenario) { + describeUpdateVariable('with include all and custom value', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -362,7 +362,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('without sort', function(scenario) { + describeUpdateVariable('without sort', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -380,7 +380,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('with alphabetical sort (asc)', function(scenario) { + describeUpdateVariable('with alphabetical sort (asc)', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -398,7 +398,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('with alphabetical sort (desc)', function(scenario) { + describeUpdateVariable('with alphabetical sort (desc)', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -416,7 +416,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('with numerical sort (asc)', function(scenario) { + describeUpdateVariable('with numerical sort (asc)', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -434,7 +434,7 @@ describe('VariableSrv', function(this: any) { }); }); - describeUpdateVariable('with numerical sort (desc)', function(scenario) { + describeUpdateVariable('with numerical sort (desc)', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'query', @@ -455,7 +455,7 @@ describe('VariableSrv', function(this: any) { // // datasource variable update // - describeUpdateVariable('datasource variable with regex filter', function(scenario) { + describeUpdateVariable('datasource variable with regex filter', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'datasource', @@ -486,7 +486,7 @@ describe('VariableSrv', function(this: any) { // // Custom variable update // - describeUpdateVariable('update custom variable', function(scenario) { + describeUpdateVariable('update custom variable', scenario => { scenario.setup(() => { scenario.variableModel = { type: 'custom', diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index db08c1d6f81..497c773687f 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -3,7 +3,7 @@ import CloudWatchDatasource from '../datasource'; import * as dateMath from 'app/core/utils/datemath'; import _ from 'lodash'; -describe('CloudWatchDatasource', function() { +describe('CloudWatchDatasource', () => { const instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; @@ -34,7 +34,7 @@ describe('CloudWatchDatasource', function() { ctx.ds = new CloudWatchDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv); }); - describe('When performing CloudWatch query', function() { + describe('When performing CloudWatch query', () => { let requestParams; const query = { @@ -80,8 +80,8 @@ describe('CloudWatchDatasource', function() { }); }); - it('should generate the correct query', function(done) { - ctx.ds.query(query).then(function() { + it('should generate the correct query', done => { + ctx.ds.query(query).then(() => { const params = requestParams.queries[0]; expect(params.namespace).toBe(query.targets[0].namespace); expect(params.metricName).toBe(query.targets[0].metricName); @@ -92,7 +92,7 @@ describe('CloudWatchDatasource', function() { }); }); - it('should generate the correct query with interval variable', function(done) { + it('should generate the correct query with interval variable', done => { ctx.templateSrv.data = { period: '10m', }; @@ -114,14 +114,14 @@ describe('CloudWatchDatasource', function() { ], }; - ctx.ds.query(query).then(function() { + ctx.ds.query(query).then(() => { const params = requestParams.queries[0]; expect(params.period).toBe('600'); done(); }); }); - it('should cancel query for invalid extended statistics', function() { + it('should cancel query for invalid extended statistics', () => { const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, @@ -141,8 +141,8 @@ describe('CloudWatchDatasource', function() { expect(ctx.ds.query.bind(ctx.ds, query)).toThrow(/Invalid extended statistics/); }); - it('should return series list', function(done) { - ctx.ds.query(query).then(function(result) { + it('should return series list', done => { + ctx.ds.query(query).then(result => { expect(result.data[0].target).toBe(response.results.A.series[0].name); expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); done(); @@ -150,8 +150,8 @@ describe('CloudWatchDatasource', function() { }); }); - describe('When query region is "default"', function() { - it('should return the datasource region if empty or "default"', function() { + describe('When query region is "default"', () => { + it('should return the datasource region if empty or "default"', () => { const defaultRegion = instanceSettings.jsonData.defaultRegion; expect(ctx.ds.getActualRegion()).toBe(defaultRegion); @@ -159,19 +159,19 @@ describe('CloudWatchDatasource', function() { expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion); }); - it('should return the specified region if specified', function() { + it('should return the specified region if specified', () => { expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); }); let requestParams; - beforeEach(function() { + beforeEach(() => { ctx.ds.performTimeSeriesQuery = jest.fn(request => { requestParams = request; return Promise.resolve({ data: {} }); }); }); - it('should query for the datasource region if empty or "default"', function(done) { + it('should query for the datasource region if empty or "default"', done => { const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, @@ -189,14 +189,14 @@ describe('CloudWatchDatasource', function() { ], }; - ctx.ds.query(query).then(function(result) { + ctx.ds.query(query).then(result => { expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion); done(); }); }); }); - describe('When performing CloudWatch query for extended statistics', function() { + describe('When performing CloudWatch query for extended statistics', () => { const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, @@ -235,14 +235,14 @@ describe('CloudWatchDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(params => { return Promise.resolve({ data: response }); }); }); - it('should return series list', function(done) { - ctx.ds.query(query).then(function(result) { + it('should return series list', done => { + ctx.ds.query(query).then(result => { expect(result.data[0].target).toBe(response.results.A.series[0].name); expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); done(); @@ -378,7 +378,7 @@ describe('CloudWatchDatasource', function() { }); }); - it('should caclculate the correct period', function() { + it('should caclculate the correct period', () => { const hourSec = 60 * 60; const daySec = hourSec * 24; const start = 1483196400 * 1000; diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index 980fbe14593..4be0c35852c 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -43,8 +43,8 @@ describe('ElasticDatasource', function(this: any) { ctx.ds = new ElasticDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv); } - describe('When testing datasource with index pattern', function() { - beforeEach(function() { + describe('When testing datasource with index pattern', () => { + beforeEach(() => { createDatasource({ url: 'http://es.com', index: '[asd-]YYYY.MM.DD', @@ -52,7 +52,7 @@ describe('ElasticDatasource', function(this: any) { }); }); - it('should translate index pattern to current day', function() { + it('should translate index pattern to current day', () => { let requestOptions; ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; @@ -66,7 +66,7 @@ describe('ElasticDatasource', function(this: any) { }); }); - describe('When issuing metric query with interval pattern', function() { + describe('When issuing metric query with interval pattern', () => { let requestOptions, parts, header; beforeEach(() => { @@ -99,20 +99,20 @@ describe('ElasticDatasource', function(this: any) { header = angular.fromJson(parts[0]); }); - it('should translate index pattern to current day', function() { + it('should translate index pattern to current day', () => { expect(header.index).toEqual(['asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']); }); - it('should json escape lucene query', function() { + it('should json escape lucene query', () => { const body = angular.fromJson(parts[1]); expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test'); }); }); - describe('When issuing document query', function() { + describe('When issuing document query', () => { let requestOptions, parts, header; - beforeEach(function() { + beforeEach(() => { createDatasource({ url: 'http://es.com', index: 'test', @@ -142,17 +142,17 @@ describe('ElasticDatasource', function(this: any) { header = angular.fromJson(parts[0]); }); - it('should set search type to query_then_fetch', function() { + it('should set search type to query_then_fetch', () => { expect(header.search_type).toEqual('query_then_fetch'); }); - it('should set size', function() { + it('should set size', () => { const body = angular.fromJson(parts[1]); expect(body.size).toBe(500); }); }); - describe('When getting fields', function() { + describe('When getting fields', () => { beforeEach(() => { createDatasource({ url: 'http://es.com', index: 'metricbeat' }); @@ -203,7 +203,7 @@ describe('ElasticDatasource', function(this: any) { }); }); - it('should return nested fields', function() { + it('should return nested fields', () => { ctx.ds .getFields({ find: 'fields', @@ -224,7 +224,7 @@ describe('ElasticDatasource', function(this: any) { }); }); - it('should return fields related to query type', function() { + it('should return fields related to query type', () => { ctx.ds .getFields({ find: 'fields', @@ -249,10 +249,10 @@ describe('ElasticDatasource', function(this: any) { }); }); - describe('When issuing aggregation query on es5.x', function() { + describe('When issuing aggregation query on es5.x', () => { let requestOptions, parts, header; - beforeEach(function() { + beforeEach(() => { createDatasource({ url: 'http://es.com', index: 'test', @@ -282,17 +282,17 @@ describe('ElasticDatasource', function(this: any) { header = angular.fromJson(parts[0]); }); - it('should not set search type to count', function() { + it('should not set search type to count', () => { expect(header.search_type).not.toEqual('count'); }); - it('should set size to 0', function() { + it('should set size to 0', () => { const body = angular.fromJson(parts[1]); expect(body.size).toBe(0); }); }); - describe('When issuing metricFind query on es5.x', function() { + describe('When issuing metricFind query on es5.x', () => { let requestOptions, parts, header, body, results; beforeEach(() => { diff --git a/public/app/plugins/datasource/graphite/specs/datasource.test.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts index 563f1047cdb..cd60a059123 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource.test.ts @@ -12,12 +12,12 @@ describe('graphiteDatasource', () => { instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} }, }; - beforeEach(function() { + beforeEach(() => { ctx.instanceSettings.url = '/api/datasources/proxy/1'; ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); }); - describe('When querying graphite with one target using query editor target spec', function() { + describe('When querying graphite with one target using query editor target spec', () => { const query = { panelId: 3, dashboardId: 5, @@ -30,14 +30,14 @@ describe('graphiteDatasource', () => { let requestOptions; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = function(options) { + ctx.backendSrv.datasourceRequest = options => { requestOptions = options; return ctx.$q.when({ data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }], }); }; - await ctx.ds.query(query).then(function(data) { + await ctx.ds.query(query).then(data => { results = data; }); }); @@ -47,15 +47,15 @@ describe('graphiteDatasource', () => { expect(requestOptions.headers['X-Panel-Id']).toBe(3); }); - it('should generate the correct query', function() { + it('should generate the correct query', () => { expect(requestOptions.url).toBe('/api/datasources/proxy/1/render'); }); - it('should set unique requestId', function() { + it('should set unique requestId', () => { expect(requestOptions.requestId).toBe('graphiteProd.panelId.3'); }); - it('should query correctly', function() { + it('should query correctly', () => { const params = requestOptions.data.split('&'); expect(params).toContain('target=prod1.count'); expect(params).toContain('target=prod2.count'); @@ -63,17 +63,17 @@ describe('graphiteDatasource', () => { expect(params).toContain('until=now'); }); - it('should exclude undefined params', function() { + it('should exclude undefined params', () => { const params = requestOptions.data.split('&'); expect(params).not.toContain('cacheTimeout=undefined'); }); - it('should return series list', function() { + it('should return series list', () => { expect(results.data.length).toBe(1); expect(results.data[0].target).toBe('prod1.count'); }); - it('should convert to millisecond resolution', function() { + it('should convert to millisecond resolution', () => { expect(results.data[0].datapoints[0][0]).toBe(10); }); }); @@ -106,11 +106,11 @@ describe('graphiteDatasource', () => { }; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = function(options) { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when(response); }; - await ctx.ds.annotationQuery(options).then(function(data) { + await ctx.ds.annotationQuery(options).then(data => { results = data; }); }); @@ -136,11 +136,11 @@ describe('graphiteDatasource', () => { ], }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = function(options) { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when(response); }; - ctx.ds.annotationQuery(options).then(function(data) { + ctx.ds.annotationQuery(options).then(data => { results = data; }); // ctx.$rootScope.$apply(); @@ -155,29 +155,29 @@ describe('graphiteDatasource', () => { }); }); - describe('building graphite params', function() { - it('should return empty array if no targets', function() { + describe('building graphite params', () => { + it('should return empty array if no targets', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{}], }); expect(results.length).toBe(0); }); - it('should uri escape targets', function() { + it('should uri escape targets', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }], }); expect(results).toContain('target=prod1.%7Btest%2Ctest2%7D'); }); - it('should replace target placeholder', function() { + it('should replace target placeholder', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }], }); expect(results[2]).toBe('target=asPercent(series1%2Cseries2)'); }); - it('should replace target placeholder for hidden series', function() { + it('should replace target placeholder for hidden series', () => { const results = ctx.ds.buildGraphiteParams({ targets: [ { target: 'series1', hide: true }, @@ -188,28 +188,28 @@ describe('graphiteDatasource', () => { expect(results[0]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); - it('should replace target placeholder when nesting query references', function() { + it('should replace target placeholder when nesting query references', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }], }); expect(results[2]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); - it('should fix wrong minute interval parameters', function() { + it('should fix wrong minute interval parameters', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }], }); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')")); }); - it('should fix wrong month interval parameters', function() { + it('should fix wrong month interval parameters', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }], }); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')")); }); - it('should ignore empty targets', function() { + it('should ignore empty targets', () => { const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: '' }], }); @@ -222,7 +222,7 @@ describe('graphiteDatasource', () => { let requestOptions; beforeEach(() => { - ctx.backendSrv.datasourceRequest = function(options) { + ctx.backendSrv.datasourceRequest = options => { requestOptions = options; return ctx.$q.when({ data: ['backend_01', 'backend_02'], @@ -307,7 +307,7 @@ describe('graphiteDatasource', () => { }); function accessScenario(name, url, fn) { - describe('access scenario ' + name, function() { + describe('access scenario ' + name, () => { const ctx: any = { backendSrv: {}, $q: $q, @@ -332,12 +332,12 @@ function accessScenario(name, url, fn) { }); } -accessScenario('with proxy access', '/api/datasources/proxy/1', function(httpOptions) { +accessScenario('with proxy access', '/api/datasources/proxy/1', httpOptions => { expect(httpOptions.headers['X-Dashboard-Id']).toBe(1); expect(httpOptions.headers['X-Panel-Id']).toBe(2); }); -accessScenario('with direct access', 'http://localhost:8080', function(httpOptions) { +accessScenario('with direct access', 'http://localhost:8080', httpOptions => { expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined); expect(httpOptions.headers['X-Panel-Id']).toBe(undefined); }); diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.test.ts b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts index 61a0e896b0f..1809adc0940 100644 --- a/public/app/plugins/datasource/graphite/specs/gfunc.test.ts +++ b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts @@ -1,7 +1,7 @@ import gfunc from '../gfunc'; -describe('when creating func instance from func names', function() { - it('should return func instance', function() { +describe('when creating func instance from func names', () => { + it('should return func instance', () => { const func = gfunc.createFuncInstance('sumSeries'); expect(func).toBeTruthy(); expect(func.def.name).toEqual('sumSeries'); @@ -10,18 +10,18 @@ describe('when creating func instance from func names', function() { expect(func.def.defaultParams.length).toEqual(1); }); - it('should return func instance with shortName', function() { + it('should return func instance with shortName', () => { const func = gfunc.createFuncInstance('sum'); expect(func).toBeTruthy(); }); - it('should return func instance from funcDef', function() { + it('should return func instance from funcDef', () => { const func = gfunc.createFuncInstance('sum'); const func2 = gfunc.createFuncInstance(func.def); expect(func2).toBeTruthy(); }); - it('func instance should have text representation', function() { + it('func instance should have text representation', () => { const func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; @@ -30,78 +30,78 @@ describe('when creating func instance from func names', function() { }); }); -describe('when rendering func instance', function() { - it('should handle single metric param', function() { +describe('when rendering func instance', () => { + it('should handle single metric param', () => { const func = gfunc.createFuncInstance('sumSeries'); expect(func.render('hello.metric')).toEqual('sumSeries(hello.metric)'); }); - it('should include default params if options enable it', function() { + it('should include default params if options enable it', () => { const func = gfunc.createFuncInstance('scaleToSeconds', { withDefaultParams: true, }); expect(func.render('hello')).toEqual('scaleToSeconds(hello, 1)'); }); - it('should handle int or interval params with number', function() { + it('should handle int or interval params with number', () => { const func = gfunc.createFuncInstance('movingMedian'); func.params[0] = '5'; expect(func.render('hello')).toEqual('movingMedian(hello, 5)'); }); - it('should handle int or interval params with interval string', function() { + it('should handle int or interval params with interval string', () => { const func = gfunc.createFuncInstance('movingMedian'); func.params[0] = '5min'; expect(func.render('hello')).toEqual("movingMedian(hello, '5min')"); }); - it('should never quote boolean paramater', function() { + it('should never quote boolean paramater', () => { const func = gfunc.createFuncInstance('sortByName'); func.params[0] = '$natural'; expect(func.render('hello')).toEqual('sortByName(hello, $natural)'); }); - it('should never quote int paramater', function() { + it('should never quote int paramater', () => { const func = gfunc.createFuncInstance('maximumAbove'); func.params[0] = '$value'; expect(func.render('hello')).toEqual('maximumAbove(hello, $value)'); }); - it('should never quote node paramater', function() { + it('should never quote node paramater', () => { const func = gfunc.createFuncInstance('aliasByNode'); func.params[0] = '$node'; expect(func.render('hello')).toEqual('aliasByNode(hello, $node)'); }); - it('should handle metric param and int param and string param', function() { + it('should handle metric param and int param and string param', () => { const func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; expect(func.render('hello.metric')).toEqual("groupByNode(hello.metric, 5, 'avg')"); }); - it('should handle function with no metric param', function() { + it('should handle function with no metric param', () => { const func = gfunc.createFuncInstance('randomWalk'); func.params[0] = 'test'; expect(func.render(undefined)).toEqual("randomWalk('test')"); }); - it('should handle function multiple series params', function() { + it('should handle function multiple series params', () => { const func = gfunc.createFuncInstance('asPercent'); func.params[0] = '#B'; expect(func.render('#A')).toEqual('asPercent(#A, #B)'); }); }); -describe('when requesting function definitions', function() { - it('should return function definitions', function() { +describe('when requesting function definitions', () => { + it('should return function definitions', () => { const funcIndex = gfunc.getFuncDefs('1.0'); expect(Object.keys(funcIndex).length).toBeGreaterThan(8); }); }); -describe('when updating func param', function() { - it('should update param value and update text representation', function() { +describe('when updating func param', () => { + it('should update param value and update text representation', () => { const func = gfunc.createFuncInstance('summarize', { withDefaultParams: true, }); @@ -110,21 +110,21 @@ describe('when updating func param', function() { expect(func.text).toBe('summarize(1h, sum, false)'); }); - it('should parse numbers as float', function() { + it('should parse numbers as float', () => { const func = gfunc.createFuncInstance('scale'); func.updateParam('0.001', 0); expect(func.params[0]).toBe('0.001'); }); }); -describe('when updating func param with optional second parameter', function() { - it('should update value and text', function() { +describe('when updating func param with optional second parameter', () => { + it('should update value and text', () => { const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('1', 0); expect(func.params[0]).toBe('1'); }); - it('should slit text and put value in second param', function() { + it('should slit text and put value in second param', () => { const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); expect(func.params[0]).toBe('4'); @@ -132,7 +132,7 @@ describe('when updating func param with optional second parameter', function() { expect(func.text).toBe('aliasByNode(4, -5)'); }); - it('should remove second param when empty string is set', function() { + it('should remove second param when empty string is set', () => { const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); func.updateParam('', 1); diff --git a/public/app/plugins/datasource/graphite/specs/lexer.test.ts b/public/app/plugins/datasource/graphite/specs/lexer.test.ts index f00df17a725..4bfe7217bfa 100644 --- a/public/app/plugins/datasource/graphite/specs/lexer.test.ts +++ b/public/app/plugins/datasource/graphite/specs/lexer.test.ts @@ -1,7 +1,7 @@ import { Lexer } from '../lexer'; -describe('when lexing graphite expression', function() { - it('should tokenize metric expression', function() { +describe('when lexing graphite expression', () => { + it('should tokenize metric expression', () => { const lexer = new Lexer('metric.test.*.asd.count'); const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('metric'); @@ -11,27 +11,27 @@ describe('when lexing graphite expression', function() { expect(tokens[4].pos).toBe(13); }); - it('should tokenize metric expression with dash', function() { + it('should tokenize metric expression with dash', () => { const lexer = new Lexer('metric.test.se1-server-*.asd.count'); const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('identifier'); expect(tokens[4].value).toBe('se1-server-*'); }); - it('should tokenize metric expression with dash2', function() { + it('should tokenize metric expression with dash2', () => { const lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*'); const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('net'); expect(tokens[2].value).toBe('192-168-1-1'); }); - it('should tokenize metric expression with equal sign', function() { + it('should tokenize metric expression with equal sign', () => { const lexer = new Lexer('apps=test'); const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('apps=test'); }); - it('simple function2', function() { + it('simple function2', () => { const lexer = new Lexer('offset(test.metric, -100)'); const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); @@ -39,7 +39,7 @@ describe('when lexing graphite expression', function() { expect(tokens[6].type).toBe('number'); }); - it('should tokenize metric expression with curly braces', function() { + it('should tokenize metric expression with curly braces', () => { const lexer = new Lexer('metric.se1-{first, second}.count'); const tokens = lexer.tokenize(); expect(tokens.length).toBe(10); @@ -49,7 +49,7 @@ describe('when lexing graphite expression', function() { expect(tokens[6].value).toBe('second'); }); - it('should tokenize metric expression with number segments', function() { + it('should tokenize metric expression with number segments', () => { const lexer = new Lexer('metric.10.12_10.test'); const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); @@ -59,7 +59,7 @@ describe('when lexing graphite expression', function() { expect(tokens[4].type).toBe('identifier'); }); - it('should tokenize metric expression with segment that start with number', function() { + it('should tokenize metric expression with segment that start with number', () => { const lexer = new Lexer('metric.001-server'); const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); @@ -67,7 +67,7 @@ describe('when lexing graphite expression', function() { expect(tokens.length).toBe(3); }); - it('should tokenize func call with numbered metric and number arg', function() { + it('should tokenize func call with numbered metric and number arg', () => { const lexer = new Lexer('scale(metric.10, 15)'); const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); @@ -78,7 +78,7 @@ describe('when lexing graphite expression', function() { expect(tokens[6].type).toBe('number'); }); - it('should tokenize metric with template parameter', function() { + it('should tokenize metric with template parameter', () => { const lexer = new Lexer('metric.[[server]].test'); const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); @@ -86,7 +86,7 @@ describe('when lexing graphite expression', function() { expect(tokens[4].type).toBe('identifier'); }); - it('should tokenize metric with question mark', function() { + it('should tokenize metric with question mark', () => { const lexer = new Lexer('metric.server_??.test'); const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); @@ -94,7 +94,7 @@ describe('when lexing graphite expression', function() { expect(tokens[4].type).toBe('identifier'); }); - it('should handle error with unterminated string', function() { + it('should handle error with unterminated string', () => { const lexer = new Lexer("alias(metric, 'asd)"); const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('alias'); @@ -106,14 +106,14 @@ describe('when lexing graphite expression', function() { expect(tokens[4].pos).toBe(20); }); - it('should handle float parameters', function() { + it('should handle float parameters', () => { const lexer = new Lexer('alias(metric, 0.002)'); const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('number'); expect(tokens[4].value).toBe('0.002'); }); - it('should handle bool parameters', function() { + it('should handle bool parameters', () => { const lexer = new Lexer('alias(metric, true, false)'); const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('bool'); diff --git a/public/app/plugins/datasource/graphite/specs/parser.test.ts b/public/app/plugins/datasource/graphite/specs/parser.test.ts index 966eb213d64..25cabd5d20c 100644 --- a/public/app/plugins/datasource/graphite/specs/parser.test.ts +++ b/public/app/plugins/datasource/graphite/specs/parser.test.ts @@ -1,7 +1,7 @@ import { Parser } from '../parser'; -describe('when parsing', function() { - it('simple metric expression', function() { +describe('when parsing', () => { + it('simple metric expression', () => { const parser = new Parser('metric.test.*.asd.count'); const rootNode = parser.getAst(); @@ -10,7 +10,7 @@ describe('when parsing', function() { expect(rootNode.segments[0].value).toBe('metric'); }); - it('simple metric expression with numbers in segments', function() { + it('simple metric expression with numbers in segments', () => { const parser = new Parser('metric.10.15_20.5'); const rootNode = parser.getAst(); @@ -21,7 +21,7 @@ describe('when parsing', function() { expect(rootNode.segments[3].value).toBe('5'); }); - it('simple metric expression with curly braces', function() { + it('simple metric expression with curly braces', () => { const parser = new Parser('metric.se1-{count, max}'); const rootNode = parser.getAst(); @@ -30,7 +30,7 @@ describe('when parsing', function() { expect(rootNode.segments[1].value).toBe('se1-{count,max}'); }); - it('simple metric expression with curly braces at start of segment and with post chars', function() { + it('simple metric expression with curly braces at start of segment and with post chars', () => { const parser = new Parser('metric.{count, max}-something.count'); const rootNode = parser.getAst(); @@ -39,14 +39,14 @@ describe('when parsing', function() { expect(rootNode.segments[1].value).toBe('{count,max}-something'); }); - it('simple function', function() { + it('simple function', () => { const parser = new Parser('sum(test)'); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); }); - it('simple function2', function() { + it('simple function2', () => { const parser = new Parser('offset(test.metric, -100)'); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); @@ -54,7 +54,7 @@ describe('when parsing', function() { expect(rootNode.params[1].type).toBe('number'); }); - it('simple function with string arg', function() { + it('simple function with string arg', () => { const parser = new Parser("randomWalk('test')"); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); @@ -62,7 +62,7 @@ describe('when parsing', function() { expect(rootNode.params[0].type).toBe('string'); }); - it('function with multiple args', function() { + it('function with multiple args', () => { const parser = new Parser("sum(test, 1, 'test')"); const rootNode = parser.getAst(); @@ -73,7 +73,7 @@ describe('when parsing', function() { expect(rootNode.params[2].type).toBe('string'); }); - it('function with nested function', function() { + it('function with nested function', () => { const parser = new Parser('sum(scaleToSeconds(test, 1))'); const rootNode = parser.getAst(); @@ -86,7 +86,7 @@ describe('when parsing', function() { expect(rootNode.params[0].params[1].type).toBe('number'); }); - it('function with multiple series', function() { + it('function with multiple series', () => { const parser = new Parser('sum(test.test.*.count, test.timers.*.count)'); const rootNode = parser.getAst(); @@ -96,7 +96,7 @@ describe('when parsing', function() { expect(rootNode.params[1].type).toBe('metric'); }); - it('function with templated series', function() { + it('function with templated series', () => { const parser = new Parser('sum(test.[[server]].count)'); const rootNode = parser.getAst(); @@ -106,7 +106,7 @@ describe('when parsing', function() { expect(rootNode.params[0].segments[1].value).toBe('[[server]]'); }); - it('invalid metric expression', function() { + it('invalid metric expression', () => { const parser = new Parser('metric.test.*.asd.'); const rootNode = parser.getAst(); @@ -114,7 +114,7 @@ describe('when parsing', function() { expect(rootNode.pos).toBe(19); }); - it('invalid function expression missing closing parenthesis', function() { + it('invalid function expression missing closing parenthesis', () => { const parser = new Parser('sum(test'); const rootNode = parser.getAst(); @@ -122,7 +122,7 @@ describe('when parsing', function() { expect(rootNode.pos).toBe(9); }); - it('unclosed string in function', function() { + it('unclosed string in function', () => { const parser = new Parser("sum('test)"); const rootNode = parser.getAst(); @@ -130,13 +130,13 @@ describe('when parsing', function() { expect(rootNode.pos).toBe(11); }); - it('handle issue #69', function() { + it('handle issue #69', () => { const parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))'); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); }); - it('handle float function arguments', function() { + it('handle float function arguments', () => { const parser = new Parser('scale(test, 0.002)'); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); @@ -144,7 +144,7 @@ describe('when parsing', function() { expect(rootNode.params[1].value).toBe(0.002); }); - it('handle curly brace pattern at start', function() { + it('handle curly brace pattern at start', () => { const parser = new Parser('{apps}.test'); const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); @@ -152,7 +152,7 @@ describe('when parsing', function() { expect(rootNode.segments[1].value).toBe('test'); }); - it('series parameters', function() { + it('series parameters', () => { const parser = new Parser('asPercent(#A, #B)'); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); @@ -161,7 +161,7 @@ describe('when parsing', function() { expect(rootNode.params[1].value).toBe('#B'); }); - it('series parameters, issue 2788', function() { + it('series parameters, issue 2788', () => { const parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)"); const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); @@ -170,7 +170,7 @@ describe('when parsing', function() { expect(rootNode.params[3].type).toBe('bool'); }); - it('should parse metric expression with ip number segments', function() { + it('should parse metric expression with ip number segments', () => { const parser = new Parser('5.10.123.5'); const rootNode = parser.getAst(); expect(rootNode.segments[0].value).toBe('5'); diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts index 6a07e44252d..13ac2a48223 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts @@ -137,7 +137,7 @@ describe('GraphiteQueryCtrl', () => { ctx.ctrl.target.target = 'test.count'; ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]); ctx.ctrl.parseTarget(); - ctx.ctrl.getAltSegments(1).then(function(results) { + ctx.ctrl.getAltSegments(1).then(results => { ctx.altSegments = results; }); }); diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts index 60f49bd4905..62049535e3e 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts @@ -10,7 +10,7 @@ describe('InfluxDataSource', () => { instanceSettings: { url: 'url', name: 'influxDb', jsonData: {} }, }; - beforeEach(function() { + beforeEach(() => { ctx.instanceSettings.url = '/api/datasources/proxy/1'; ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); }); @@ -26,7 +26,7 @@ describe('InfluxDataSource', () => { let requestQuery; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = function(req) { + ctx.backendSrv.datasourceRequest = req => { requestQuery = req.params.q; return ctx.$q.when({ results: [ @@ -43,7 +43,7 @@ describe('InfluxDataSource', () => { }); }; - await ctx.ds.metricFindQuery(query, queryOptions).then(function(_) {}); + await ctx.ds.metricFindQuery(query, queryOptions).then(_ => {}); }); it('should replace $timefilter', () => { diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts index a62d5384ac6..f8e65c21f2d 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts @@ -1,10 +1,10 @@ import InfluxQuery from '../influx_query'; -describe('InfluxQuery', function() { +describe('InfluxQuery', () => { const templateSrv = { replace: val => val }; - describe('render series with mesurement only', function() { - it('should generate correct query', function() { + describe('render series with mesurement only', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -18,8 +18,8 @@ describe('InfluxQuery', function() { }); }); - describe('render series with policy only', function() { - it('should generate correct query', function() { + describe('render series with policy only', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -36,8 +36,8 @@ describe('InfluxQuery', function() { }); }); - describe('render series with math and alias', function() { - it('should generate correct query', function() { + describe('render series with math and alias', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -61,8 +61,8 @@ describe('InfluxQuery', function() { }); }); - describe('series with single tag only', function() { - it('should generate correct query', function() { + describe('series with single tag only', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -81,7 +81,7 @@ describe('InfluxQuery', function() { ); }); - it('should switch regex operator with tag value is regex', function() { + it('should switch regex operator with tag value is regex', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -99,8 +99,8 @@ describe('InfluxQuery', function() { }); }); - describe('series with multiple tags only', function() { - it('should generate correct query', function() { + describe('series with multiple tags only', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -119,8 +119,8 @@ describe('InfluxQuery', function() { }); }); - describe('series with tags OR condition', function() { - it('should generate correct query', function() { + describe('series with tags OR condition', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -139,8 +139,8 @@ describe('InfluxQuery', function() { }); }); - describe('query with value condition', function() { - it('should not quote value', function() { + describe('query with value condition', () => { + it('should not quote value', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -156,8 +156,8 @@ describe('InfluxQuery', function() { }); }); - describe('series with groupByTag', function() { - it('should generate correct query', function() { + describe('series with groupByTag', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -173,8 +173,8 @@ describe('InfluxQuery', function() { }); }); - describe('render series without group by', function() { - it('should generate correct query', function() { + describe('render series without group by', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -189,8 +189,8 @@ describe('InfluxQuery', function() { }); }); - describe('render series without group by and fill', function() { - it('should generate correct query', function() { + describe('render series without group by and fill', () => { + it('should generate correct query', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -205,8 +205,8 @@ describe('InfluxQuery', function() { }); }); - describe('when adding group by part', function() { - it('should add tag before fill', function() { + describe('when adding group by part', () => { + it('should add tag before fill', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -223,7 +223,7 @@ describe('InfluxQuery', function() { expect(query.target.groupBy[2].type).toBe('fill'); }); - it('should add tag last if no fill', function() { + it('should add tag last if no fill', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -239,8 +239,8 @@ describe('InfluxQuery', function() { }); }); - describe('when adding select part', function() { - it('should add mean after after field', function() { + describe('when adding select part', () => { + it('should add mean after after field', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -255,7 +255,7 @@ describe('InfluxQuery', function() { expect(query.target.select[0][1].type).toBe('mean'); }); - it('should replace sum by mean', function() { + it('should replace sum by mean', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -270,7 +270,7 @@ describe('InfluxQuery', function() { expect(query.target.select[0][1].type).toBe('sum'); }); - it('should add math before alias', function() { + it('should add math before alias', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -285,7 +285,7 @@ describe('InfluxQuery', function() { expect(query.target.select[0][2].type).toBe('math'); }); - it('should add math last', function() { + it('should add math last', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -300,7 +300,7 @@ describe('InfluxQuery', function() { expect(query.target.select[0][2].type).toBe('math'); }); - it('should replace math', function() { + it('should replace math', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -315,7 +315,7 @@ describe('InfluxQuery', function() { expect(query.target.select[0][2].type).toBe('math'); }); - it('should add math when one only query part', function() { + it('should add math when one only query part', () => { const query = new InfluxQuery( { measurement: 'cpu', @@ -330,8 +330,8 @@ describe('InfluxQuery', function() { expect(query.target.select[0][1].type).toBe('math'); }); - describe('when render adhoc filters', function() { - it('should generate correct query segment', function() { + describe('when render adhoc filters', () => { + it('should generate correct query segment', () => { const query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {}); const queryText = query.renderAdhocFilters([ diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts index bb20db1ba76..44232173e27 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts @@ -1,7 +1,7 @@ import InfluxSeries from '../influx_series'; -describe('when generating timeseries from influxdb response', function() { - describe('given multiple fields for series', function() { +describe('when generating timeseries from influxdb response', () => { + describe('given multiple fields for series', () => { const options = { alias: '', series: [ @@ -13,8 +13,8 @@ describe('when generating timeseries from influxdb response', function() { }, ], }; - describe('and no alias', function() { - it('should generate multiple datapoints for each column', function() { + describe('and no alias', () => { + it('should generate multiple datapoints for each column', () => { const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -39,8 +39,8 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('and simple alias', function() { - it('should use alias', function() { + describe('and simple alias', () => { + it('should use alias', () => { options.alias = 'new series'; const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -51,8 +51,8 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('and alias patterns', function() { - it('should replace patterns', function() { + describe('and alias patterns', () => { + it('should replace patterns', () => { options.alias = 'alias: $m -> $tag_server ([[measurement]])'; const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -64,7 +64,7 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given measurement with default fieldname', function() { + describe('given measurement with default fieldname', () => { const options = { series: [ { @@ -82,8 +82,8 @@ describe('when generating timeseries from influxdb response', function() { ], }; - describe('and no alias', function() { - it('should generate label with no field', function() { + describe('and no alias', () => { + it('should generate label with no field', () => { const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -93,7 +93,7 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given two series', function() { + describe('given two series', () => { const options = { alias: '', series: [ @@ -112,8 +112,8 @@ describe('when generating timeseries from influxdb response', function() { ], }; - describe('and no alias', function() { - it('should generate two time series', function() { + describe('and no alias', () => { + it('should generate two time series', () => { const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -132,8 +132,8 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('and simple alias', function() { - it('should use alias', function() { + describe('and simple alias', () => { + it('should use alias', () => { options.alias = 'new series'; const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -142,8 +142,8 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('and alias patterns', function() { - it('should replace patterns', function() { + describe('and alias patterns', () => { + it('should replace patterns', () => { options.alias = 'alias: $m -> $tag_server ([[measurement]])'; const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -154,7 +154,7 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given measurement with dots', function() { + describe('given measurement with dots', () => { const options = { alias: '', series: [ @@ -167,7 +167,7 @@ describe('when generating timeseries from influxdb response', function() { ], }; - it('should replace patterns', function() { + it('should replace patterns', () => { options.alias = 'alias: $1 -> [[3]]'; const series = new InfluxSeries(options); const result = series.getTimeSeries(); @@ -176,7 +176,7 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given table response', function() { + describe('given table response', () => { const options = { alias: '', series: [ @@ -189,7 +189,7 @@ describe('when generating timeseries from influxdb response', function() { ], }; - it('should return table', function() { + it('should return table', () => { const series = new InfluxSeries(options); const table = series.getTable(); @@ -200,7 +200,7 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given table response from SHOW CARDINALITY', function() { + describe('given table response from SHOW CARDINALITY', () => { const options = { alias: '', series: [ @@ -212,7 +212,7 @@ describe('when generating timeseries from influxdb response', function() { ], }; - it('should return table', function() { + it('should return table', () => { const series = new InfluxSeries(options); const table = series.getTable(); @@ -223,8 +223,8 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given annotation response', function() { - describe('with empty tagsColumn', function() { + describe('given annotation response', () => { + describe('with empty tagsColumn', () => { const options = { alias: '', annotation: {}, @@ -238,7 +238,7 @@ describe('when generating timeseries from influxdb response', function() { ], }; - it('should multiple tags', function() { + it('should multiple tags', () => { const series = new InfluxSeries(options); const annotations = series.getAnnotations(); @@ -246,7 +246,7 @@ describe('when generating timeseries from influxdb response', function() { }); }); - describe('given annotation response', function() { + describe('given annotation response', () => { const options = { alias: '', annotation: { @@ -262,7 +262,7 @@ describe('when generating timeseries from influxdb response', function() { ], }; - it('should multiple tags', function() { + it('should multiple tags', () => { const series = new InfluxSeries(options); const annotations = series.getAnnotations(); diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts index d8b27f8b1bf..e21b95ac374 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts @@ -1,14 +1,14 @@ import { InfluxQueryBuilder } from '../query_builder'; -describe('InfluxQueryBuilder', function() { - describe('when building explore queries', function() { - it('should only have measurement condition in tag keys query given query with measurement', function() { +describe('InfluxQueryBuilder', () => { + describe('when building explore queries', () => { + it('should only have measurement condition in tag keys query given query with measurement', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }); const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS FROM "cpu"'); }); - it('should handle regex measurement in tag keys query', function() { + it('should handle regex measurement in tag keys query', () => { const builder = new InfluxQueryBuilder({ measurement: '/.*/', tags: [], @@ -17,13 +17,13 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG KEYS FROM /.*/'); }); - it('should have no conditions in tags keys query given query with no measurement or tag', function() { + it('should have no conditions in tags keys query given query with no measurement or tag', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS'); }); - it('should have where condition in tag keys query with tags', function() { + it('should have where condition in tag keys query with tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'host', value: 'se1' }], @@ -32,25 +32,25 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\''); }); - it('should have no conditions in measurement query for query with no tags', function() { + it('should have no conditions in measurement query for query with no tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); const query = builder.buildExploreQuery('MEASUREMENTS'); expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); }); - it('should have no conditions in measurement query for query with no tags and empty query', function() { + it('should have no conditions in measurement query for query with no tags and empty query', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); const query = builder.buildExploreQuery('MEASUREMENTS', undefined, ''); expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); }); - it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', function() { + it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ LIMIT 100'); }); - it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', function() { + it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'email' }], @@ -59,7 +59,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ WHERE "app" = \'email\' LIMIT 100'); }); - it('should have where condition in measurement query for query with tags', function() { + it('should have where condition in measurement query for query with tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'email' }], @@ -68,7 +68,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW MEASUREMENTS WHERE "app" = \'email\' LIMIT 100'); }); - it('should have where tag name IN filter in tag values query for query with one tag', function() { + it('should have where tag name IN filter in tag values query for query with one tag', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'asdsadsad' }], @@ -77,7 +77,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG VALUES WITH KEY = "app"'); }); - it('should have measurement tag condition and tag name IN filter in tag values query', function() { + it('should have measurement tag condition and tag name IN filter in tag values query', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'app', value: 'email' }, { key: 'host', value: 'server1' }], @@ -86,7 +86,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\''); }); - it('should select from policy correctly if policy is specified', function() { + it('should select from policy correctly if policy is specified', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', policy: 'one_week', @@ -96,7 +96,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\''); }); - it('should not include policy when policy is default', function() { + it('should not include policy when policy is default', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', policy: 'default', @@ -106,7 +106,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app"'); }); - it('should switch to regex operator in tag condition', function() { + it('should switch to regex operator in tag condition', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'host', value: '/server.*/' }], @@ -115,7 +115,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); }); - it('should build show field query', function() { + it('should build show field query', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'app', value: 'email' }], @@ -124,7 +124,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW FIELD KEYS FROM "cpu"'); }); - it('should build show field query with regexp', function() { + it('should build show field query with regexp', () => { const builder = new InfluxQueryBuilder({ measurement: '/$var/', tags: [{ key: 'app', value: 'email' }], @@ -133,7 +133,7 @@ describe('InfluxQueryBuilder', function() { expect(query).toBe('SHOW FIELD KEYS FROM /$var/'); }); - it('should build show retention policies query', function() { + it('should build show retention policies query', () => { const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site'); const query = builder.buildExploreQuery('RETENTION POLICIES'); expect(query).toBe('SHOW RETENTION POLICIES on "site"'); diff --git a/public/app/plugins/datasource/mssql/specs/datasource.test.ts b/public/app/plugins/datasource/mssql/specs/datasource.test.ts index 0308717775b..0dd496bfe59 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.test.ts @@ -4,20 +4,20 @@ import { TemplateSrvStub } from 'test/specs/helpers'; import { CustomVariable } from 'app/features/templating/custom_variable'; import q from 'q'; -describe('MSSQLDatasource', function() { +describe('MSSQLDatasource', () => { const ctx: any = { backendSrv: {}, templateSrv: new TemplateSrvStub(), }; - beforeEach(function() { + beforeEach(() => { ctx.$q = q; ctx.instanceSettings = { name: 'mssql' }; ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv); }); - describe('When performing annotationQuery', function() { + describe('When performing annotationQuery', () => { let results; const annotationName = 'MyAnno'; @@ -61,7 +61,7 @@ describe('MSSQLDatasource', function() { }); }); - it('should return annotation list', function() { + it('should return annotation list', () => { expect(results.length).toBe(3); expect(results[0].text).toBe('some text'); @@ -75,7 +75,7 @@ describe('MSSQLDatasource', function() { }); }); - describe('When performing metricFindQuery', function() { + describe('When performing metricFindQuery', () => { let results; const query = 'select * from atable'; const response = { @@ -95,24 +95,24 @@ describe('MSSQLDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { + beforeEach(() => { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when({ data: response, status: 200 }); }; - return ctx.ds.metricFindQuery(query).then(function(data) { + return ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of all column values', function() { + it('should return list of all column values', () => { expect(results.length).toBe(6); expect(results[0].text).toBe('aTitle'); expect(results[5].text).toBe('some text3'); }); }); - describe('When performing metricFindQuery with key, value columns', function() { + describe('When performing metricFindQuery with key, value columns', () => { let results; const query = 'select * from atable'; const response = { @@ -132,17 +132,17 @@ describe('MSSQLDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { + beforeEach(() => { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when({ data: response, status: 200 }); }; - return ctx.ds.metricFindQuery(query).then(function(data) { + return ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of as text, value', function() { + it('should return list of as text, value', () => { expect(results.length).toBe(3); expect(results[0].text).toBe('aTitle'); expect(results[0].value).toBe('value1'); @@ -151,7 +151,7 @@ describe('MSSQLDatasource', function() { }); }); - describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() { + describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => { let results; const query = 'select * from atable'; const response = { @@ -171,17 +171,17 @@ describe('MSSQLDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { + beforeEach(() => { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when({ data: response, status: 200 }); }; - return ctx.ds.metricFindQuery(query).then(function(data) { + return ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of unique keys', function() { + it('should return list of unique keys', () => { expect(results.length).toBe(1); expect(results[0].text).toBe('aTitle'); expect(results[0].value).toBe('same'); @@ -189,7 +189,7 @@ describe('MSSQLDatasource', function() { }); describe('When interpolating variables', () => { - beforeEach(function() { + beforeEach(() => { ctx.variable = new CustomVariable({}, {}); }); diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index 0990be6f21c..2cd9b189ec0 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -2,7 +2,7 @@ import moment from 'moment'; import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; -describe('MySQLDatasource', function() { +describe('MySQLDatasource', () => { const instanceSettings = { name: 'mysql' }; const backendSrv = {}; const templateSrv = { @@ -17,7 +17,7 @@ describe('MySQLDatasource', function() { ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv); }); - describe('When performing annotationQuery', function() { + describe('When performing annotationQuery', () => { let results; const annotationName = 'MyAnno'; @@ -51,16 +51,16 @@ describe('MySQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.annotationQuery(options).then(function(data) { + ctx.ds.annotationQuery(options).then(data => { results = data; }); }); - it('should return annotation list', function() { + it('should return annotation list', () => { expect(results.length).toBe(3); expect(results[0].text).toBe('some text'); @@ -74,7 +74,7 @@ describe('MySQLDatasource', function() { }); }); - describe('When performing metricFindQuery', function() { + describe('When performing metricFindQuery', () => { let results; const query = 'select * from atable'; const response = { @@ -94,23 +94,23 @@ describe('MySQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.metricFindQuery(query).then(function(data) { + ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of all column values', function() { + it('should return list of all column values', () => { expect(results.length).toBe(6); expect(results[0].text).toBe('aTitle'); expect(results[5].text).toBe('some text3'); }); }); - describe('When performing metricFindQuery with key, value columns', function() { + describe('When performing metricFindQuery with key, value columns', () => { let results; const query = 'select * from atable'; const response = { @@ -130,16 +130,16 @@ describe('MySQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.metricFindQuery(query).then(function(data) { + ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of as text, value', function() { + it('should return list of as text, value', () => { expect(results.length).toBe(3); expect(results[0].text).toBe('aTitle'); expect(results[0].value).toBe('value1'); @@ -148,7 +148,7 @@ describe('MySQLDatasource', function() { }); }); - describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() { + describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => { let results; const query = 'select * from atable'; const response = { @@ -168,16 +168,16 @@ describe('MySQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.metricFindQuery(query).then(function(data) { + ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of unique keys', function() { + it('should return list of unique keys', () => { expect(results.length).toBe(1); expect(results[0].text).toBe('aTitle'); expect(results[0].value).toBe('same'); @@ -185,7 +185,7 @@ describe('MySQLDatasource', function() { }); describe('When interpolating variables', () => { - beforeEach(function() { + beforeEach(() => { ctx.variable = new CustomVariable({}, {}); }); diff --git a/public/app/plugins/datasource/postgres/specs/datasource.test.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts index 2c0c0554250..8ee687543cc 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.test.ts @@ -2,7 +2,7 @@ import moment from 'moment'; import { PostgresDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; -describe('PostgreSQLDatasource', function() { +describe('PostgreSQLDatasource', () => { const instanceSettings = { name: 'postgresql' }; const backendSrv = {}; @@ -17,7 +17,7 @@ describe('PostgreSQLDatasource', function() { ctx.ds = new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv); }); - describe('When performing annotationQuery', function() { + describe('When performing annotationQuery', () => { let results; const annotationName = 'MyAnno'; @@ -51,16 +51,16 @@ describe('PostgreSQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.annotationQuery(options).then(function(data) { + ctx.ds.annotationQuery(options).then(data => { results = data; }); }); - it('should return annotation list', function() { + it('should return annotation list', () => { expect(results.length).toBe(3); expect(results[0].text).toBe('some text'); @@ -74,7 +74,7 @@ describe('PostgreSQLDatasource', function() { }); }); - describe('When performing metricFindQuery', function() { + describe('When performing metricFindQuery', () => { let results; const query = 'select * from atable'; const response = { @@ -94,23 +94,23 @@ describe('PostgreSQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.metricFindQuery(query).then(function(data) { + ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of all column values', function() { + it('should return list of all column values', () => { expect(results.length).toBe(6); expect(results[0].text).toBe('aTitle'); expect(results[5].text).toBe('some text3'); }); }); - describe('When performing metricFindQuery with key, value columns', function() { + describe('When performing metricFindQuery with key, value columns', () => { let results; const query = 'select * from atable'; const response = { @@ -130,16 +130,16 @@ describe('PostgreSQLDatasource', function() { }, }; - beforeEach(function() { + beforeEach(() => { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.metricFindQuery(query).then(function(data) { + ctx.ds.metricFindQuery(query).then(data => { results = data; }); }); - it('should return list of as text, value', function() { + it('should return list of as text, value', () => { expect(results.length).toBe(3); expect(results[0].text).toBe('aTitle'); expect(results[0].value).toBe('value1'); @@ -148,7 +148,7 @@ describe('PostgreSQLDatasource', function() { }); }); - describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() { + describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => { let results; const query = 'select * from atable'; const response = { @@ -172,13 +172,13 @@ describe('PostgreSQLDatasource', function() { ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); - ctx.ds.metricFindQuery(query).then(function(data) { + ctx.ds.metricFindQuery(query).then(data => { results = data; }); //ctx.$rootScope.$apply(); }); - it('should return list of unique keys', function() { + it('should return list of unique keys', () => { expect(results.length).toBe(1); expect(results[0].text).toBe('aTitle'); expect(results[0].value).toBe('same'); @@ -186,7 +186,7 @@ describe('PostgreSQLDatasource', function() { }); describe('When interpolating variables', () => { - beforeEach(function() { + beforeEach(() => { ctx.variable = new CustomVariable({}, {}); }); diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 05952e1dc4a..36765ef708b 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 getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 35d13684240..064f1bc1818 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -437,7 +437,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { + await ctx.ds.query(query).then(data => { results = data; }); }); @@ -487,7 +487,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { + await ctx.ds.query(query).then(data => { results = data; }); }); @@ -548,7 +548,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { + await ctx.ds.query(query).then(data => { results = data; }); }); @@ -603,7 +603,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); 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; }); }); @@ -642,7 +642,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { + await ctx.ds.query(query).then(data => { results = data; }); }); @@ -1156,7 +1156,7 @@ describe('PrometheusDatasource for POST', () => { }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { + await ctx.ds.query(query).then(data => { results = data; }); }); diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts index bfbf241ba06..1466bd8ac96 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts @@ -3,7 +3,7 @@ import { PrometheusDatasource } from '../datasource'; import PrometheusMetricFindQuery from '../metric_find_query'; import q from 'q'; -describe('PrometheusMetricFindQuery', function() { +describe('PrometheusMetricFindQuery', () => { const instanceSettings = { url: 'proxied', directUrl: 'direct', diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.test.ts b/public/app/plugins/panel/graph/specs/align_yaxes.test.ts index da3aff91275..d87de91fdd5 100644 --- a/public/app/plugins/panel/graph/specs/align_yaxes.test.ts +++ b/public/app/plugins/panel/graph/specs/align_yaxes.test.ts @@ -1,6 +1,6 @@ import { alignYLevel } from '../align_yaxes'; -describe('Graph Y axes aligner', function() { +describe('Graph Y axes aligner', () => { let yaxes, expected; let alignY = 0; diff --git a/public/app/plugins/panel/graph/specs/data_processor.test.ts b/public/app/plugins/panel/graph/specs/data_processor.test.ts index 2e8d4eb6eab..b919bb0f3b1 100644 --- a/public/app/plugins/panel/graph/specs/data_processor.test.ts +++ b/public/app/plugins/panel/graph/specs/data_processor.test.ts @@ -1,6 +1,6 @@ import { DataProcessor } from '../data_processor'; -describe('Graph DataProcessor', function() { +describe('Graph DataProcessor', () => { const panel: any = { xaxis: {}, }; diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index 037a02a5bf0..d86e860b27d 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -1,5 +1,5 @@ jest.mock('app/features/annotations/all', () => ({ - EventManager: function() { + EventManager: () => { return { on: () => {}, addFlotEvents: () => {}, @@ -40,7 +40,7 @@ const scope = { }; let link; -describe('grafanaGraph', function() { +describe('grafanaGraph', () => { const setupCtx = (beforeRender?) => { config.bootData = { user: { @@ -242,7 +242,7 @@ describe('grafanaGraph', function() { }); }); - it('should apply axis transform, autoscaling (if necessary) and ticks', function() { + it('should apply axis transform, autoscaling (if necessary) and ticks', () => { const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); @@ -277,7 +277,7 @@ describe('grafanaGraph', function() { }); }); - it('should not set min and max and should create some fake ticks', function() { + it('should not set min and max and should create some fake ticks', () => { const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); @@ -303,7 +303,7 @@ describe('grafanaGraph', function() { ctx.data[0].yaxis = 1; }); }); - it('should set min to 0.1 and add a tick for 0.1', function() { + it('should set min to 0.1 and add a tick for 0.1', () => { const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); @@ -330,7 +330,7 @@ describe('grafanaGraph', function() { }); }); - it('should regenerate ticks so that if fits on the y-axis', function() { + it('should regenerate ticks so that if fits on the y-axis', () => { const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.min).toBe(0.1); expect(axisAutoscale.ticks.length).toBe(8); @@ -339,7 +339,7 @@ describe('grafanaGraph', function() { expect(axisAutoscale.max).toBe(262144); }); - it('should set axis max to be max tick value', function() { + it('should set axis max to be max tick value', () => { expect(ctx.plotOptions.yaxes[0].max).toBe(262144); }); }); @@ -353,7 +353,7 @@ describe('grafanaGraph', function() { }); }); - it('should configure dashed plot with correct options', function() { + it('should configure dashed plot with correct options', () => { expect(ctx.plotOptions.series.lines.show).toBe(true); expect(ctx.plotOptions.series.dashes.lineWidth).toBe(2); expect(ctx.plotOptions.series.dashes.show).toBe(true); @@ -371,7 +371,7 @@ describe('grafanaGraph', function() { }); }); - it('should set barWidth', function() { + it('should set barWidth', () => { expect(ctx.plotOptions.series.bars.barWidth).toBe(1 / 1.5); }); }); @@ -388,7 +388,7 @@ describe('grafanaGraph', function() { }); }); - it('should match second series and fill zero, and enable points', function() { + it('should match second series and fill zero, and enable points', () => { expect(ctx.plotOptions.series.lines.fill).toBe(0.5); expect(ctx.plotData[1].lines.fill).toBe(0.001); expect(ctx.plotData[1].points.show).toBe(true); @@ -403,7 +403,7 @@ describe('grafanaGraph', function() { }); }); - it('should move zindex 2 last', function() { + it('should move zindex 2 last', () => { expect(ctx.plotData[0].alias).toBe('series2'); expect(ctx.plotData[1].alias).toBe('series1'); }); @@ -416,7 +416,7 @@ describe('grafanaGraph', function() { }); }); - it('should remove datapoints and disable stack', function() { + it('should remove datapoints and disable stack', () => { expect(ctx.plotData[0].alias).toBe('series1'); expect(ctx.plotData[1].data.length).toBe(0); expect(ctx.plotData[1].stack).toBe(false); @@ -431,7 +431,7 @@ describe('grafanaGraph', function() { }); }); - it('should show percentage', function() { + it('should show percentage', () => { const axis = ctx.plotOptions.yaxes[0]; expect(axis.tickFormatter(100, axis)).toBe('100%'); }); @@ -439,7 +439,7 @@ describe('grafanaGraph', function() { describe('when panel too narrow to show x-axis dates in same granularity as wide panels', () => { //Set width to 10px - describe('and the range is less than 24 hours', function() { + describe('and the range is less than 24 hours', () => { beforeEach(() => { setupCtx(() => { ctrl.range.from = moment([2015, 1, 1, 10]); @@ -447,13 +447,13 @@ describe('grafanaGraph', function() { }); }); - it('should format dates as hours minutes', function() { + it('should format dates as hours minutes', () => { const axis = ctx.plotOptions.xaxis; expect(axis.timeformat).toBe('%H:%M'); }); }); - describe('and the range is less than one year', function() { + describe('and the range is less than one year', () => { beforeEach(() => { setupCtx(() => { ctrl.range.from = moment([2015, 1, 1]); @@ -461,7 +461,7 @@ describe('grafanaGraph', function() { }); }); - it('should format dates as month days', function() { + it('should format dates as month days', () => { const axis = ctx.plotOptions.xaxis; expect(axis.timeformat).toBe('%m/%d'); }); @@ -485,7 +485,7 @@ describe('grafanaGraph', function() { }); }); - it('should calculate correct histogram', function() { + it('should calculate correct histogram', () => { expect(ctx.plotData[0].data[0][0]).toBe(100); expect(ctx.plotData[0].data[0][1]).toBe(2); expect(ctx.plotData[1].data[0][0]).toBe(100); @@ -510,7 +510,7 @@ describe('grafanaGraph', function() { }); }); - it('should calculate correct histogram', function() { + it('should calculate correct histogram', () => { expect(ctx.plotData[0].data[0][0]).toBe(100); expect(ctx.plotData[0].data[0][1]).toBe(2); }); diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts index ecc6ce0fb21..00a3cf0dcf1 100644 --- a/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts @@ -24,12 +24,12 @@ function describeSharedTooltip(desc, fn) { stack: false, }; - ctx.setup = function(setupFn) { + ctx.setup = setupFn => { ctx.setupFn = setupFn; }; - describe(desc, function() { - beforeEach(function() { + describe(desc, () => { + beforeEach(() => { ctx.setupFn(); const tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos); @@ -39,35 +39,35 @@ function describeSharedTooltip(desc, fn) { }); } -describe('findHoverIndexFromData', function() { +describe('findHoverIndexFromData', () => { const tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); const series = { data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]], }; - it('should return 0 if posX out of lower bounds', function() { + it('should return 0 if posX out of lower bounds', () => { const posX = 99; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(0); }); - it('should return n - 1 if posX out of upper bounds', function() { + it('should return n - 1 if posX out of upper bounds', () => { const posX = 108; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(series.data.length - 1); }); - it('should return i if posX in series', function() { + it('should return i if posX in series', () => { const posX = 104; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4); }); - it('should return i if posX not in series and i + 1 > posX', function() { + it('should return i if posX not in series and i + 1 > posX', () => { const posX = 104.9; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4); }); }); -describeSharedTooltip('steppedLine false, stack false', function(ctx) { - ctx.setup(function() { +describeSharedTooltip('steppedLine false, stack false', ctx => { + ctx.setup(() => { ctx.data = [ { data: [[10, 15], [12, 20]], lines: {}, hideTooltip: false }, { data: [[10, 2], [12, 3]], lines: {}, hideTooltip: false }, @@ -75,30 +75,30 @@ describeSharedTooltip('steppedLine false, stack false', function(ctx) { ctx.pos = { x: 11 }; }); - it('should return 2 series', function() { + it('should return 2 series', () => { expect(ctx.results.length).toBe(2); }); - it('should add time to results array', function() { + it('should add time to results array', () => { expect(ctx.results.time).toBe(10); }); - it('should set value and hoverIndex', function() { + it('should set value and hoverIndex', () => { expect(ctx.results[0].value).toBe(15); expect(ctx.results[1].value).toBe(2); expect(ctx.results[0].hoverIndex).toBe(0); }); }); -describeSharedTooltip('one series is hidden', function(ctx) { - ctx.setup(function() { +describeSharedTooltip('one series is hidden', ctx => { + ctx.setup(() => { ctx.data = [{ data: [[10, 15], [12, 20]] }, { data: [] }]; ctx.pos = { x: 11 }; }); }); -describeSharedTooltip('steppedLine false, stack true, individual false', function(ctx) { - ctx.setup(function() { +describeSharedTooltip('steppedLine false, stack true, individual false', ctx => { + ctx.setup(() => { ctx.data = [ { data: [[10, 15], [12, 20]], @@ -125,13 +125,13 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio ctx.pos = { x: 11 }; }); - it('should show stacked value', function() { + it('should show stacked value', () => { expect(ctx.results[1].value).toBe(17); }); }); -describeSharedTooltip('steppedLine false, stack true, individual false, series stack false', function(ctx) { - ctx.setup(function() { +describeSharedTooltip('steppedLine false, stack true, individual false, series stack false', ctx => { + ctx.setup(() => { ctx.data = [ { data: [[10, 15], [12, 20]], @@ -158,13 +158,13 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s ctx.pos = { x: 11 }; }); - it('should not show stacked value', function() { + it('should not show stacked value', () => { expect(ctx.results[1].value).toBe(2); }); }); -describeSharedTooltip('steppedLine false, stack true, individual true', function(ctx) { - ctx.setup(function() { +describeSharedTooltip('steppedLine false, stack true, individual true', ctx => { + ctx.setup(() => { ctx.data = [ { data: [[10, 15], [12, 20]], @@ -192,7 +192,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function ctx.pos = { x: 11 }; }); - it('should not show stacked value', function() { + it('should not show stacked value', () => { expect(ctx.results[1].value).toBe(2); }); }); diff --git a/public/app/plugins/panel/graph/specs/histogram.test.ts b/public/app/plugins/panel/graph/specs/histogram.test.ts index adbc0fcba68..d6b8527910b 100644 --- a/public/app/plugins/panel/graph/specs/histogram.test.ts +++ b/public/app/plugins/panel/graph/specs/histogram.test.ts @@ -1,6 +1,6 @@ import { convertValuesToHistogram, getSeriesValues } from '../histogram'; -describe('Graph Histogam Converter', function() { +describe('Graph Histogam Converter', () => { describe('Values to histogram converter', () => { let values; let bucketSize = 10; diff --git a/public/app/plugins/panel/graph/specs/threshold_manager.test.ts b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts index ecbc382923e..bbeaf1b993a 100644 --- a/public/app/plugins/panel/graph/specs/threshold_manager.test.ts +++ b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts @@ -2,9 +2,9 @@ import angular from 'angular'; import TimeSeries from 'app/core/time_series2'; import { ThresholdManager } from '../threshold_manager'; -describe('ThresholdManager', function() { +describe('ThresholdManager', () => { function plotOptionsScenario(desc, func) { - describe(desc, function() { + describe(desc, () => { const ctx: any = { panel: { thresholds: [], @@ -15,7 +15,7 @@ describe('ThresholdManager', function() { panelCtrl: {}, }; - ctx.setup = function(thresholds, data) { + ctx.setup = (thresholds, data) => { ctx.panel.thresholds = thresholds; const manager = new ThresholdManager(ctx.panelCtrl); if (data !== undefined) { @@ -33,7 +33,7 @@ describe('ThresholdManager', function() { plotOptionsScenario('for simple gt threshold', ctx => { ctx.setup([{ op: 'gt', value: 300, fill: true, line: true, colorMode: 'critical' }]); - it('should add fill for threshold with fill: true', function() { + it('should add fill for threshold with fill: true', () => { const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); @@ -41,7 +41,7 @@ describe('ThresholdManager', function() { expect(markings[0].color).toBe('rgba(234, 112, 112, 0.12)'); }); - it('should add line', function() { + it('should add line', () => { const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(300); expect(markings[1].yaxis.to).toBe(300); @@ -55,13 +55,13 @@ describe('ThresholdManager', function() { { op: 'gt', value: 300, fill: true, colorMode: 'critical' }, ]); - it('should add fill for first thresholds to next threshold', function() { + it('should add fill for first thresholds to next threshold', () => { const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(200); expect(markings[0].yaxis.to).toBe(300); }); - it('should add fill for last thresholds to infinity', function() { + it('should add fill for last thresholds to infinity', () => { const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(300); expect(markings[1].yaxis.to).toBe(Infinity); @@ -74,13 +74,13 @@ describe('ThresholdManager', function() { { op: 'gt', value: 200, fill: true, colorMode: 'critical' }, ]); - it('should add fill for first thresholds to next threshold', function() { + it('should add fill for first thresholds to next threshold', () => { const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(200); }); - it('should add fill for last thresholds to itself', function() { + it('should add fill for last thresholds to itself', () => { const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(200); expect(markings[1].yaxis.to).toBe(200); @@ -93,13 +93,13 @@ describe('ThresholdManager', function() { { op: 'lt', value: 200, fill: true, colorMode: 'critical' }, ]); - it('should add fill for first thresholds to next threshold', function() { + it('should add fill for first thresholds to next threshold', () => { const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(Infinity); }); - it('should add fill for last thresholds to itself', function() { + it('should add fill for last thresholds to itself', () => { const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(200); expect(markings[1].yaxis.to).toBe(-Infinity); @@ -126,12 +126,12 @@ describe('ThresholdManager', function() { data ); - it('should add first threshold for left axis', function() { + it('should add first threshold for left axis', () => { const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(100); }); - it('should add second threshold for right axis', function() { + it('should add second threshold for right axis', () => { const markings = ctx.options.grid.markings; expect(markings[1].y2axis.from).toBe(200); }); diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts index 740d4045013..a51be8eb723 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts @@ -1,7 +1,7 @@ import moment from 'moment'; import { HeatmapCtrl } from '../heatmap_ctrl'; -describe('HeatmapCtrl', function() { +describe('HeatmapCtrl', () => { const ctx = {} as any; const $injector = { @@ -23,8 +23,8 @@ describe('HeatmapCtrl', function() { ctx.ctrl = new HeatmapCtrl($scope, $injector, {}); }); - describe('when time series are outside range', function() { - beforeEach(function() { + describe('when time series are outside range', () => { + beforeEach(() => { const data = [ { target: 'test.cpu1', @@ -36,13 +36,13 @@ describe('HeatmapCtrl', function() { ctx.ctrl.onDataReceived(data); }); - it('should set datapointsOutside', function() { + it('should set datapointsOutside', () => { expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range'); }); }); - describe('when time series are inside range', function() { - beforeEach(function() { + describe('when time series are inside range', () => { + beforeEach(() => { const range = { from: moment() .subtract(1, 'days') @@ -61,18 +61,18 @@ describe('HeatmapCtrl', function() { ctx.ctrl.onDataReceived(data); }); - it('should set datapointsOutside', function() { + it('should set datapointsOutside', () => { expect(ctx.ctrl.dataWarning).toBe(null); }); }); - describe('datapointsCount given 2 series', function() { - beforeEach(function() { + describe('datapointsCount given 2 series', () => { + beforeEach(() => { const data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; ctx.ctrl.onDataReceived(data); }); - it('should set datapointsCount warning', function() { + it('should set datapointsCount warning', () => { expect(ctx.ctrl.dataWarning.title).toBe('No data points'); }); }); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index bacc972c058..6003acd89a6 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -1,7 +1,7 @@ import { SingleStatCtrl } from '../module'; import moment from 'moment'; -describe('SingleStatCtrl', function() { +describe('SingleStatCtrl', () => { const ctx = {} as any; const epoch = 1505826363746; Date.now = () => epoch; @@ -28,9 +28,9 @@ describe('SingleStatCtrl', function() { }; function singleStatScenario(desc, func) { - describe(desc, function() { - ctx.setup = function(setupFunc) { - beforeEach(function() { + describe(desc, () => { + ctx.setup = setupFunc => { + beforeEach(() => { ctx.ctrl = new SingleStatCtrl($scope, $injector, {}); setupFunc(); ctx.ctrl.onDataReceived(ctx.data); @@ -42,191 +42,189 @@ describe('SingleStatCtrl', function() { }); } - singleStatScenario('with defaults', function(ctx) { - ctx.setup(function() { + singleStatScenario('with defaults', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; }); - it('Should use series avg as default main value', function() { + it('Should use series avg as default main value', () => { expect(ctx.data.value).toBe(15); expect(ctx.data.valueRounded).toBe(15); }); - it('should set formatted falue', function() { + it('should set formatted falue', () => { expect(ctx.data.valueFormatted).toBe('15'); }); }); - singleStatScenario('showing serie name instead of value', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing serie name instead of value', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; ctx.ctrl.panel.valueName = 'name'; }); - it('Should use series avg as default main value', function() { + it('Should use series avg as default main value', () => { expect(ctx.data.value).toBe(0); expect(ctx.data.valueRounded).toBe(0); }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe('test.cpu1'); }); }); - singleStatScenario('showing last iso time instead of value', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing last iso time instead of value', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsIso'; ctx.ctrl.dashboard.isTimezoneUtc = () => false; }); - it('Should use time instead of value', function() { + it('Should use time instead of value', () => { expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.valueRounded).toBe(1505634997920); }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(moment(ctx.data.valueFormatted).valueOf()).toBe(1505634997000); }); }); - singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing last iso time instead of value (in UTC)', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsIso'; ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); - it('should set value', function() { + it('should set value', () => { expect(ctx.data.valueFormatted).toBe('1970-01-01 00:00:05'); }); }); - singleStatScenario('showing last us time instead of value', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing last us time instead of value', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsUS'; ctx.ctrl.dashboard.isTimezoneUtc = () => false; }); - it('Should use time instead of value', function() { + it('Should use time instead of value', () => { expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.valueRounded).toBe(1505634997920); }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a')); }); }); - singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing last us time instead of value (in UTC)', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsUS'; ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe('01/01/1970 12:00:05 am'); }); }); - singleStatScenario('showing last time from now instead of value', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing last time from now instead of value', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeFromNow'; }); - it('Should use time instead of value', function() { + it('Should use time instead of value', () => { expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.valueRounded).toBe(1505634997920); }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe('2 days ago'); }); }); - singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) { - ctx.setup(function() { + singleStatScenario('showing last time from now instead of value (in UTC)', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeFromNow'; }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe('2 days ago'); }); }); - singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( - ctx - ) { - ctx.setup(function() { + singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }]; ctx.ctrl.panel.valueName = 'avg'; ctx.ctrl.panel.format = 'none'; }); - it('Should be rounded', function() { + it('Should be rounded', () => { expect(ctx.data.value).toBe(99.999495); expect(ctx.data.valueRounded).toBe(100); }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe('100'); }); }); - singleStatScenario('When value to text mapping is specified', function(ctx) { - ctx.setup(function() { + singleStatScenario('When value to text mapping is specified', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }]; ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; }); - it('value should remain', function() { + it('value should remain', () => { expect(ctx.data.value).toBe(9.9); }); - it('round should be rounded up', function() { + it('round should be rounded up', () => { expect(ctx.data.valueRounded).toBe(10); }); - it('Should replace value with text', function() { + it('Should replace value with text', () => { expect(ctx.data.valueFormatted).toBe('OK'); }); }); - singleStatScenario('When range to text mapping is specified for first range', function(ctx) { - ctx.setup(function() { + singleStatScenario('When range to text mapping is specified for first range', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }]; ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; }); - it('Should replace value with text OK', function() { + it('Should replace value with text OK', () => { expect(ctx.data.valueFormatted).toBe('OK'); }); }); - singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { - ctx.setup(function() { + singleStatScenario('When range to text mapping is specified for other ranges', ctx => { + ctx.setup(() => { ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }]; ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; }); - it('Should replace value with text NOT OK', function() { + it('Should replace value with text NOT OK', () => { expect(ctx.data.valueFormatted).toBe('NOT OK'); }); }); - describe('When table data', function() { + describe('When table data', () => { const tableData = [ { columns: [{ text: 'Time', type: 'time' }, { text: 'test1' }, { text: 'mean' }, { text: 'test2' }], @@ -235,8 +233,8 @@ describe('SingleStatCtrl', function() { }, ]; - singleStatScenario('with default values', function(ctx) { - ctx.setup(function() { + singleStatScenario('with default values', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.ctrl.panel = { emit: () => {}, @@ -245,49 +243,47 @@ describe('SingleStatCtrl', function() { ctx.ctrl.panel.format = 'none'; }); - it('Should use first rows value as default main value', function() { + it('Should use first rows value as default main value', () => { expect(ctx.data.value).toBe(15); expect(ctx.data.valueRounded).toBe(15); }); - it('should set formatted value', function() { + it('should set formatted value', () => { expect(ctx.data.valueFormatted).toBe('15'); }); }); - singleStatScenario('When table data has multiple columns', function(ctx) { - ctx.setup(function() { + singleStatScenario('When table data has multiple columns', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.ctrl.panel.tableColumn = ''; }); - it('Should set column to first column that is not time', function() { + it('Should set column to first column that is not time', () => { expect(ctx.ctrl.panel.tableColumn).toBe('test1'); }); }); - singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( - ctx - ) { - ctx.setup(function() { + singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; ctx.ctrl.panel.mappingType = 0; ctx.ctrl.panel.tableColumn = 'mean'; }); - it('Should be rounded', function() { + it('Should be rounded', () => { expect(ctx.data.value).toBe(99.99999); expect(ctx.data.valueRounded).toBe(100); }); - it('should set formatted falue', function() { + it('should set formatted falue', () => { expect(ctx.data.valueFormatted).toBe('100'); }); }); - singleStatScenario('When value to text mapping is specified', function(ctx) { - ctx.setup(function() { + singleStatScenario('When value to text mapping is specified', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2']; ctx.ctrl.panel.mappingType = 2; @@ -295,21 +291,21 @@ describe('SingleStatCtrl', function() { ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; }); - it('value should remain', function() { + it('value should remain', () => { expect(ctx.data.value).toBe(9.9); }); - it('round should be rounded up', function() { + it('round should be rounded up', () => { expect(ctx.data.valueRounded).toBe(10); }); - it('Should replace value with text', function() { + it('Should replace value with text', () => { expect(ctx.data.valueFormatted).toBe('OK'); }); }); - singleStatScenario('When range to text mapping is specified for first range', function(ctx) { - ctx.setup(function() { + singleStatScenario('When range to text mapping is specified for first range', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2']; ctx.ctrl.panel.tableColumn = 'mean'; @@ -317,13 +313,13 @@ describe('SingleStatCtrl', function() { ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; }); - it('Should replace value with text OK', function() { + it('Should replace value with text OK', () => { expect(ctx.data.valueFormatted).toBe('OK'); }); }); - singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { - ctx.setup(function() { + singleStatScenario('When range to text mapping is specified for other ranges', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; ctx.ctrl.panel.tableColumn = 'mean'; @@ -331,31 +327,31 @@ describe('SingleStatCtrl', function() { ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; }); - it('Should replace value with text NOT OK', function() { + it('Should replace value with text NOT OK', () => { expect(ctx.data.valueFormatted).toBe('NOT OK'); }); }); - singleStatScenario('When value is string', function(ctx) { - ctx.setup(function() { + singleStatScenario('When value is string', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; ctx.ctrl.panel.tableColumn = 'test1'; }); - it('Should replace value with text NOT OK', function() { + it('Should replace value with text NOT OK', () => { expect(ctx.data.valueFormatted).toBe('ignore1'); }); }); - singleStatScenario('When value is zero', function(ctx) { - ctx.setup(function() { + singleStatScenario('When value is zero', ctx => { + ctx.setup(() => { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2']; ctx.ctrl.panel.tableColumn = 'mean'; }); - it('Should return zero', function() { + it('Should return zero', () => { expect(ctx.data.value).toBe(0); }); }); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts index 114cdf132e1..1a88c6bb770 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts @@ -1,6 +1,6 @@ import { getColorForValue } from '../module'; -describe('grafanaSingleStat', function() { +describe('grafanaSingleStat', () => { describe('legacy thresholds', () => { describe('positive thresholds', () => { const data: any = { diff --git a/public/app/plugins/panel/table/specs/renderer.test.ts b/public/app/plugins/panel/table/specs/renderer.test.ts index b66984ba223..f29c69e4acd 100644 --- a/public/app/plugins/panel/table/specs/renderer.test.ts +++ b/public/app/plugins/panel/table/specs/renderer.test.ts @@ -163,15 +163,15 @@ describe('when rendering table', () => { ], }; - const sanitize = function(value) { + const sanitize = value => { return 'sanitized'; }; const templateSrv = { - replace: function(value, scopedVars) { + replace: (value, scopedVars) => { if (scopedVars) { // For testing variables replacement in link - _.each(scopedVars, function(val, key) { + _.each(scopedVars, (val, key) => { value = value.replace('$' + key, val.value); }); } diff --git a/public/app/plugins/panel/table/specs/transformers.test.ts b/public/app/plugins/panel/table/specs/transformers.test.ts index 2425d98f26d..8d581b68842 100644 --- a/public/app/plugins/panel/table/specs/transformers.test.ts +++ b/public/app/plugins/panel/table/specs/transformers.test.ts @@ -161,15 +161,15 @@ describe('when transforming time series table', () => { }, ]; - describe('getColumns', function() { - it('should return data columns given a single query', function() { + describe('getColumns', () => { + it('should return data columns given a single query', () => { const columns = transformers[transform].getColumns(singleQueryData); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Value'); }); - it('should return the union of data columns given a multiple queries', function() { + it('should return the union of data columns given a multiple queries', () => { const columns = transformers[transform].getColumns(multipleQueriesDataSameLabels); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); @@ -178,7 +178,7 @@ describe('when transforming time series table', () => { expect(columns[4].text).toBe('Value #B'); }); - it('should return the union of data columns given a multiple queries with different labels', function() { + it('should return the union of data columns given a multiple queries with different labels', () => { const columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); @@ -189,7 +189,7 @@ describe('when transforming time series table', () => { }); }); - describe('transform', function() { + describe('transform', () => { it('should throw an error with non-table data', () => { expect(() => transformDataToTable(nonTableData, panel)).toThrow(); }); @@ -286,8 +286,8 @@ describe('when transforming time series table', () => { }, ]; - describe('getColumns', function() { - it('should return nested properties', function() { + describe('getColumns', () => { + it('should return nested properties', () => { const columns = transformers['json'].getColumns(rawData); expect(columns[0].text).toBe('timestamp'); expect(columns[1].text).toBe('message'); @@ -295,7 +295,7 @@ describe('when transforming time series table', () => { }); }); - describe('transform', function() { + describe('transform', () => { beforeEach(() => { table = transformDataToTable(rawData, panel); }); diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index f585498d247..0345576d96c 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -15,10 +15,10 @@ export function ControllerTestContext(this: any) { this.timeSrv = new TimeSrvStub(); this.templateSrv = new TemplateSrvStub(); this.datasourceSrv = { - getMetricSources: function() {}, - get: function() { + getMetricSources: () => {}, + get: () => { return { - then: function(callback) { + then: callback => { callback(self.datasource); }, }; @@ -26,8 +26,8 @@ export function ControllerTestContext(this: any) { }; this.isUtc = false; - this.providePhase = function(mocks) { - return angularMocks.module(function($provide) { + this.providePhase = mocks => { + return angularMocks.module($provide => { $provide.value('contextSrv', self.contextSrv); $provide.value('datasourceSrv', self.datasourceSrv); $provide.value('annotationsSrv', self.annotationsSrv); @@ -35,14 +35,14 @@ export function ControllerTestContext(this: any) { $provide.value('templateSrv', self.templateSrv); $provide.value('$element', self.$element); $provide.value('$sanitize', self.$sanitize); - _.each(mocks, function(value, key) { + _.each(mocks, (value, key) => { $provide.value(key, value); }); }); }; - this.createPanelController = function(Ctrl) { - return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) { + this.createPanelController = Ctrl => { + return angularMocks.inject(($controller, $rootScope, $q, $location, $browser) => { self.scope = $rootScope.$new(); self.$location = $location; self.$browser = $browser; @@ -50,7 +50,7 @@ export function ControllerTestContext(this: any) { self.panel = new PanelModel({ type: 'test' }); self.dashboard = { meta: {} }; self.isUtc = false; - self.dashboard.isTimezoneUtc = function() { + self.dashboard.isTimezoneUtc = () => { return self.isUtc; }; @@ -74,8 +74,8 @@ export function ControllerTestContext(this: any) { }); }; - this.createControllerPhase = function(controllerName) { - return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) { + this.createControllerPhase = controllerName => { + return angularMocks.inject(($controller, $rootScope, $q, $location, $browser) => { self.scope = $rootScope.$new(); self.$location = $location; self.$browser = $browser; @@ -101,7 +101,7 @@ export function ControllerTestContext(this: any) { }); }; - this.setIsUtc = function(isUtc = false) { + this.setIsUtc = (isUtc = false) => { self.isUtc = isUtc; }; } @@ -114,23 +114,23 @@ export function ServiceTestContext(this: any) { self.backendSrv = {}; self.$routeParams = {}; - this.providePhase = function(mocks) { - return angularMocks.module(function($provide) { - _.each(mocks, function(key) { + this.providePhase = mocks => { + return angularMocks.module($provide => { + _.each(mocks, key => { $provide.value(key, self[key]); }); }); }; - this.createService = function(name) { - return angularMocks.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) { + this.createService = name => { + return angularMocks.inject(($q, $rootScope, $httpBackend, $injector, $location, $timeout) => { self.$q = $q; self.$rootScope = $rootScope; self.$httpBackend = $httpBackend; self.$location = $location; - self.$rootScope.onAppEvent = function() {}; - self.$rootScope.appEvent = function() {}; + self.$rootScope.onAppEvent = () => {}; + self.$rootScope.appEvent = () => {}; self.$timeout = $timeout; self.service = $injector.get(name); @@ -139,7 +139,7 @@ export function ServiceTestContext(this: any) { } export function DashboardViewStateStub(this: any) { - this.registerPanel = function() {}; + this.registerPanel = () => {}; } export function TimeSrvStub(this: any) { @@ -155,7 +155,7 @@ export function TimeSrvStub(this: any) { }; }; - this.replace = function(target) { + this.replace = target => { return target; }; @@ -165,7 +165,7 @@ export function TimeSrvStub(this: any) { } export function ContextSrvStub(this: any) { - this.hasRole = function() { + this.hasRole = () => { return true; }; } @@ -177,17 +177,17 @@ export function TemplateSrvStub(this: any) { this.replace = function(text) { return _.template(text, this.templateSettings)(this.data); }; - this.init = function() {}; - this.getAdhocFilters = function() { + this.init = () => {}; + this.getAdhocFilters = () => { return []; }; - this.fillVariableValuesForUrl = function() {}; - this.updateTemplateData = function() {}; - this.variableExists = function() { + this.fillVariableValuesForUrl = () => {}; + this.updateTemplateData = () => {}; + this.variableExists = () => { return false; }; - this.variableInitialized = function() {}; - this.highlightVariablesAsHtml = function(str) { + this.variableInitialized = () => {}; + this.highlightVariablesAsHtml = str => { return str; }; this.setGrafanaVariable = function(name, value) { From 5ac5a08e9e9b21ac711bcb2095fa9d10afb80119 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 4 Sep 2018 09:53:24 +0200 Subject: [PATCH 0084/2611] 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 b891a858ca0934fbec5fd54b64d55d2763bf6f80 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 4 Sep 2018 12:49:13 +0300 Subject: [PATCH 0085/2611] graph legend: implement series toggling and sorting --- public/app/plugins/panel/graph/Legend.tsx | 88 +++++++++++++++++++---- public/app/plugins/panel/graph/graph.ts | 8 ++- public/app/plugins/panel/graph/module.ts | 6 ++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index becb52d1aeb..362ca238e80 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -7,6 +7,8 @@ const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; interface LegendProps { seriesList: TimeSeries[]; optionalClass?: string; + onToggleSeries?: (series: TimeSeries, event: Event) => void; + onToggleSort?: (sortBy, sortDesc) => void; } interface LegendDisplayProps { @@ -70,11 +72,18 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; @@ -87,14 +96,19 @@ export class GraphLegend extends React.PureComponent this.onToggleSeries(s, e), + onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), + ...seriesValuesProps, + ...sortProps, + }; + return (
    - {this.props.alignAsTable ? ( - - ) : ( - - )} + {this.props.alignAsTable ? : }
    ); @@ -106,7 +120,14 @@ class LegendSeriesList extends React.PureComponent { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; return seriesList.map((series, i) => ( - + this.props.onToggleSeries(series, e)} + /> )); } } @@ -114,6 +135,7 @@ class LegendSeriesList extends React.PureComponent { interface LegendSeriesProps { series: TimeSeries; index: number; + onLabelClick?: (event) => void; } type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; @@ -125,7 +147,11 @@ class LegendSeriesItem extends React.PureComponent { const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; return (
    - + this.props.onLabelClick(e)} + /> {valueItems}
    ); @@ -135,16 +161,18 @@ class LegendSeriesItem extends React.PureComponent { interface LegendSeriesLabelProps { label: string; color: string; + onLabelClick?: (event) => void; + onIconClick?: (event) => void; } class LegendSeriesLabel extends React.PureComponent { render() { const { label, color } = this.props; return [ -
    +
    this.props.onIconClick(e)}>
    , - + this.props.onLabelClick(e)}> {label} , ]; @@ -180,6 +208,24 @@ function renderLegendValues(props: LegendSeriesItemProps, series, asTable = fals } class LegendTable extends React.PureComponent> { + onToggleSort(stat) { + let sortDesc = this.props.sortDesc; + let sortBy = this.props.sort; + if (stat !== sortBy) { + sortDesc = null; + } + + // if already sort ascending, disable sorting + if (sortDesc === false) { + sortBy = null; + sortDesc = null; + } else { + sortDesc = !sortDesc; + sortBy = stat; + } + this.props.onToggleSort(sortBy, sortDesc); + } + render() { const seriesList = this.props.seriesList; const { values, min, max, avg, current, total, sort, sortDesc } = this.props; @@ -192,7 +238,13 @@ class LegendTable extends React.PureComponent> { {LEGEND_STATS.map( statName => seriesValuesProps[statName] && ( - + this.onToggleSort(statName)} + /> ) )}
    @@ -203,6 +255,7 @@ class LegendTable extends React.PureComponent> { index={i} hiddenSeries={this.props.hiddenSeries} {...seriesValuesProps} + onLabelClick={e => this.props.onToggleSeries(series, e)} /> ))} @@ -213,12 +266,13 @@ class LegendTable extends React.PureComponent> { interface LegendTableHeaderProps { statName: string; + onClick?: (event) => void; } -function LegendTableHeader(props: LegendTableHeaderProps & LegendSortProps) { +function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) { const { statName, sort, sortDesc } = props; return ( - @@ -233,7 +287,11 @@ class LegendSeriesItemAsTable extends React.PureComponent return ( {valueItems} @@ -246,7 +304,7 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { if (series.yaxis === 2) { classes.push('graph-legend-series--right-y'); } - if (hiddenSeries[series.alias]) { + if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { classes.push('graph-legend-series-hidden'); } return classes.join(' '); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index a1066295048..2a6962d78e7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -86,16 +86,18 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); - console.log(this.ctrl); + // console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; - const { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero } = this.panel.legend; - const legendOptions = { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero }; + const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; const valueOptions = { values, min, max, avg, current, total }; const legendProps: GraphLegendProps = { seriesList: this.data, hiddenSeries: this.ctrl.hiddenSeries, ...legendOptions, ...valueOptions, + onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), + onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), }; const legendReactElem = React.createElement(GraphLegend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 6467f4e816a..a83417f6e2a 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -287,6 +287,12 @@ class GraphCtrl extends MetricsPanelCtrl { } } + toggleSort(sortBy, sortDesc) { + this.panel.legend.sort = sortBy; + this.panel.legend.sortDesc = sortDesc; + this.render(); + } + toggleAxis(info) { var override = _.find(this.panel.seriesOverrides, { alias: info.alias }); if (!override) { From 19b7ad61dd36e7c90415974a07ba07070a8d2616 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 4 Sep 2018 14:27:03 +0200 Subject: [PATCH 0086/2611] Changed functions to arrow functions for only-arrow-functions rule. --- public/app/core/components/grafana_app.ts | 25 +++--- public/app/core/components/info_popover.ts | 6 +- .../layout_selector/layout_selector.ts | 2 +- public/app/core/components/navbar/navbar.ts | 2 +- .../query_part/query_part_editor.ts | 20 ++--- .../app/core/components/sidemenu/sidemenu.ts | 6 +- public/app/core/controllers/inspect_ctrl.ts | 4 +- .../app/core/controllers/json_editor_ctrl.ts | 2 +- public/app/core/directives/array_join.ts | 2 +- .../annotations/annotation_tooltip.ts | 2 +- public/app/features/panellinks/link_srv.ts | 4 +- public/app/features/plugins/datasource_srv.ts | 6 +- public/app/features/plugins/plugin_loader.ts | 2 +- .../cloudwatch/query_parameter_ctrl.ts | 48 +++++------ .../datasource/elasticsearch/bucket_agg.ts | 36 ++++----- .../datasource/elasticsearch/datasource.ts | 20 ++--- .../elasticsearch/elastic_response.ts | 2 +- .../datasource/elasticsearch/metric_agg.ts | 32 ++++---- .../datasource/elasticsearch/query_def.ts | 8 +- .../datasource/graphite/graphite_query.ts | 4 +- .../app/plugins/datasource/graphite/lexer.ts | 2 +- .../plugins/datasource/influxdb/datasource.ts | 4 +- .../datasource/influxdb/influx_query.ts | 6 +- .../datasource/influxdb/influx_series.ts | 10 +-- .../datasource/influxdb/query_builder.ts | 2 +- .../plugins/datasource/influxdb/query_ctrl.ts | 2 +- .../plugins/datasource/influxdb/query_part.ts | 4 +- .../plugins/datasource/mixed/datasource.ts | 4 +- .../plugins/datasource/mssql/datasource.ts | 2 +- .../plugins/datasource/mysql/datasource.ts | 2 +- .../plugins/datasource/opentsdb/datasource.ts | 80 +++++++++---------- .../plugins/datasource/opentsdb/query_ctrl.ts | 2 +- .../datasource/prometheus/datasource.ts | 6 +- .../prometheus/metric_find_query.ts | 26 +++--- .../datasource/prometheus/query_ctrl.ts | 2 +- .../prometheus/result_transformer.ts | 10 +-- public/app/plugins/panel/graph/graph.ts | 12 +-- .../app/plugins/panel/graph/graph_tooltip.ts | 22 ++--- .../plugins/panel/graph/jquery.flot.events.ts | 42 +++++----- public/app/plugins/panel/graph/legend.ts | 14 ++-- .../panel/graph/series_overrides_ctrl.ts | 22 ++--- .../plugins/panel/graph/threshold_manager.ts | 2 +- .../plugins/panel/graph/thresholds_form.ts | 2 +- .../app/plugins/panel/heatmap/color_legend.ts | 12 +-- .../plugins/panel/heatmap/heatmap_tooltip.ts | 2 +- public/app/plugins/panel/heatmap/rendering.ts | 2 +- public/app/plugins/panel/singlestat/module.ts | 16 ++-- .../app/plugins/panel/table/column_options.ts | 2 +- public/app/plugins/panel/table/module.ts | 4 +- .../app/plugins/panel/table/transformers.ts | 26 +++--- 50 files changed, 289 insertions(+), 288 deletions(-) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 286c3f743c7..6c7b8cf3bf7 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -28,7 +28,7 @@ export class GrafanaCtrl { setBackendSrv(backendSrv); createStore({ backendSrv, datasourceSrv }); - $scope.init = function() { + $scope.init = () => { $scope.contextSrv = contextSrv; $scope.appSubUrl = config.appSubUrl; $scope._ = _; @@ -43,7 +43,7 @@ export class GrafanaCtrl { $rootScope.colors = colors; - $scope.initDashboard = function(dashboardData, viewScope) { + $scope.initDashboard = (dashboardData, viewScope) => { $scope.appEvent('dashboard-fetch-end', dashboardData); $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); }; @@ -60,7 +60,7 @@ export class GrafanaCtrl { callerScope.$on('$destroy', unbind); }; - $rootScope.appEvent = function(name, payload) { + $rootScope.appEvent = (name, payload) => { $rootScope.$emit(name, payload); appEvents.emit(name, payload); }; @@ -103,7 +103,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop const body = $('body'); // see https://github.com/zenorocha/clipboard.js/issues/155 - $.fn.modal.Constructor.prototype.enforceFocus = function() {}; + $.fn.modal.Constructor.prototype.enforceFocus = () => {}; $('.preloader').remove(); @@ -123,9 +123,12 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop body.toggleClass('sidemenu-hidden'); }); - scope.$watch(() => playlistSrv.isPlaying, function(newValue) { - elem.toggleClass('view-mode--playlist', newValue === true); - }); + scope.$watch( + () => playlistSrv.isPlaying, + newValue => { + elem.toggleClass('view-mode--playlist', newValue === true); + } + ); // check if we are in server side render if (document.cookie.indexOf('renderKey') !== -1) { @@ -135,7 +138,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // tooltip removal fix // manage page classes let pageClass; - scope.$on('$routeChangeSuccess', function(evt, data) { + scope.$on('$routeChangeSuccess', (evt, data) => { if (pageClass) { body.removeClass(pageClass); } @@ -236,7 +239,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop }); // handle document clicks that should hide things - body.click(function(evt) { + body.click(evt => { const target = $(evt.target); if (target.parents().length === 0) { return; @@ -248,7 +251,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop if (clickAutoHide.length) { const clickAutoHideParent = clickAutoHide.parent(); clickAutoHide.detach(); - setTimeout(function() { + setTimeout(() => { clickAutoHideParent.append(clickAutoHide); }, 100); } @@ -260,7 +263,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // hide search if (body.find('.search-container').length > 0) { if (target.parents('.search-results-container, .search-field-wrapper').length === 0) { - scope.$apply(function() { + scope.$apply(() => { scope.appEvent('hide-dash-search'); }); } diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index ae4feeec701..2ada91b09f1 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -7,7 +7,7 @@ export function infoPopover() { restrict: 'E', template: '', transclude: true, - link: function(scope, elem, attrs, ctrl, transclude) { + link: (scope, elem, attrs, ctrl, transclude) => { const offset = attrs.offset || '0 -10px'; const position = attrs.position || 'right middle'; let classes = 'drop-help drop-hide-out-of-bounds'; @@ -23,7 +23,7 @@ export function infoPopover() { elem.addClass('gf-form-help-icon--' + attrs.mode); } - transclude(function(clone, newScope) { + transclude((clone, newScope) => { const content = document.createElement('div'); content.className = 'markdown-html'; @@ -54,7 +54,7 @@ export function infoPopover() { scope.$applyAsync(() => { const drop = new Drop(dropOptions); - const unbind = scope.$on('$destroy', function() { + const unbind = scope.$on('$destroy', () => { drop.destroy(); unbind(); }); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index 6fcc768c846..b3f3cdc14d1 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -50,7 +50,7 @@ export function layoutMode($rootScope) { return { restrict: 'A', scope: {}, - link: function(scope, elem) { + link: (scope, elem) => { const layout = store.get('grafana.list.layout.mode') || 'grid'; let className = 'card-list-layout-' + layout; elem.addClass(className); diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts index 7e95be72458..db0924738ed 100644 --- a/public/app/core/components/navbar/navbar.ts +++ b/public/app/core/components/navbar/navbar.ts @@ -30,7 +30,7 @@ export function navbarDirective() { scope: { model: '=', }, - link: function(scope, elem) {}, + link: (scope, elem) => {}, }; } diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts index d45c6532364..6181d020471 100644 --- a/public/app/core/components/query_part/query_part_editor.ts +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -89,20 +89,20 @@ export function queryPartEditorDirective($compile, templateSrv) { return; } - const typeaheadSource = function(query, callback) { + const typeaheadSource = (query, callback) => { if (param.options) { let options = param.options; if (param.type === 'int') { - options = _.map(options, function(val) { + options = _.map(options, val => { return val.toString(); }); } return options; } - $scope.$apply(function() { - $scope.handleEvent({ $event: { name: 'get-param-options' } }).then(function(result) { - const dynamicOptions = _.map(result, function(op) { + $scope.$apply(() => { + $scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => { + const dynamicOptions = _.map(result, op => { return op.value; }); callback(dynamicOptions); @@ -116,8 +116,8 @@ export function queryPartEditorDirective($compile, templateSrv) { source: typeaheadSource, minLength: 0, items: 1000, - updater: function(value) { - setTimeout(function() { + updater: value => { + setTimeout(() => { inputBlur.call($input[0], paramIndex); }, 0); return value; @@ -136,18 +136,18 @@ export function queryPartEditorDirective($compile, templateSrv) { } } - $scope.showActionsMenu = function() { + $scope.showActionsMenu = () => { $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => { $scope.partActions = res; }); }; - $scope.triggerPartAction = function(action) { + $scope.triggerPartAction = action => { $scope.handleEvent({ $event: { name: 'action', action: action } }); }; function addElementsAndCompile() { - _.each(partDef.params, function(param, index) { + _.each(partDef.params, (param, index) => { if (param.optional && part.params.length <= index) { return; } diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index f2798f874bd..1348033d296 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -71,14 +71,14 @@ export function sideMenuDirective() { bindToController: true, controllerAs: 'ctrl', scope: {}, - link: function(scope, elem) { + link: (scope, elem) => { // hack to hide dropdown menu - elem.on('click.dropdown', '.dropdown-menu a', function(evt) { + elem.on('click.dropdown', '.dropdown-menu a', evt => { const menu = $(evt.target).parents('.dropdown-menu'); const parent = menu.parent(); menu.detach(); - setTimeout(function() { + setTimeout(() => { parent.append(menu); }, 100); }); diff --git a/public/app/core/controllers/inspect_ctrl.ts b/public/app/core/controllers/inspect_ctrl.ts index c55a0d50902..d106b42da16 100644 --- a/public/app/core/controllers/inspect_ctrl.ts +++ b/public/app/core/controllers/inspect_ctrl.ts @@ -28,7 +28,7 @@ export class InspectCtrl { } if (model.error.config && model.error.config.params) { - $scope.request_parameters = _.map(model.error.config.params, function(value, key) { + $scope.request_parameters = _.map(model.error.config.params, (value, key) => { return { key: key, value: value }; }); } @@ -45,7 +45,7 @@ export class InspectCtrl { if (_.isString(model.error.config.data)) { $scope.request_parameters = this.getParametersFromQueryString(model.error.config.data); } else { - $scope.request_parameters = _.map(model.error.config.data, function(value, key) { + $scope.request_parameters = _.map(model.error.config.data, (value, key) => { return { key: key, value: angular.toJson(value, true) }; }); } diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index 3260f6ff537..9c3f9d9e98d 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -8,7 +8,7 @@ export class JsonEditorCtrl { $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; $scope.canCopy = $scope.enableCopy; - $scope.update = function() { + $scope.update = () => { const newObject = angular.fromJson($scope.json); $scope.updateHandler(newObject, $scope.object); }; diff --git a/public/app/core/directives/array_join.ts b/public/app/core/directives/array_join.ts index f3416f43576..c906319e985 100644 --- a/public/app/core/directives/array_join.ts +++ b/public/app/core/directives/array_join.ts @@ -7,7 +7,7 @@ export function arrayJoin() { return { restrict: 'A', require: 'ngModel', - link: function(scope, element, attr, ngModel) { + link: (scope, element, attr, ngModel) => { function split_array(text) { return (text || '').split(','); } diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 6da6fc4f66d..7e626bc5860 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -20,7 +20,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, event: '=', onEdit: '&', }, - link: function(scope, element) { + link: (scope, element) => { const event = scope.event; let title = event.title; let text = event.text; diff --git a/public/app/features/panellinks/link_srv.ts b/public/app/features/panellinks/link_srv.ts index 9f18a76bf61..f9ad40c50da 100644 --- a/public/app/features/panellinks/link_srv.ts +++ b/public/app/features/panellinks/link_srv.ts @@ -26,14 +26,14 @@ export class LinkSrv { addParamsToUrl(url, params) { const paramsArray = []; - _.each(params, function(value, key) { + _.each(params, (value, key) => { if (value === null) { return; } if (value === true) { paramsArray.push(key); } else if (_.isArray(value)) { - _.each(value, function(instance) { + _.each(value, instance => { paramsArray.push(key + '=' + encodeURIComponent(instance)); }); } else { diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index b73c91ddb2b..7ef82519668 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -77,7 +77,7 @@ export class DatasourceSrv { this.addDataSourceVariables(sources); - _.each(config.datasources, function(value) { + _.each(config.datasources, value => { if (value.meta && value.meta.annotations) { sources.push(value); } @@ -97,7 +97,7 @@ export class DatasourceSrv { getMetricSources(options) { const metricSources = []; - _.each(config.datasources, function(value, key) { + _.each(config.datasources, (value, key) => { if (value.meta && value.meta.metrics) { let metricSource = { value: key, name: key, meta: value.meta, sort: key }; @@ -121,7 +121,7 @@ export class DatasourceSrv { this.addDataSourceVariables(metricSources); } - metricSources.sort(function(a, b) { + metricSources.sort((a, b) => { if (a.sort.toLowerCase() > b.sort.toLowerCase()) { return 1; } diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index e227dbb910c..bc3c719917c 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -65,7 +65,7 @@ System.config({ }); function exposeToPlugin(name: string, component: any) { - System.registerDynamic(name, [], true, function(require, exports, module) { + System.registerDynamic(name, [], true, (require, exports, module) => { module.exports = component; }); } diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts index 065a7100b09..4f4b2961761 100644 --- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts @@ -19,7 +19,7 @@ export class CloudWatchQueryParameter { export class CloudWatchQueryParameterCtrl { /** @ngInject */ constructor($scope, templateSrv, uiSegmentSrv, datasourceSrv, $q) { - $scope.init = function() { + $scope.init = () => { const target = $scope.target; target.namespace = target.namespace || ''; target.metricName = target.metricName || ''; @@ -38,7 +38,7 @@ export class CloudWatchQueryParameterCtrl { $scope.dimSegments = _.reduce( $scope.target.dimensions, - function(memo, value, key) { + (memo, value, key) => { memo.push(uiSegmentSrv.newKey(key)); memo.push(uiSegmentSrv.newOperator('=')); memo.push(uiSegmentSrv.newKeyValue(value)); @@ -47,7 +47,7 @@ export class CloudWatchQueryParameterCtrl { [] ); - $scope.statSegments = _.map($scope.target.statistics, function(stat) { + $scope.statSegments = _.map($scope.target.statistics, stat => { return uiSegmentSrv.getSegmentForValue(stat); }); @@ -67,15 +67,15 @@ export class CloudWatchQueryParameterCtrl { } if (!$scope.onChange) { - $scope.onChange = function() {}; + $scope.onChange = () => {}; } }; - $scope.getStatSegments = function() { + $scope.getStatSegments = () => { return $q.when( _.flatten([ angular.copy($scope.removeStatSegment), - _.map($scope.datasource.standardStatistics, function(s) { + _.map($scope.datasource.standardStatistics, s => { return uiSegmentSrv.getSegmentForValue(s); }), uiSegmentSrv.getSegmentForValue('pNN.NN'), @@ -83,7 +83,7 @@ export class CloudWatchQueryParameterCtrl { ); }; - $scope.statSegmentChanged = function(segment, index) { + $scope.statSegmentChanged = (segment, index) => { if (segment.value === $scope.removeStatSegment.value) { $scope.statSegments.splice(index, 1); } else { @@ -92,7 +92,7 @@ export class CloudWatchQueryParameterCtrl { $scope.target.statistics = _.reduce( $scope.statSegments, - function(memo, seg) { + (memo, seg) => { if (!seg.fake) { memo.push(seg.value); } @@ -105,7 +105,7 @@ export class CloudWatchQueryParameterCtrl { $scope.onChange(); }; - $scope.ensurePlusButton = function(segments) { + $scope.ensurePlusButton = segments => { const count = segments.length; const lastSegment = segments[Math.max(count - 1, 0)]; @@ -114,7 +114,7 @@ export class CloudWatchQueryParameterCtrl { } }; - $scope.getDimSegments = function(segment, $index) { + $scope.getDimSegments = (segment, $index) => { if (segment.type === 'operator') { return $q.when([]); } @@ -135,7 +135,7 @@ export class CloudWatchQueryParameterCtrl { ); } - return query.then($scope.transformToSegments(true)).then(function(results) { + return query.then($scope.transformToSegments(true)).then(results => { if (segment.type === 'key') { results.splice(0, 0, angular.copy($scope.removeDimSegment)); } @@ -143,7 +143,7 @@ export class CloudWatchQueryParameterCtrl { }); }; - $scope.dimSegmentChanged = function(segment, index) { + $scope.dimSegmentChanged = (segment, index) => { $scope.dimSegments[index] = segment; if (segment.value === $scope.removeDimSegment.value) { @@ -160,7 +160,7 @@ export class CloudWatchQueryParameterCtrl { $scope.onChange(); }; - $scope.syncDimSegmentsWithModel = function() { + $scope.syncDimSegmentsWithModel = () => { const dims = {}; const length = $scope.dimSegments.length; @@ -175,44 +175,44 @@ export class CloudWatchQueryParameterCtrl { $scope.target.dimensions = dims; }; - $scope.getRegions = function() { + $scope.getRegions = () => { return $scope.datasource .metricFindQuery('regions()') - .then(function(results) { + .then(results => { results.unshift({ text: 'default' }); return results; }) .then($scope.transformToSegments(true)); }; - $scope.getNamespaces = function() { + $scope.getNamespaces = () => { return $scope.datasource.metricFindQuery('namespaces()').then($scope.transformToSegments(true)); }; - $scope.getMetrics = function() { + $scope.getMetrics = () => { return $scope.datasource .metricFindQuery('metrics(' + $scope.target.namespace + ',' + $scope.target.region + ')') .then($scope.transformToSegments(true)); }; - $scope.regionChanged = function() { + $scope.regionChanged = () => { $scope.target.region = $scope.regionSegment.value; $scope.onChange(); }; - $scope.namespaceChanged = function() { + $scope.namespaceChanged = () => { $scope.target.namespace = $scope.namespaceSegment.value; $scope.onChange(); }; - $scope.metricChanged = function() { + $scope.metricChanged = () => { $scope.target.metricName = $scope.metricSegment.value; $scope.onChange(); }; - $scope.transformToSegments = function(addTemplateVars) { - return function(results) { - const segments = _.map(results, function(segment) { + $scope.transformToSegments = addTemplateVars => { + return results => { + const segments = _.map(results, segment => { return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable, @@ -220,7 +220,7 @@ export class CloudWatchQueryParameterCtrl { }); if (addTemplateVars) { - _.each(templateSrv.variables, function(variable) { + _.each(templateSrv.variables, variable => { segments.unshift( uiSegmentSrv.newSegment({ type: 'template', diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts index 8c8e00b34ec..e17a34778ee 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts @@ -23,36 +23,36 @@ export class ElasticBucketAggCtrl { $scope.orderByOptions = []; - $scope.getBucketAggTypes = function() { + $scope.getBucketAggTypes = () => { return queryDef.bucketAggTypes; }; - $scope.getOrderOptions = function() { + $scope.getOrderOptions = () => { return queryDef.orderOptions; }; - $scope.getSizeOptions = function() { + $scope.getSizeOptions = () => { return queryDef.sizeOptions; }; $rootScope.onAppEvent( 'elastic-query-updated', - function() { + () => { $scope.validateModel(); }, $scope ); - $scope.init = function() { + $scope.init = () => { $scope.agg = bucketAggs[$scope.index]; $scope.validateModel(); }; - $scope.onChangeInternal = function() { + $scope.onChangeInternal = () => { $scope.onChange(); }; - $scope.onTypeChanged = function() { + $scope.onTypeChanged = () => { $scope.agg.settings = {}; $scope.showOptions = false; @@ -79,7 +79,7 @@ export class ElasticBucketAggCtrl { $scope.onChange(); }; - $scope.validateModel = function() { + $scope.validateModel = () => { $scope.index = _.indexOf(bucketAggs, $scope.agg); $scope.isFirst = $scope.index === 0; $scope.bucketAggCount = bucketAggs.length; @@ -114,7 +114,7 @@ export class ElasticBucketAggCtrl { settings.filters = settings.filters || [{ query: '*' }]; settingsLinkText = _.reduce( settings.filters, - function(memo, value, index) { + (memo, value, index) => { memo += 'Q' + (index + 1) + ' = ' + value.query + ' '; return memo; }, @@ -168,23 +168,23 @@ export class ElasticBucketAggCtrl { return true; }; - $scope.addFiltersQuery = function() { + $scope.addFiltersQuery = () => { $scope.agg.settings.filters.push({ query: '*' }); }; - $scope.removeFiltersQuery = function(filter) { + $scope.removeFiltersQuery = filter => { $scope.agg.settings.filters = _.without($scope.agg.settings.filters, filter); }; - $scope.toggleOptions = function() { + $scope.toggleOptions = () => { $scope.showOptions = !$scope.showOptions; }; - $scope.getOrderByOptions = function() { + $scope.getOrderByOptions = () => { return queryDef.getOrderByOptions($scope.target); }; - $scope.getFieldsInternal = function() { + $scope.getFieldsInternal = () => { if ($scope.agg.type === 'date_histogram') { return $scope.getFields({ $fieldType: 'date' }); } else { @@ -192,11 +192,11 @@ export class ElasticBucketAggCtrl { } }; - $scope.getIntervalOptions = function() { + $scope.getIntervalOptions = () => { return $q.when(uiSegmentSrv.transformToSegments(true, 'interval')(queryDef.intervalOptions)); }; - $scope.addBucketAgg = function() { + $scope.addBucketAgg = () => { // if last is date histogram add it before const lastBucket = bucketAggs[bucketAggs.length - 1]; let addIndex = bucketAggs.length - 1; @@ -207,7 +207,7 @@ export class ElasticBucketAggCtrl { const id = _.reduce( $scope.target.bucketAggs.concat($scope.target.metrics), - function(max, val) { + (max, val) => { return parseInt(val.id) > max ? parseInt(val.id) : max; }, 0 @@ -217,7 +217,7 @@ export class ElasticBucketAggCtrl { $scope.onChange(); }; - $scope.removeBucketAgg = function() { + $scope.removeBucketAgg = () => { bucketAggs.splice($scope.index, 1); $scope.onChange(); }; diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index f1e7bf78514..c2f2364d49d 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -59,12 +59,12 @@ export class ElasticDatasource { const range = this.timeSrv.timeRange(); const indexList = this.indexPattern.getIndexList(range.from.valueOf(), range.to.valueOf()); if (_.isArray(indexList) && indexList.length) { - return this.request('GET', indexList[0] + url).then(function(results) { + return this.request('GET', indexList[0] + url).then(results => { results.data.$$config = results.config; return results.data; }); } else { - return this.request('GET', this.indexPattern.getIndexForToday() + url).then(function(results) { + return this.request('GET', this.indexPattern.getIndexForToday() + url).then(results => { results.data.$$config = results.config; return results.data; }); @@ -73,7 +73,7 @@ export class ElasticDatasource { private post(url, data) { return this.request('POST', url, data) - .then(function(results) { + .then(results => { results.data.$$config = results.config; return results.data; }) @@ -145,7 +145,7 @@ export class ElasticDatasource { const list = []; const hits = res.responses[0].hits.hits; - const getFieldFromSource = function(source, fieldName) { + const getFieldFromSource = (source, fieldName) => { if (!fieldName) { return; } @@ -213,7 +213,7 @@ export class ElasticDatasource { } return { status: 'success', message: 'Index OK. Time field name OK.' }; }, - function(err) { + err => { console.log(err); if (err.data && err.data.error) { let message = angular.toJson(err.data.error); @@ -274,13 +274,13 @@ export class ElasticDatasource { payload = payload.replace(/\$timeTo/g, options.range.to.valueOf()); payload = this.templateSrv.replace(payload, options.scopedVars); - return this.post('_msearch', payload).then(function(res) { + return this.post('_msearch', payload).then(res => { return new ElasticResponse(sentTargets, res).getTimeSeries(); }); } getFields(query) { - return this.get('/_mapping').then(function(result) { + return this.get('/_mapping').then(result => { const typeMap = { float: 'number', double: 'number', @@ -352,7 +352,7 @@ export class ElasticDatasource { } // transform to array - return _.map(fields, function(value) { + return _.map(fields, value => { return value; }); }); @@ -368,13 +368,13 @@ export class ElasticDatasource { esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf()); esQuery = header + '\n' + esQuery + '\n'; - return this.post('_msearch?search_type=' + searchType, esQuery).then(function(res) { + return this.post('_msearch?search_type=' + searchType, esQuery).then(res => { if (!res.responses[0].aggregations) { return []; } const buckets = res.responses[0].aggregations['1'].buckets; - return _.map(buckets, function(bucket) { + return _.map(buckets, bucket => { return { text: bucket.key_as_string || bucket.key, value: bucket.key, diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts index 2be97423176..7adec22c545 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -227,7 +227,7 @@ export class ElasticResponse { if (target.alias) { const regex = /\{\{([\s\S]+?)\}\}/g; - return target.alias.replace(regex, function(match, g1, g2) { + return target.alias.replace(regex, (match, g1, g2) => { const group = g1 || g2; if (group.indexOf('term ') === 0) { diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 08af5ab825e..7e5300b43e1 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -25,19 +25,19 @@ export class ElasticMetricAggCtrl { $scope.pipelineAggOptions = []; $scope.modelSettingsValues = {}; - $scope.init = function() { + $scope.init = () => { $scope.agg = metricAggs[$scope.index]; $scope.validateModel(); $scope.updatePipelineAggOptions(); }; - $scope.updatePipelineAggOptions = function() { + $scope.updatePipelineAggOptions = () => { $scope.pipelineAggOptions = queryDef.getPipelineAggOptions($scope.target); }; $rootScope.onAppEvent( 'elastic-query-updated', - function() { + () => { $scope.index = _.indexOf(metricAggs, $scope.agg); $scope.updatePipelineAggOptions(); $scope.validateModel(); @@ -45,7 +45,7 @@ export class ElasticMetricAggCtrl { $scope ); - $scope.validateModel = function() { + $scope.validateModel = () => { $scope.isFirst = $scope.index === 0; $scope.isSingle = metricAggs.length === 1; $scope.settingsLinkText = ''; @@ -57,7 +57,7 @@ export class ElasticMetricAggCtrl { const pipelineOptions = queryDef.getPipelineOptions($scope.agg); if (pipelineOptions.length > 0) { - _.each(pipelineOptions, function(opt) { + _.each(pipelineOptions, opt => { $scope.agg.settings[opt.text] = $scope.agg.settings[opt.text] || opt.default; }); $scope.settingsLinkText = 'Options'; @@ -84,7 +84,7 @@ export class ElasticMetricAggCtrl { const stats = _.reduce( $scope.agg.meta, - function(memo, val, key) { + (memo, val, key) => { if (val) { const def = _.find($scope.extendedStats, { value: key }); memo.push(def.text); @@ -128,16 +128,16 @@ export class ElasticMetricAggCtrl { } }; - $scope.toggleOptions = function() { + $scope.toggleOptions = () => { $scope.showOptions = !$scope.showOptions; $scope.updatePipelineAggOptions(); }; - $scope.onChangeInternal = function() { + $scope.onChangeInternal = () => { $scope.onChange(); }; - $scope.updateMovingAvgModelSettings = function() { + $scope.updateMovingAvgModelSettings = () => { const modelSettingsKeys = []; const modelSettings = queryDef.getMovingAvgSettings($scope.agg.settings.model, false); for (let i = 0; i < modelSettings.length; i++) { @@ -151,12 +151,12 @@ export class ElasticMetricAggCtrl { } }; - $scope.onChangeClearInternal = function() { + $scope.onChangeClearInternal = () => { delete $scope.agg.settings.minimize; $scope.onChange(); }; - $scope.onTypeChange = function() { + $scope.onTypeChange = () => { $scope.agg.settings = {}; $scope.agg.meta = {}; $scope.showOptions = false; @@ -164,19 +164,19 @@ export class ElasticMetricAggCtrl { $scope.onChange(); }; - $scope.getFieldsInternal = function() { + $scope.getFieldsInternal = () => { if ($scope.agg.type === 'cardinality') { return $scope.getFields(); } return $scope.getFields({ $fieldType: 'number' }); }; - $scope.addMetricAgg = function() { + $scope.addMetricAgg = () => { const addIndex = metricAggs.length; const id = _.reduce( $scope.target.bucketAggs.concat($scope.target.metrics), - function(max, val) { + (max, val) => { return parseInt(val.id) > max ? parseInt(val.id) : max; }, 0 @@ -186,12 +186,12 @@ export class ElasticMetricAggCtrl { $scope.onChange(); }; - $scope.removeMetricAgg = function() { + $scope.removeMetricAgg = () => { metricAggs.splice($scope.index, 1); $scope.onChange(); }; - $scope.toggleShowMetric = function() { + $scope.toggleShowMetric = () => { $scope.agg.hide = !$scope.agg.hide; if (!$scope.agg.hide) { delete $scope.agg.hide; diff --git a/public/app/plugins/datasource/elasticsearch/query_def.ts b/public/app/plugins/datasource/elasticsearch/query_def.ts index eec219d0065..dd65a8b373e 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.ts +++ b/public/app/plugins/datasource/elasticsearch/query_def.ts @@ -145,7 +145,7 @@ export const movingAvgModelSettings = { }; export function getMetricAggTypes(esVersion) { - return _.filter(metricAggTypes, function(f) { + return _.filter(metricAggTypes, f => { if (f.minVersion) { return f.minVersion <= esVersion; } else { @@ -173,7 +173,7 @@ export function isPipelineAgg(metricType) { export function getPipelineAggOptions(targets) { const result = []; - _.each(targets.metrics, function(metric) { + _.each(targets.metrics, metric => { if (!isPipelineAgg(metric.type)) { result.push({ text: describeMetric(metric), value: metric.id }); } @@ -185,7 +185,7 @@ export function getPipelineAggOptions(targets) { export function getMovingAvgSettings(model, filtered) { const filteredResult = []; if (filtered) { - _.each(movingAvgModelSettings[model], function(setting) { + _.each(movingAvgModelSettings[model], setting => { if (!setting.isCheckbox) { filteredResult.push(setting); } @@ -197,7 +197,7 @@ export function getMovingAvgSettings(model, filtered) { export function getOrderByOptions(target) { const metricRefs = []; - _.each(target.metrics, function(metric) { + _.each(target.metrics, metric => { if (metric.type !== 'count') { metricRefs.push({ text: describeMetric(metric), value: metric.id }); } diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 20179e0e509..ab137a6a299 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -73,7 +73,7 @@ export default class GraphiteQuery { return _.reduce( arr, - function(result, segment) { + (result, segment) => { return result ? result + '.' + segment.value : segment.value; }, '' @@ -133,7 +133,7 @@ export default class GraphiteQuery { } moveAliasFuncLast() { - const aliasFunc = _.find(this.functions, function(func) { + const aliasFunc = _.find(this.functions, func => { return func.def.name.startsWith('alias'); }); diff --git a/public/app/plugins/datasource/graphite/lexer.ts b/public/app/plugins/datasource/graphite/lexer.ts index 1f2da854991..0d72116a217 100644 --- a/public/app/plugins/datasource/graphite/lexer.ts +++ b/public/app/plugins/datasource/graphite/lexer.ts @@ -1370,7 +1370,7 @@ Lexer.prototype = { }; }, - isPunctuator: function(ch1) { + isPunctuator: ch1 => { switch (ch1) { case '.': case '(': diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index d9fd1a3605d..ec995de630a 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -23,7 +23,7 @@ export default class InfluxDatasource { /** @ngInject */ constructor(instanceSettings, private $q, private backendSrv, private templateSrv) { this.type = 'influxdb'; - this.urls = _.map(instanceSettings.url.split(','), function(url) { + this.urls = _.map(instanceSettings.url.split(','), url => { return url.trim(); }); @@ -274,7 +274,7 @@ export default class InfluxDatasource { result => { return result.data; }, - function(err) { + err => { if (err.status !== 0 || err.status >= 300) { if (err.data && err.data.error) { throw { diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index e9158739327..60eac1d3f2b 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -27,15 +27,15 @@ export default class InfluxQuery { } updateProjection() { - this.selectModels = _.map(this.target.select, function(parts: any) { + this.selectModels = _.map(this.target.select, (parts: any) => { return _.map(parts, queryPart.create); }); this.groupByParts = _.map(this.target.groupBy, queryPart.create); } updatePersistedParts() { - this.target.select = _.map(this.selectModels, function(selectParts) { - return _.map(selectParts, function(part: any) { + this.target.select = _.map(this.selectModels, selectParts => { + return _.map(selectParts, (part: any) => { return { type: part.def.type, params: part.params }; }); }); diff --git a/public/app/plugins/datasource/influxdb/influx_series.ts b/public/app/plugins/datasource/influxdb/influx_series.ts index a9268979592..d2a8482eced 100644 --- a/public/app/plugins/datasource/influxdb/influx_series.ts +++ b/public/app/plugins/datasource/influxdb/influx_series.ts @@ -22,7 +22,7 @@ export default class InfluxSeries { _.each(this.series, series => { const columns = series.columns.length; - const tags = _.map(series.tags, function(value, key) { + const tags = _.map(series.tags, (value, key) => { return key + ': ' + value; }); @@ -57,7 +57,7 @@ export default class InfluxSeries { const regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g; const segments = series.name.split('.'); - return this.alias.replace(regex, function(match, g1, g2) { + return this.alias.replace(regex, (match, g1, g2) => { const group = g1 || g2; const segIndex = parseInt(group, 10); @@ -124,10 +124,10 @@ export default class InfluxSeries { // Remove empty values, then split in different tags for comma separated values tags: _.flatten( tagsCol - .filter(function(t) { + .filter(t => { return value[t]; }) - .map(function(t) { + .map(t => { return value[t].split(','); }) ), @@ -158,7 +158,7 @@ export default class InfluxSeries { table.columns.push({ text: 'Time', type: 'time' }); j++; } - _.each(_.keys(series.tags), function(key) { + _.each(_.keys(series.tags), key => { table.columns.push({ text: key }); }); for (; j < series.columns.length; j++) { diff --git a/public/app/plugins/datasource/influxdb/query_builder.ts b/public/app/plugins/datasource/influxdb/query_builder.ts index 3d5ce476c69..a61216787d3 100644 --- a/public/app/plugins/datasource/influxdb/query_builder.ts +++ b/public/app/plugins/datasource/influxdb/query_builder.ts @@ -84,7 +84,7 @@ export class InfluxQueryBuilder { if (this.target.tags && this.target.tags.length > 0) { const whereConditions = _.reduce( this.target.tags, - function(memo, tag) { + (memo, tag) => { // do not add a condition for the key we want to explore for if (tag.key === withKey) { return memo; diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index 4a9310c63d1..f531fe6c4d9 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -70,7 +70,7 @@ export class InfluxQueryCtrl extends QueryCtrl { const categories = queryPart.getCategories(); this.selectMenu = _.reduce( categories, - function(memo, cat, key) { + (memo, cat, key) => { const menu = { text: key, submenu: cat.map(item => { diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index 22cac9bb608..4bc92bcfe72 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -126,7 +126,7 @@ function addAliasStrategy(selectParts, partModel) { function addFieldStrategy(selectParts, partModel, query) { // copy all parts - const parts = _.map(selectParts, function(part: any) { + const parts = _.map(selectParts, (part: any) => { return createPart({ type: part.def.type, params: _.clone(part.params) }); }); @@ -453,7 +453,7 @@ register({ export default { create: createPart, - getCategories: function() { + getCategories: () => { return categories; }, replaceAggregationAdd: replaceAggregationAddStrategy, diff --git a/public/app/plugins/datasource/mixed/datasource.ts b/public/app/plugins/datasource/mixed/datasource.ts index bfdfcd61c77..6018329093e 100644 --- a/public/app/plugins/datasource/mixed/datasource.ts +++ b/public/app/plugins/datasource/mixed/datasource.ts @@ -13,14 +13,14 @@ class MixedDatasource { return this.$q([]); } - return this.datasourceSrv.get(dsName).then(function(ds) { + return this.datasourceSrv.get(dsName).then(ds => { const opt = angular.copy(options); opt.targets = targets; return ds.query(opt); }); }); - return this.$q.all(promises).then(function(results) { + return this.$q.all(promises).then(results => { return { data: _.flatten(_.map(results, 'data')) }; }); } diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index fc497b2c274..4b67252632a 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -26,7 +26,7 @@ export class MssqlDatasource { return value; } - const quotedValues = _.map(value, function(val) { + const quotedValues = _.map(value, val => { if (typeof value === 'number') { return value; } diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index eca223f2d6d..6303dbb2c6d 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -26,7 +26,7 @@ export class MysqlDatasource { return value; } - const quotedValues = _.map(value, function(val) { + const quotedValues = _.map(value, val => { if (typeof value === 'number') { return value; } diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index 843b36a5dc0..08bd1585b42 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -56,19 +56,19 @@ export default class OpenTsDatasource { } const groupByTags = {}; - _.each(queries, function(query) { + _.each(queries, query => { if (query.filters && query.filters.length > 0) { - _.each(query.filters, function(val) { + _.each(query.filters, val => { groupByTags[val.tagk] = true; }); } else { - _.each(query.tags, function(val, key) { + _.each(query.tags, (val, key) => { groupByTags[key] = true; }); } }); - options.targets = _.filter(options.targets, function(query) { + options.targets = _.filter(options.targets, query => { return query.hide !== true; }); @@ -97,28 +97,26 @@ export default class OpenTsDatasource { const queries = _.compact(qs); - return this.performTimeSeriesQuery(queries, start, end).then( - function(results) { - if (results.data[0]) { - let annotationObject = results.data[0].annotations; - if (options.annotation.isGlobal) { - annotationObject = results.data[0].globalAnnotations; - } - if (annotationObject) { - _.each(annotationObject, function(annotation) { - const event = { - text: annotation.description, - time: Math.floor(annotation.startTime) * 1000, - annotation: options.annotation, - }; - - eventList.push(event); - }); - } + return this.performTimeSeriesQuery(queries, start, end).then(results => { + if (results.data[0]) { + let annotationObject = results.data[0].annotations; + if (options.annotation.isGlobal) { + annotationObject = results.data[0].globalAnnotations; } - return eventList; - }.bind(this) - ); + if (annotationObject) { + _.each(annotationObject, annotation => { + const event = { + text: annotation.description, + time: Math.floor(annotation.startTime) * 1000, + annotation: options.annotation, + }; + + eventList.push(event); + }); + } + } + return eventList; + }); } targetContainsTemplate(target) { @@ -177,7 +175,7 @@ export default class OpenTsDatasource { _saveTagKeys(metricData) { const tagKeys = Object.keys(metricData.tags); - _.each(metricData.aggregateTags, function(tag) { + _.each(metricData.aggregateTags, tag => { tagKeys.push(tag); }); @@ -185,7 +183,7 @@ export default class OpenTsDatasource { } _performSuggestQuery(query, type) { - return this._get('/api/suggest', { type: type, q: query, max: 1000 }).then(function(result) { + return this._get('/api/suggest', { type: type, q: query, max: 1000 }).then(result => { return result.data; }); } @@ -195,7 +193,7 @@ export default class OpenTsDatasource { return this.$q.when([]); } - const keysArray = keys.split(',').map(function(key) { + const keysArray = keys.split(',').map(key => { return key.trim(); }); const key = keysArray[0]; @@ -207,10 +205,10 @@ export default class OpenTsDatasource { const m = metric + '{' + keysQuery + '}'; - return this._get('/api/search/lookup', { m: m, limit: 3000 }).then(function(result) { + return this._get('/api/search/lookup', { m: m, limit: 3000 }).then(result => { result = result.data.results; const tagvs = []; - _.each(result, function(r) { + _.each(result, r => { if (tagvs.indexOf(r.tags[key]) === -1) { tagvs.push(r.tags[key]); } @@ -224,11 +222,11 @@ export default class OpenTsDatasource { return this.$q.when([]); } - return this._get('/api/search/lookup', { m: metric, limit: 1000 }).then(function(result) { + return this._get('/api/search/lookup', { m: metric, limit: 1000 }).then(result => { result = result.data.results; const tagks = []; - _.each(result, function(r) { - _.each(r.tags, function(tagv, tagk) { + _.each(result, r => { + _.each(r.tags, (tagv, tagk) => { if (tagks.indexOf(tagk) === -1) { tagks.push(tagk); } @@ -271,8 +269,8 @@ export default class OpenTsDatasource { return this.$q.reject(err); } - const responseTransform = function(result) { - return _.map(result, function(value) { + const responseTransform = result => { + return _.map(result, value => { return { text: value }; }); }; @@ -312,7 +310,7 @@ export default class OpenTsDatasource { } testDatasource() { - return this._performSuggestQuery('cpu', 'metrics').then(function() { + return this._performSuggestQuery('cpu', 'metrics').then(() => { return { status: 'success', message: 'Data source is working' }; }); } @@ -322,7 +320,7 @@ export default class OpenTsDatasource { return this.aggregatorsPromise; } - this.aggregatorsPromise = this._get('/api/aggregators').then(function(result) { + this.aggregatorsPromise = this._get('/api/aggregators').then(result => { if (result.data && _.isArray(result.data)) { return result.data.sort(); } @@ -336,7 +334,7 @@ export default class OpenTsDatasource { return this.filterTypesPromise; } - this.filterTypesPromise = this._get('/api/config/filters').then(function(result) { + this.filterTypesPromise = this._get('/api/config/filters').then(result => { if (result.data) { return Object.keys(result.data).sort(); } @@ -351,7 +349,7 @@ export default class OpenTsDatasource { // TSDB returns datapoints has a hash of ts => value. // Can't use _.pairs(invert()) because it stringifies keys/values - _.each(md.dps, function(v, k) { + _.each(md.dps, (v, k) => { if (tsdbResolution === 2) { dps.push([v, k * 1]); } else { @@ -365,7 +363,7 @@ export default class OpenTsDatasource { createMetricLabel(md, target, groupByTags, options) { if (target.alias) { const scopedVars = _.clone(options.scopedVars || {}); - _.each(md.tags, function(value, key) { + _.each(md.tags, (value, key) => { scopedVars['tag_' + key] = { value: value }; }); return this.templateSrv.replace(target.alias, scopedVars); @@ -375,7 +373,7 @@ export default class OpenTsDatasource { const tagData = []; if (!_.isEmpty(md.tags)) { - _.each(_.toPairs(md.tags), function(tag) { + _.each(_.toPairs(md.tags), tag => { if (_.has(groupByTags, tag[0])) { tagData.push(tag[0] + '=' + tag[1]); } diff --git a/public/app/plugins/datasource/opentsdb/query_ctrl.ts b/public/app/plugins/datasource/opentsdb/query_ctrl.ts index 4c8a0ed8d12..2e6f1d54302 100644 --- a/public/app/plugins/datasource/opentsdb/query_ctrl.ts +++ b/public/app/plugins/datasource/opentsdb/query_ctrl.ts @@ -88,7 +88,7 @@ export class OpenTsQueryCtrl extends QueryCtrl { } getTextValues(metricFindResult) { - return _.map(metricFindResult, function(value) { + return _.map(metricFindResult, value => { return value.text; }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 356322cf369..4d4da0a415b 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -526,13 +526,13 @@ export class PrometheusDatasource { const query = this.createQuery({ expr, interval: step }, queryOptions, start, end); const self = this; - return this.performTimeSeriesQuery(query, query.start, query.end).then(function(results) { + return this.performTimeSeriesQuery(query, query.start, query.end).then(results => { const eventList = []; tagKeys = tagKeys.split(','); - _.each(results.data.data.result, function(series) { + _.each(results.data.data.result, series => { const tags = _.chain(series.metric) - .filter(function(v, k) { + .filter((v, k) => { return _.includes(tagKeys, k); }) .value(); diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 8e15541ce62..feada28deea 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -46,8 +46,8 @@ export default class PrometheusMetricFindQuery { // return label values globally url = '/api/v1/label/' + label + '/values'; - return this.datasource.metadataRequest(url).then(function(result) { - return _.map(result.data.data, function(value) { + return this.datasource.metadataRequest(url).then(result => { + return _.map(result.data.data, value => { return { text: value }; }); }); @@ -56,14 +56,14 @@ export default class PrometheusMetricFindQuery { const end = this.datasource.getPrometheusTime(this.range.to, true); url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + '&start=' + start + '&end=' + end; - return this.datasource.metadataRequest(url).then(function(result) { - const _labels = _.map(result.data.data, function(metric) { + return this.datasource.metadataRequest(url).then(result => { + const _labels = _.map(result.data.data, metric => { return metric[label] || ''; - }).filter(function(label) { + }).filter(label => { return label !== ''; }); - return _.uniq(_labels).map(function(metric) { + return _.uniq(_labels).map(metric => { return { text: metric, expandable: true, @@ -76,13 +76,13 @@ export default class PrometheusMetricFindQuery { metricNameQuery(metricFilterPattern) { const url = '/api/v1/label/__name__/values'; - return this.datasource.metadataRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(result => { return _.chain(result.data.data) - .filter(function(metricName) { + .filter(metricName => { const r = new RegExp(metricFilterPattern); return r.test(metricName); }) - .map(function(matchedMetricName) { + .map(matchedMetricName => { return { text: matchedMetricName, expandable: true, @@ -94,13 +94,13 @@ export default class PrometheusMetricFindQuery { queryResultQuery(query) { const end = this.datasource.getPrometheusTime(this.range.to, true); - return this.datasource.performInstantQuery({ expr: query }, end).then(function(result) { - return _.map(result.data.data.result, function(metricData) { + return this.datasource.performInstantQuery({ expr: query }, end).then(result => { + return _.map(result.data.data.result, metricData => { let text = metricData.metric.__name__ || ''; delete metricData.metric.__name__; text += '{' + - _.map(metricData.metric, function(v, k) { + _.map(metricData.metric, (v, k) => { return k + '="' + v + '"'; }).join(',') + '}'; @@ -120,7 +120,7 @@ export default class PrometheusMetricFindQuery { const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; const self = this; - return this.datasource.metadataRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(result => { return _.map(result.data.data, metric => { return { text: self.datasource.getOriginalMetricName(metric), diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 36c3b35a817..66038cb5ca4 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -27,7 +27,7 @@ class PrometheusQueryCtrl extends QueryCtrl { target.format = target.format || this.getDefaultFormat(); this.metric = ''; - this.resolutions = _.map([1, 2, 3, 4, 5, 10], function(f) { + this.resolutions = _.map([1, 2, 3, 4, 5, 10], f => { return { factor: f, label: '1/' + f }; }); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 3f4e4592a2d..96b8e0d4137 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -81,7 +81,7 @@ export class ResultTransformer { } // Collect all labels across all metrics - _.each(md, function(series) { + _.each(md, series => { for (const label in series.metric) { if (!metricLabels.hasOwnProperty(label)) { metricLabels[label] = 1; @@ -92,7 +92,7 @@ export class ResultTransformer { // Sort metric labels, create columns for them and record their index const sortedLabels = _.keys(metricLabels).sort(); table.columns.push({ text: 'Time', type: 'time' }); - _.each(sortedLabels, function(label, labelIndex) { + _.each(sortedLabels, (label, labelIndex) => { metricLabels[label] = labelIndex + 1; table.columns.push({ text: label, filterable: !label.startsWith('__') }); }); @@ -100,7 +100,7 @@ export class ResultTransformer { table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. - _.each(md, function(series) { + _.each(md, series => { if (series.value) { series.values = [series.value]; } @@ -150,7 +150,7 @@ export class ResultTransformer { renderTemplate(aliasPattern, aliasData) { const aliasRegex = /\{\{\s*(.+?)\s*\}\}/g; - return aliasPattern.replace(aliasRegex, function(match, g1) { + return aliasPattern.replace(aliasRegex, (match, g1) => { if (aliasData[g1]) { return aliasData[g1]; } @@ -161,7 +161,7 @@ export class ResultTransformer { getOriginalMetricName(labelData) { const metricName = labelData.__name__ || ''; delete labelData.__name__; - const labelPart = _.map(_.toPairs(labelData), function(label) { + const labelPart = _.map(_.toPairs(labelData), label => { return label[0] + '="' + label[1] + '"'; }).join(','); return metricName + '{' + labelPart + '}'; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 8e871566877..33db0e7220a 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -464,7 +464,7 @@ class GraphElement { } addXSeriesAxis(options) { - const ticks = _.map(this.data, function(series, index) { + const ticks = _.map(this.data, (series, index) => { return [index + 1, series.alias]; }); @@ -533,8 +533,8 @@ class GraphElement { } addXTableAxis(options) { - let ticks = _.map(this.data, function(series, seriesIndex) { - return _.map(series.datapoints, function(point, pointIndex) { + let ticks = _.map(this.data, (series, seriesIndex) => { + return _.map(series.datapoints, (point, pointIndex) => { const tickIndex = seriesIndex * series.datapoints.length + pointIndex; return [tickIndex + 1, point[1]]; }); @@ -627,10 +627,10 @@ class GraphElement { } } - axis.transform = function(v) { + axis.transform = v => { return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase); }; - axis.inverseTransform = function(v) { + axis.inverseTransform = v => { return Math.pow(axis.logBase, v); }; @@ -701,7 +701,7 @@ class GraphElement { } configureAxisMode(axis, format) { - axis.tickFormatter = function(val, axis) { + axis.tickFormatter = (val, axis) => { if (!kbn.valueFormats[format]) { throw new Error(`Unit '${format}' is not supported`); } diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts index 53b646b4442..aeba9fa6ed3 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.ts +++ b/public/app/plugins/panel/graph/graph_tooltip.ts @@ -8,11 +8,11 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie const $tooltip = $('
    '); - this.destroy = function() { + this.destroy = () => { $tooltip.remove(); }; - this.findHoverIndexFromDataPoints = function(posX, series, last) { + this.findHoverIndexFromDataPoints = (posX, series, last) => { const ps = series.datapoints.pointsize; const initial = last * ps; const len = series.datapoints.points.length; @@ -30,7 +30,7 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie return j / ps - 1; }; - this.findHoverIndexFromData = function(posX, series) { + this.findHoverIndexFromData = (posX, series) => { let lower = 0; let upper = series.data.length - 1; let middle; @@ -49,7 +49,7 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie } }; - this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) { + this.renderAndShow = (absoluteTime, innerHtml, pos, xMode) => { if (xMode === 'time') { innerHtml = '
    ' + absoluteTime + '
    ' + innerHtml; } @@ -147,7 +147,7 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie return results; }; - elem.mouseleave(function() { + elem.mouseleave(() => { if (panel.tooltip.shared) { const plot = elem.data().plot; if (plot) { @@ -158,7 +158,7 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie appEvents.emit('graph-hover-clear'); }); - elem.bind('plothover', function(event, pos, item) { + elem.bind('plothover', (event, pos, item) => { self.show(pos, item); // broadcast to other graph panels that we are hovering! @@ -166,17 +166,17 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie appEvents.emit('graph-hover', { pos: pos, panel: panel }); }); - elem.bind('plotclick', function(event, pos, item) { + elem.bind('plotclick', (event, pos, item) => { appEvents.emit('graph-click', { pos: pos, panel: panel, item: item }); }); - this.clear = function(plot) { + this.clear = plot => { $tooltip.detach(); plot.clearCrosshair(); plot.unhighlight(); }; - this.show = function(pos, item) { + this.show = (pos, item) => { const plot = elem.data().plot; const plotData = plot.getData(); const xAxes = plot.getXAxes(); @@ -232,11 +232,11 @@ export default function GraphTooltip(this: any, elem, dashboard, scope, getSerie // Dynamically reorder the hovercard for the current time point if the // option is enabled. if (panel.tooltip.sort === 2) { - seriesHoverInfo.sort(function(a, b) { + seriesHoverInfo.sort((a, b) => { return b.value - a.value; }); } else if (panel.tooltip.sort === 1) { - seriesHoverInfo.sort(function(a, b) { + seriesHoverInfo.sort((a, b) => { return a.value - b.value; }); } diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts index f0600bdcd40..ed2b2dab92a 100644 --- a/public/app/plugins/panel/graph/jquery.flot.events.ts +++ b/public/app/plugins/panel/graph/jquery.flot.events.ts @@ -12,11 +12,11 @@ export function createAnnotationToolip(element, event, plot) { injector.invoke([ '$compile', '$rootScope', - function($compile, $rootScope) { + ($compile, $rootScope) => { const eventManager = plot.getOptions().events.manager; const tmpScope = $rootScope.$new(true); tmpScope.event = event; - tmpScope.onEdit = function() { + tmpScope.onEdit = () => { eventManager.editEvent(event); }; @@ -38,8 +38,8 @@ export function createAnnotationToolip(element, event, plot) { drop.open(); - drop.on('close', function() { - setTimeout(function() { + drop.on('close', () => { + setTimeout(() => { drop.destroy(); }); }); @@ -65,7 +65,7 @@ export function createEditPopover(element, event, plot) { markerElementToAttachTo = element; // wait for element to be attached and positioned - setTimeout(function() { + setTimeout(() => { const injector = angular.element(document).injector(); const content = document.createElement('div'); content.innerHTML = ''; @@ -73,13 +73,13 @@ export function createEditPopover(element, event, plot) { injector.invoke([ '$compile', '$rootScope', - function($compile, $rootScope) { + ($compile, $rootScope) => { const scope = $rootScope.$new(true); let drop; scope.event = event; scope.panelCtrl = eventManager.panelCtrl; - scope.close = function() { + scope.close = () => { drop.close(); }; @@ -100,9 +100,9 @@ export function createEditPopover(element, event, plot) { drop.open(); eventManager.editorOpened(); - drop.on('close', function() { + drop.on('close', () => { // need timeout here in order call drop.destroy - setTimeout(function() { + setTimeout(() => { eventManager.editorClosed(); scope.$destroy(); drop.destroy(); @@ -428,7 +428,7 @@ export class EventMarkers { createEditPopover(marker, event.editModel, that._plot); } - const mouseleave = function() { + const mouseleave = () => { that._plot.clearSelection(); }; @@ -443,10 +443,10 @@ export class EventMarkers { function drawFunc(obj) { obj.show(); }, - function(obj) { + obj => { obj.remove(); }, - function(obj, position) { + (obj, position) => { obj.css({ top: position.top, left: position.left, @@ -549,7 +549,7 @@ export class EventMarkers { createEditPopover(region, event.editModel, that._plot); } - const mouseleave = function() { + const mouseleave = () => { that._plot.clearSelection(); }; @@ -563,10 +563,10 @@ export class EventMarkers { function drawFunc(obj) { obj.show(); }, - function(obj) { + obj => { obj.remove(); }, - function(obj, position) { + (obj, position) => { obj.css({ top: position.top, left: position.left, @@ -601,11 +601,11 @@ export function init(this: any, plot) { const that = this; const eventMarkers = new EventMarkers(plot); - plot.getEvents = function() { + plot.getEvents = () => { return eventMarkers._events; }; - plot.hideEvents = function() { + plot.hideEvents = () => { $.each(eventMarkers._events, (index, event) => { event .visual() @@ -614,7 +614,7 @@ export function init(this: any, plot) { }); }; - plot.showEvents = function() { + plot.showEvents = () => { plot.hideEvents(); $.each(eventMarkers._events, (index, event) => { event.hide(); @@ -624,20 +624,20 @@ export function init(this: any, plot) { }; // change events on an existing plot - plot.setEvents = function(events) { + plot.setEvents = events => { if (eventMarkers.eventsEnabled) { eventMarkers.setupEvents(events); } }; - plot.hooks.processOptions.push(function(plot, options) { + plot.hooks.processOptions.push((plot, options) => { // enable the plugin if (options.events.data != null) { eventMarkers.eventsEnabled = true; } }); - plot.hooks.draw.push(function(plot) { + plot.hooks.draw.push(plot => { const options = plot.getOptions(); if (eventMarkers.eventsEnabled) { diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index ef5da3e296a..cf317389941 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -5,9 +5,9 @@ import baron from 'baron'; const module = angular.module('grafana.directives'); -module.directive('graphLegend', function(popoverSrv, $timeout) { +module.directive('graphLegend', (popoverSrv, $timeout) => { return { - link: function(scope, elem) { + link: (scope, elem) => { let firstRender = true; const ctrl = scope.ctrl; const panel = ctrl.panel; @@ -18,7 +18,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { const legendRightDefaultWidth = 10; const legendElem = elem.parent(); - scope.$on('$destroy', function() { + scope.$on('$destroy', () => { destroyScrollbar(); }); @@ -44,7 +44,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { const index = getSeriesIndexForElement(el); const series = seriesList[index]; - $timeout(function() { + $timeout(() => { popoverSrv.show({ element: el[0], position: 'bottom left', @@ -55,10 +55,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { openOn: 'hover', model: { series: series, - toggleAxis: function() { + toggleAxis: () => { ctrl.toggleAxis(series); }, - colorSelected: function(color) { + colorSelected: color => { ctrl.changeSeriesColor(series, color); }, }, @@ -154,7 +154,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } if (panel.legend.sort) { - seriesList = _.sortBy(seriesList, function(series) { + seriesList = _.sortBy(seriesList, series => { let sort = series.stats[panel.legend.sort]; if (sort === null) { sort = -Infinity; diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index 15e67f3083a..deb7bd8ba61 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -7,13 +7,13 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.currentOverrides = []; $scope.override = $scope.override || {}; - $scope.addOverrideOption = function(name, propertyName, values) { + $scope.addOverrideOption = (name, propertyName, values) => { const option = { text: name, propertyName: propertyName, index: $scope.overrideMenu.lenght, values: values, - submenu: _.map(values, function(value) { + submenu: _.map(values, value => { return { text: String(value), value: value }; }), }; @@ -21,7 +21,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.overrideMenu.push(option); }; - $scope.setOverride = function(item, subItem) { + $scope.setOverride = (item, subItem) => { // handle color overrides if (item.propertyName === 'color') { $scope.openColorSelector($scope.override['color']); @@ -41,13 +41,13 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.ctrl.render(); }; - $scope.colorSelected = function(color) { + $scope.colorSelected = color => { $scope.override['color'] = color; $scope.updateCurrentOverrides(); $scope.ctrl.render(); }; - $scope.openColorSelector = function(color) { + $scope.openColorSelector = color => { const fakeSeries = { color: color }; popoverSrv.show({ element: $element.find('.dropdown')[0], @@ -59,27 +59,27 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { colorSelected: $scope.colorSelected, series: fakeSeries, }, - onClose: function() { + onClose: () => { $scope.ctrl.render(); }, }); }; - $scope.removeOverride = function(option) { + $scope.removeOverride = option => { delete $scope.override[option.propertyName]; $scope.updateCurrentOverrides(); $scope.ctrl.refresh(); }; - $scope.getSeriesNames = function() { - return _.map($scope.ctrl.seriesList, function(series) { + $scope.getSeriesNames = () => { + return _.map($scope.ctrl.seriesList, series => { return series.alias; }); }; - $scope.updateCurrentOverrides = function() { + $scope.updateCurrentOverrides = () => { $scope.currentOverrides = []; - _.each($scope.overrideMenu, function(option) { + _.each($scope.overrideMenu, option => { const value = $scope.override[option.propertyName]; if (_.isUndefined(value)) { return; diff --git a/public/app/plugins/panel/graph/threshold_manager.ts b/public/app/plugins/panel/graph/threshold_manager.ts index 0a35d282b5e..46ec9e61854 100644 --- a/public/app/plugins/panel/graph/threshold_manager.ts +++ b/public/app/plugins/panel/graph/threshold_manager.ts @@ -61,7 +61,7 @@ export class ThresholdManager { handleElem.off('mouseleave', dragging); // trigger digest and render - panelCtrl.$scope.$apply(function() { + panelCtrl.$scope.$apply(() => { panelCtrl.render(); panelCtrl.events.emit('threshold-changed', { threshold: model, diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index fa3f3c7d114..5f1edb8aa9a 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -138,7 +138,7 @@ const template = `
    `; -coreModule.directive('graphThresholdForm', function() { +coreModule.directive('graphThresholdForm', () => { return { restrict: 'E', template: template, diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 005d1e3ca8c..628186569dd 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -16,17 +16,17 @@ const LEGEND_VALUE_MARGIN = 0; /** * Color legend for heatmap editor. */ -module.directive('colorLegend', function() { +module.directive('colorLegend', () => { return { restrict: 'E', template: '
    ', - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { const ctrl = scope.ctrl; const panel = scope.ctrl.panel; render(); - ctrl.events.on('render', function() { + ctrl.events.on('render', () => { render(); }); @@ -52,16 +52,16 @@ module.directive('colorLegend', function() { /** * Heatmap legend with scale values. */ -module.directive('heatmapLegend', function() { +module.directive('heatmapLegend', () => { return { restrict: 'E', template: `
    `, - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { const ctrl = scope.ctrl; const panel = scope.ctrl.panel; render(); - ctrl.events.on('render', function() { + ctrl.events.on('render', () => { render(); }); diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 5e48849ca59..4ff0176d0bf 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -267,7 +267,7 @@ export class HeatmapTooltip { countValueFormatter(decimals, scaledDecimals = null) { const format = 'short'; - return function(value) { + return value => { return kbn.valueFormats[format](value, decimals, scaledDecimals); }; } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index ba6921b5ec2..8092eaaeb9a 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -436,7 +436,7 @@ export class HeatmapRenderer { tickValueFormatter(decimals, scaledDecimals = null) { const format = this.panel.yAxis.format; - return function(value) { + return value => { try { return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; } catch (err) { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 6b244cd577f..fe79b5f5043 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -528,7 +528,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }, value: { color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, - formatter: function() { + formatter: () => { return getValueText(); }, font: { @@ -617,7 +617,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { data = ctrl.data; // get thresholds - data.thresholds = panel.thresholds.split(',').map(function(strVale) { + data.thresholds = panel.thresholds.split(',').map(strVale => { return Number(strVale.trim()); }); data.colorMap = panel.colors; @@ -662,16 +662,16 @@ class SingleStatCtrl extends MetricsPanelCtrl { // drilldown link tooltip const drilldownTooltip = $('
    hello
    "'); - elem.mouseleave(function() { + elem.mouseleave(() => { if (panel.links.length === 0) { return; } - $timeout(function() { + $timeout(() => { drilldownTooltip.detach(); }); }); - elem.click(function(evt) { + elem.click(evt => { if (!linkInfo) { return; } @@ -688,7 +688,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { if (linkInfo.href.indexOf('http') === 0) { window.location.href = linkInfo.href; } else { - $timeout(function() { + $timeout(() => { $location.url(linkInfo.href); }); } @@ -696,7 +696,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { drilldownTooltip.detach(); }); - elem.mousemove(function(e) { + elem.mousemove(e => { if (!linkInfo) { return; } @@ -708,7 +708,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { hookupDrilldownLinkTooltip(); - this.events.on('render', function() { + this.events.on('render', () => { render(); ctrl.renderingCompleted(); }); diff --git a/public/app/plugins/panel/table/column_options.ts b/public/app/plugins/panel/table/column_options.ts index 205b988f557..ca6d271643b 100644 --- a/public/app/plugins/panel/table/column_options.ts +++ b/public/app/plugins/panel/table/column_options.ts @@ -48,7 +48,7 @@ export class ColumnOptionsCtrl { if (!this.panelCtrl.table) { return []; } - return _.map(this.panelCtrl.table.columns, function(col: any) { + return _.map(this.panelCtrl.table.columns, (col: any) => { return col.text; }); }; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 68080c0558d..193a9b47c54 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -257,13 +257,13 @@ class TablePanelCtrl extends MetricsPanelCtrl { elem.on('click', '.table-panel-page-link', switchPage); elem.on('click', '.table-panel-filter-link', addFilterClicked); - const unbindDestroy = scope.$on('$destroy', function() { + const unbindDestroy = scope.$on('$destroy', () => { elem.off('click', '.table-panel-page-link'); elem.off('click', '.table-panel-filter-link'); unbindDestroy(); }); - ctrl.events.on('render', function(renderData) { + ctrl.events.on('render', renderData => { data = renderData || data; if (data) { renderPanel(); diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 428fbff05cf..5a75fa7acf6 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -7,10 +7,10 @@ const transformers = {}; transformers['timeseries_to_rows'] = { description: 'Time series to rows', - getColumns: function() { + getColumns: () => { return []; }, - transform: function(data, panel, model) { + transform: (data, panel, model) => { model.columns = [{ text: 'Time', type: 'date' }, { text: 'Metric' }, { text: 'Value' }]; for (let i = 0; i < data.length; i++) { @@ -25,10 +25,10 @@ transformers['timeseries_to_rows'] = { transformers['timeseries_to_columns'] = { description: 'Time series to columns', - getColumns: function() { + getColumns: () => { return []; }, - transform: function(data, panel, model) { + transform: (data, panel, model) => { model.columns.push({ text: 'Time', type: 'date' }); // group by time @@ -67,7 +67,7 @@ transformers['timeseries_to_columns'] = { transformers['timeseries_aggregations'] = { description: 'Time series aggregations', - getColumns: function() { + getColumns: () => { return [ { text: 'Avg', value: 'avg' }, { text: 'Min', value: 'min' }, @@ -77,7 +77,7 @@ transformers['timeseries_aggregations'] = { { text: 'Count', value: 'count' }, ]; }, - transform: function(data, panel, model) { + transform: (data, panel, model) => { let i, y; model.columns.push({ text: 'Metric' }); @@ -105,10 +105,10 @@ transformers['timeseries_aggregations'] = { transformers['annotations'] = { description: 'Annotations', - getColumns: function() { + getColumns: () => { return []; }, - transform: function(data, panel, model) { + transform: (data, panel, model) => { model.columns.push({ text: 'Time', type: 'date' }); model.columns.push({ text: 'Title' }); model.columns.push({ text: 'Text' }); @@ -127,7 +127,7 @@ transformers['annotations'] = { transformers['table'] = { description: 'Table', - getColumns: function(data) { + getColumns: data => { if (!data || data.length === 0) { return []; } @@ -154,7 +154,7 @@ transformers['table'] = { return columns; }, - transform: function(data, panel, model) { + transform: (data, panel, model) => { if (!data || data.length === 0) { return; } @@ -264,7 +264,7 @@ transformers['table'] = { transformers['json'] = { description: 'JSON Data', - getColumns: function(data) { + getColumns: data => { if (!data || data.length === 0) { return []; } @@ -287,11 +287,11 @@ transformers['json'] = { } } - return _.map(names, function(value, key) { + return _.map(names, (value, key) => { return { text: key, value: key }; }); }, - transform: function(data, panel, model) { + transform: (data, panel, model) => { let i, y, z; for (const column of panel.columns) { From 22510be450a7930a9a907c4f84385ac75fa79fcc Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 4 Sep 2018 15:00:04 +0200 Subject: [PATCH 0087/2611] 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 0088/2611] 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 dc4f547a40c3907d77d352213d26ca8d70535438 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 4 Sep 2018 17:02:32 +0200 Subject: [PATCH 0089/2611] Changed functions to arrow functions for only-arrow-functions rule. --- .../components/code_editor/code_editor.ts | 4 +- .../components/colorpicker/spectrum_picker.ts | 2 +- .../components/json_explorer/json_explorer.ts | 4 +- .../app/core/components/jsontree/jsontree.ts | 2 +- public/app/core/directives/dash_class.ts | 10 +- .../app/core/directives/dropdown_typeahead.ts | 60 +++++------ public/app/core/directives/give_focus.ts | 10 +- public/app/core/directives/metric_segment.ts | 40 +++---- public/app/core/directives/misc.ts | 26 ++--- .../app/core/directives/ng_model_on_blur.ts | 14 +-- .../app/core/directives/rebuild_on_change.ts | 6 +- public/app/core/directives/tags.ts | 10 +- .../core/directives/value_select_dropdown.ts | 6 +- public/app/core/jquery_extended.ts | 4 +- public/app/core/partials.ts | 2 +- public/app/core/profiler.ts | 6 +- public/app/core/services/analytics.ts | 1 + public/app/core/services/context_srv.ts | 2 +- public/app/core/services/ng_react.ts | 27 ++--- public/app/core/table_model.ts | 2 +- public/app/core/utils/file_export.ts | 4 +- public/app/core/utils/flatten.ts | 2 +- public/app/core/utils/kbn.ts | 102 +++++++++--------- public/app/core/utils/outline.ts | 8 +- public/app/core/utils/rangeutil.ts | 2 +- public/app/core/utils/url.ts | 6 +- .../features/dashboard/dashboard_migration.ts | 28 ++--- .../app/features/dashboard/dashboard_model.ts | 6 +- public/app/features/panel/panel_ctrl.ts | 2 +- .../datasource/graphite/add_graphite_func.ts | 24 ++--- .../plugins/datasource/graphite/datasource.ts | 16 +-- .../datasource/graphite/func_editor.ts | 22 ++-- .../app/plugins/datasource/graphite/gfunc.ts | 4 +- .../app/plugins/datasource/graphite/lexer.ts | 2 +- .../opentsdb/specs/datasource.test.ts | 18 ++-- public/app/stores/FolderStore/FolderStore.ts | 2 +- 36 files changed, 244 insertions(+), 242 deletions(-) diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 753f7a5a330..50ff55f3083 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -97,7 +97,7 @@ function link(scope, elem, attrs) { textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { - setTimeout(function() { + setTimeout(() => { textarea.focus(); const domEl = textarea[0]; if (domEl.setSelectionRange) { @@ -119,7 +119,7 @@ function link(scope, elem, attrs) { scope.$watch('content', (newValue, oldValue) => { const editorValue = codeEditor.getValue(); if (newValue !== editorValue && newValue !== oldValue) { - scope.$$postDigest(function() { + scope.$$postDigest(() => { setEditorContent(newValue); }); } diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts index 6e93a4f39f4..4576648df83 100644 --- a/public/app/core/components/colorpicker/spectrum_picker.ts +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -13,7 +13,7 @@ export function spectrumPicker() { scope: true, replace: true, template: '', - link: function(scope, element, attrs, ngModel) { + link: (scope, element, attrs, ngModel) => { scope.ngModel = ngModel; scope.onColorChange = color => { ngModel.$setViewValue(color); diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 779e5a93cba..9a344d3195b 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -14,10 +14,10 @@ const MAX_ANIMATED_TOGGLE_ITEMS = 10; const requestAnimationFrame = window.requestAnimationFrame || - function(cb: () => void) { + ((cb: () => void) => { cb(); return 0; - }; + }); export interface JsonExplorerConfig { animateOpen?: boolean; diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index 5fbda5560b3..4bcb2f632c2 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -10,7 +10,7 @@ coreModule.directive('jsonTree', [ startExpanded: '@', rootName: '@', }, - link: function(scope, elem) { + link: (scope, elem) => { const jsonExp = new JsonExplorer(scope.object, 3, { animateOpen: true, }); diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 031338d3c5b..224bc2c772d 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -4,19 +4,19 @@ import coreModule from '../core_module'; /** @ngInject */ export function dashClass() { return { - link: function($scope, elem) { - $scope.onAppEvent('panel-fullscreen-enter', function() { + link: ($scope, elem) => { + $scope.onAppEvent('panel-fullscreen-enter', () => { elem.toggleClass('panel-in-fullscreen', true); }); - $scope.onAppEvent('panel-fullscreen-exit', function() { + $scope.onAppEvent('panel-fullscreen-exit', () => { elem.toggleClass('panel-in-fullscreen', false); }); - $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) { + $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => { if (newValue) { elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); - setTimeout(function() { + setTimeout(() => { elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); }, 10); } else { diff --git a/public/app/core/directives/dropdown_typeahead.ts b/public/app/core/directives/dropdown_typeahead.ts index af8c4ddc3bb..cdba0f3e3c2 100644 --- a/public/app/core/directives/dropdown_typeahead.ts +++ b/public/app/core/directives/dropdown_typeahead.ts @@ -20,7 +20,7 @@ export function dropdownTypeahead($compile) { dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect', model: '=ngModel', }, - link: function($scope, elem, attrs) { + link: ($scope, elem, attrs) => { const $input = $(inputTemplate); const $button = $(buttonTemplate); $input.appendTo(elem); @@ -31,9 +31,9 @@ export function dropdownTypeahead($compile) { } if (attrs.ngModel) { - $scope.$watch('model', function(newValue) { - _.each($scope.menuItems, function(item) { - _.each(item.submenu, function(subItem) { + $scope.$watch('model', newValue => { + _.each($scope.menuItems, item => { + _.each(item.submenu, subItem => { if (subItem.value === newValue) { $button.html(subItem.text); } @@ -44,12 +44,12 @@ export function dropdownTypeahead($compile) { const typeaheadValues = _.reduce( $scope.menuItems, - function(memo, value, index) { + (memo, value, index) => { if (!value.submenu) { value.click = 'menuItemSelected(' + index + ')'; memo.push(value.text); } else { - _.each(value.submenu, function(item, subIndex) { + _.each(value.submenu, (item, subIndex) => { item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; memo.push(value.text + ' ' + item.text); }); @@ -59,7 +59,7 @@ export function dropdownTypeahead($compile) { [] ); - $scope.menuItemSelected = function(index, subIndex) { + $scope.menuItemSelected = (index, subIndex) => { const menuItem = $scope.menuItems[index]; const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { @@ -73,10 +73,10 @@ export function dropdownTypeahead($compile) { source: typeaheadValues, minLength: 1, items: 10, - updater: function(value) { + updater: value => { const result: any = {}; - _.each($scope.menuItems, function(menuItem) { - _.each(menuItem.submenu, function(submenuItem) { + _.each($scope.menuItems, menuItem => { + _.each(menuItem.submenu, submenuItem => { if (value === menuItem.text + ' ' + submenuItem.text) { result.$subItem = submenuItem; result.$item = menuItem; @@ -85,7 +85,7 @@ export function dropdownTypeahead($compile) { }); if (result.$item) { - $scope.$apply(function() { + $scope.$apply(() => { $scope.dropdownTypeaheadOnSelect(result); }); } @@ -95,24 +95,24 @@ export function dropdownTypeahead($compile) { }, }); - $button.click(function() { + $button.click(() => { $button.hide(); $input.show(); $input.focus(); }); - $input.keyup(function() { + $input.keyup(() => { elem.toggleClass('open', $input.val() === ''); }); - $input.blur(function() { + $input.blur(() => { $input.hide(); $input.val(''); $button.show(); $button.focus(); // clicking the function dropdown menu won't // work if you remove class at once - setTimeout(function() { + setTimeout(() => { elem.removeClass('open'); }, 200); }); @@ -138,7 +138,7 @@ export function dropdownTypeahead2($compile) { dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect', model: '=ngModel', }, - link: function($scope, elem, attrs) { + link: ($scope, elem, attrs) => { const $input = $(inputTemplate); const $button = $(buttonTemplate); $input.appendTo(elem); @@ -149,9 +149,9 @@ export function dropdownTypeahead2($compile) { } if (attrs.ngModel) { - $scope.$watch('model', function(newValue) { - _.each($scope.menuItems, function(item) { - _.each(item.submenu, function(subItem) { + $scope.$watch('model', newValue => { + _.each($scope.menuItems, item => { + _.each(item.submenu, subItem => { if (subItem.value === newValue) { $button.html(subItem.text); } @@ -162,12 +162,12 @@ export function dropdownTypeahead2($compile) { const typeaheadValues = _.reduce( $scope.menuItems, - function(memo, value, index) { + (memo, value, index) => { if (!value.submenu) { value.click = 'menuItemSelected(' + index + ')'; memo.push(value.text); } else { - _.each(value.submenu, function(item, subIndex) { + _.each(value.submenu, (item, subIndex) => { item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; memo.push(value.text + ' ' + item.text); }); @@ -177,7 +177,7 @@ export function dropdownTypeahead2($compile) { [] ); - $scope.menuItemSelected = function(index, subIndex) { + $scope.menuItemSelected = (index, subIndex) => { const menuItem = $scope.menuItems[index]; const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { @@ -191,10 +191,10 @@ export function dropdownTypeahead2($compile) { source: typeaheadValues, minLength: 1, items: 10, - updater: function(value) { + updater: value => { const result: any = {}; - _.each($scope.menuItems, function(menuItem) { - _.each(menuItem.submenu, function(submenuItem) { + _.each($scope.menuItems, menuItem => { + _.each(menuItem.submenu, submenuItem => { if (value === menuItem.text + ' ' + submenuItem.text) { result.$subItem = submenuItem; result.$item = menuItem; @@ -203,7 +203,7 @@ export function dropdownTypeahead2($compile) { }); if (result.$item) { - $scope.$apply(function() { + $scope.$apply(() => { $scope.dropdownTypeaheadOnSelect(result); }); } @@ -213,24 +213,24 @@ export function dropdownTypeahead2($compile) { }, }); - $button.click(function() { + $button.click(() => { $button.hide(); $input.show(); $input.focus(); }); - $input.keyup(function() { + $input.keyup(() => { elem.toggleClass('open', $input.val() === ''); }); - $input.blur(function() { + $input.blur(() => { $input.hide(); $input.val(''); $button.show(); $button.focus(); // clicking the function dropdown menu won't // work if you remove class at once - setTimeout(function() { + setTimeout(() => { elem.removeClass('open'); }, 200); }); diff --git a/public/app/core/directives/give_focus.ts b/public/app/core/directives/give_focus.ts index 9b2cf01750e..4ef574ec68e 100644 --- a/public/app/core/directives/give_focus.ts +++ b/public/app/core/directives/give_focus.ts @@ -1,18 +1,18 @@ import coreModule from '../core_module'; -coreModule.directive('giveFocus', function() { - return function(scope, element, attrs) { - element.click(function(e) { +coreModule.directive('giveFocus', () => { + return (scope, element, attrs) => { + element.click(e => { e.stopPropagation(); }); scope.$watch( attrs.giveFocus, - function(newValue) { + newValue => { if (!newValue) { return; } - setTimeout(function() { + setTimeout(() => { element.focus(); const domEl = element[0]; if (domEl.setSelectionRange) { diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index 117f776f487..7759e14f2cc 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -24,7 +24,7 @@ export function metricSegment($compile, $sce) { onChange: '&', debounce: '@', }, - link: function($scope, elem) { + link: ($scope, elem) => { const $input = $(inputTemplate); const segment = $scope.segment; const $button = $(segment.selectMode ? selectTemplate : linkTemplate); @@ -36,14 +36,14 @@ export function metricSegment($compile, $sce) { $input.appendTo(elem); $button.appendTo(elem); - $scope.updateVariableValue = function(value) { + $scope.updateVariableValue = value => { if (value === '' || segment.value === value) { return; } value = _.unescape(value); - $scope.$apply(function() { + $scope.$apply(() => { const selected = _.find($scope.altSegments, { value: value }); if (selected) { segment.value = selected.value; @@ -65,7 +65,7 @@ export function metricSegment($compile, $sce) { }); }; - $scope.switchToLink = function(fromClick) { + $scope.switchToLink = fromClick => { if (linkMode && !fromClick) { return; } @@ -78,17 +78,17 @@ export function metricSegment($compile, $sce) { $scope.updateVariableValue($input.val()); }; - $scope.inputBlur = function() { + $scope.inputBlur = () => { // happens long before the click event on the typeahead options // need to have long delay because the blur cancelBlur = setTimeout($scope.switchToLink, 200); }; - $scope.source = function(query, callback) { - $scope.$apply(function() { - $scope.getOptions({ $query: query }).then(function(altSegments) { + $scope.source = (query, callback) => { + $scope.$apply(() => { + $scope.getOptions({ $query: query }).then(altSegments => { $scope.altSegments = altSegments; - options = _.map($scope.altSegments, function(alt) { + options = _.map($scope.altSegments, alt => { return _.escape(alt.value); }); @@ -104,7 +104,7 @@ export function metricSegment($compile, $sce) { }); }; - $scope.updater = function(value) { + $scope.updater = value => { if (value === segment.value) { clearTimeout(cancelBlur); $input.focus(); @@ -152,14 +152,14 @@ export function metricSegment($compile, $sce) { typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true }); } - $button.keydown(function(evt) { + $button.keydown(evt => { // trigger typeahead on down arrow or enter key if (evt.keyCode === 40 || evt.keyCode === 13) { $button.click(); } }); - $button.click(function() { + $button.click(() => { options = null; $input.css('width', Math.max($button.width(), 80) + 16 + 'px'); @@ -199,7 +199,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) { pre: function postLink($scope, elem, attrs) { let cachedOptions; - $scope.valueToSegment = function(value) { + $scope.valueToSegment = value => { const option = _.find($scope.options, { value: value }); const segment = { cssClass: attrs.cssClass, @@ -211,18 +211,18 @@ export function metricSegmentModel(uiSegmentSrv, $q) { return uiSegmentSrv.newSegment(segment); }; - $scope.getOptionsInternal = function() { + $scope.getOptionsInternal = () => { if ($scope.options) { cachedOptions = $scope.options; return $q.when( - _.map($scope.options, function(option) { + _.map($scope.options, option => { return { value: option.text }; }) ); } else { - return $scope.getOptions().then(function(options) { + return $scope.getOptions().then(options => { cachedOptions = options; - return _.map(options, function(option) { + return _.map(options, option => { if (option.html) { return option; } @@ -232,7 +232,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) { } }; - $scope.onSegmentChange = function() { + $scope.onSegmentChange = () => { if (cachedOptions) { const option = _.find(cachedOptions, { text: $scope.segment.value }); if (option && option.value !== $scope.property) { @@ -246,8 +246,8 @@ export function metricSegmentModel(uiSegmentSrv, $q) { // needs to call this after digest so // property is synced with outerscope - $scope.$$postDigest(function() { - $scope.$apply(function() { + $scope.$$postDigest(() => { + $scope.$apply(() => { $scope.onChange(); }); }); diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 07ba3263763..192e2df4167 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -8,7 +8,7 @@ import { appEvents } from 'app/core/core'; function tip($compile) { return { restrict: 'E', - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { let _t = '' + attrs.tip + '' : ''; const showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : ''; @@ -118,7 +118,7 @@ function editorOptBool($compile) { function editorCheckbox($compile, $interpolate) { return { restrict: 'E', - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { const text = $interpolate(attrs.text)(scope); const model = $interpolate(attrs.model)(scope); const ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; @@ -194,7 +194,7 @@ function gfDropdown($parse, $compile, $timeout) { link: function postLink(scope, iElement, iAttrs) { const getter = $parse(iAttrs.gfDropdown), items = getter(scope); - $timeout(function() { + $timeout(() => { const placement = iElement.data('placement'); const dropdown = angular.element(buildTemplate(items, placement).join('')); dropdown.insertAfter(iElement); diff --git a/public/app/core/directives/ng_model_on_blur.ts b/public/app/core/directives/ng_model_on_blur.ts index 2818f620dde..7e903c1f889 100644 --- a/public/app/core/directives/ng_model_on_blur.ts +++ b/public/app/core/directives/ng_model_on_blur.ts @@ -6,14 +6,14 @@ function ngModelOnBlur() { restrict: 'A', priority: 1, require: 'ngModel', - link: function(scope, elm, attr, ngModelCtrl) { + link: (scope, elm, attr, ngModelCtrl) => { if (attr.type === 'radio' || attr.type === 'checkbox') { return; } elm.off('input keydown change'); - elm.bind('blur', function() { - scope.$apply(function() { + elm.bind('blur', () => { + scope.$apply(() => { ngModelCtrl.$setViewValue(elm.val()); }); }); @@ -25,8 +25,8 @@ function emptyToNull() { return { restrict: 'A', require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.push(function(viewValue) { + link: (scope, elm, attrs, ctrl) => { + ctrl.$parsers.push(viewValue => { if (viewValue === '') { return null; } @@ -39,8 +39,8 @@ function emptyToNull() { function validTimeSpan() { return { require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$validators.integer = function(modelValue, viewValue) { + link: (scope, elm, attrs, ctrl) => { + ctrl.$validators.integer = (modelValue, viewValue) => { if (ctrl.$isEmpty(modelValue)) { return true; } diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts index 72b9c05064a..378c32b14f7 100644 --- a/public/app/core/directives/rebuild_on_change.ts +++ b/public/app/core/directives/rebuild_on_change.ts @@ -26,7 +26,7 @@ function rebuildOnChange($animate) { transclude: true, priority: 600, restrict: 'E', - link: function(scope, elem, attrs, ctrl, transclude) { + link: (scope, elem, attrs, ctrl, transclude) => { let block, childScope, previousElements; function cleanUp() { @@ -40,7 +40,7 @@ function rebuildOnChange($animate) { } if (block) { previousElements = getBlockNodes(block.clone); - $animate.leave(previousElements).then(function() { + $animate.leave(previousElements).then(() => { previousElements = null; }); block = null; @@ -53,7 +53,7 @@ function rebuildOnChange($animate) { } if (!childScope && (value || attrs.showNull)) { - transclude(function(clone, newScope) { + transclude((clone, newScope) => { childScope = newScope; clone[clone.length++] = document.createComment(' end rebuild on change '); block = { clone: clone }; diff --git a/public/app/core/directives/tags.ts b/public/app/core/directives/tags.ts index 00da9105e5f..33a2252a683 100644 --- a/public/app/core/directives/tags.ts +++ b/public/app/core/directives/tags.ts @@ -13,7 +13,7 @@ function setColor(name, element) { function tagColorFromName() { return { scope: { tagColorFromName: '=' }, - link: function(scope, element) { + link: (scope, element) => { setColor(scope.tagColorFromName, element); }, }; @@ -29,7 +29,7 @@ function bootstrapTagsinput() { return scope.$parent[property]; } - return function(item) { + return item => { return item[property]; }; } @@ -64,7 +64,7 @@ function bootstrapTagsinput() { itemText: getItemProperty(scope, attrs.itemtext), tagClass: angular.isFunction(scope.$parent[attrs.tagclass]) ? scope.$parent[attrs.tagclass] - : function() { + : () => { return attrs.tagclass; }, }); @@ -85,7 +85,7 @@ function bootstrapTagsinput() { setColor(event.item, tagElement); }); - select.on('itemRemoved', function(event) { + select.on('itemRemoved', event => { const idx = scope.model.indexOf(event.item); if (idx !== -1) { scope.model.splice(idx, 1); @@ -97,7 +97,7 @@ function bootstrapTagsinput() { scope.$watch( 'model', - function() { + () => { if (!angular.isArray(scope.model)) { scope.model = []; } diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index 69504c1bb1b..a75ecd46ad0 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -245,7 +245,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { controller: 'ValueSelectDropdownCtrl', controllerAs: 'vm', bindToController: true, - link: function(scope, elem) { + link: (scope, elem) => { const bodyEl = angular.element($window.document.body); const linkEl = elem.find('.variable-value-link'); const inputEl = elem.find('input'); @@ -258,7 +258,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { inputEl.focus(); $timeout( - function() { + () => { bodyEl.on('click', bodyOnClick); }, 0, @@ -274,7 +274,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { function bodyOnClick(e) { if (elem.has(e.target).length === 0) { - scope.$apply(function() { + scope.$apply(() => { scope.vm.commitChanges(); }); } diff --git a/public/app/core/jquery_extended.ts b/public/app/core/jquery_extended.ts index 241baa1af22..fa9b1aeb823 100644 --- a/public/app/core/jquery_extended.ts +++ b/public/app/core/jquery_extended.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; const $win = $(window); -$.fn.place_tt = (function() { +$.fn.place_tt = (() => { const defaults = { offset: 5, }; @@ -28,7 +28,7 @@ $.fn.place_tt = (function() { .invoke([ '$compile', '$rootScope', - function($compile, $rootScope) { + ($compile, $rootScope) => { const tmpScope = $rootScope.$new(true); _.extend(tmpScope, opts.scopeData); diff --git a/public/app/core/partials.ts b/public/app/core/partials.ts index 64b0b11b8ca..864a3dcfa8a 100644 --- a/public/app/core/partials.ts +++ b/public/app/core/partials.ts @@ -1,4 +1,4 @@ let templates = (require as any).context('../', true, /\.html$/); -templates.keys().forEach(function(key) { +templates.keys().forEach(key => { templates(key); }); diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts index 9ad451353d2..0e738dd3da1 100644 --- a/public/app/core/profiler.ts +++ b/public/app/core/profiler.ts @@ -82,15 +82,15 @@ export class Profiler { let scopes = 0; const root = $(document.getElementsByTagName('body')); - const f = function(element) { + const f = element => { if (element.data().hasOwnProperty('$scope')) { scopes++; - angular.forEach(element.data().$scope.$$watchers, function() { + angular.forEach(element.data().$scope.$$watchers, () => { count++; }); } - angular.forEach(element.children(), function(childElement) { + angular.forEach(element.children(), childElement => { f($(childElement)); }); }; diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index a0faf4016fd..d50140bbd75 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -15,6 +15,7 @@ export class Analytics { const ga = ((window as any).ga = (window as any).ga || function() { + //tslint:disable-line:only-arrow-functions (ga.q = ga.q || []).push(arguments); }); ga.l = +new Date(); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index c2742c82958..97200e1aed3 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -62,6 +62,6 @@ export class ContextSrv { const contextSrv = new ContextSrv(); export { contextSrv }; -coreModule.factory('contextSrv', function() { +coreModule.factory('contextSrv', () => { return contextSrv; }); diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 4036b4bd1fd..643e34dd62e 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -34,7 +34,7 @@ function getReactComponent(name, $injector) { if (!reactComponent) { try { - reactComponent = name.split('.').reduce(function(current, namePart) { + reactComponent = name.split('.').reduce((current, namePart) => { return current[namePart]; }, window); } catch (e) {} @@ -53,12 +53,13 @@ function applied(fn, scope) { return fn; } const wrapped: any = function() { + //tslint:disable-line:only-arrow-functions const args = arguments; const phase = scope.$root.$$phase; if (phase === '$apply' || phase === '$digest') { return fn.apply(null, args); } else { - return scope.$apply(function() { + return scope.$apply(() => { return fn.apply(null, args); }); } @@ -80,7 +81,7 @@ function applied(fn, scope) { * @returns {Object} props with the functions wrapped in scope.$apply */ function applyFunctions(obj, scope, propsConfig?) { - return Object.keys(obj || {}).reduce(function(prev, key) { + return Object.keys(obj || {}).reduce((prev, key) => { const value = obj[key]; const config = (propsConfig || {})[key] || {}; /** @@ -108,7 +109,7 @@ function watchProps(watchDepth, scope, watchExpressions, listener) { const watchGroupExpressions = []; - watchExpressions.forEach(function(expr) { + watchExpressions.forEach(expr => { const actualExpr = getPropExpression(expr); const exprWatchDepth = getPropWatchDepth(watchDepth, expr); @@ -134,7 +135,7 @@ function watchProps(watchDepth, scope, watchExpressions, listener) { // render React component, with scope[attrs.props] being passed in as the component props function renderComponent(component, props, scope, elem) { - scope.$evalAsync(function() { + scope.$evalAsync(() => { ReactDOM.render(React.createElement(component, props), elem[0]); }); } @@ -156,7 +157,7 @@ function getPropExpression(prop) { // find the normalized attribute knowing that React props accept any type of capitalization function findAttribute(attrs, propName) { - const index = Object.keys(attrs).filter(function(attr) { + const index = Object.keys(attrs).filter(attr => { return attr.toLowerCase() === propName.toLowerCase(); })[0]; return attrs[index]; @@ -186,14 +187,14 @@ function getPropWatchDepth(defaultWatch, prop) { // } // })); // -const reactComponent = function($injector) { +const reactComponent = $injector => { return { restrict: 'E', replace: true, link: function(scope, elem, attrs) { const reactComponent = getReactComponent(attrs.name, $injector); - const renderMyComponent = function() { + const renderMyComponent = () => { const scopeProps = scope.$eval(attrs.props); const props = applyFunctions(scopeProps, scope); @@ -243,8 +244,8 @@ const reactComponent = function($injector) { // // // -const reactDirective = function($injector) { - return function(reactComponentName, props, conf, injectableProps) { +const reactDirective = $injector => { + return (reactComponentName, props, conf, injectableProps) => { const directive = { restrict: 'E', replace: true, @@ -255,11 +256,11 @@ const reactDirective = function($injector) { props = props || Object.keys(reactComponent.propTypes || {}); // for each of the properties, get their scope value and set it to scope.props - const renderMyComponent = function() { + const renderMyComponent = () => { let scopeProps = {}; const config = {}; - props.forEach(function(prop) { + props.forEach(prop => { const propName = getPropName(prop); scopeProps[propName] = scope.$eval(findAttribute(attrs, propName)); config[propName] = getPropConfig(prop); @@ -272,7 +273,7 @@ const reactDirective = function($injector) { // watch each property name and trigger an update whenever something changes, // to update scope.props with new values - const propExpressions = props.map(function(prop) { + const propExpressions = props.map(prop => { return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop]; }); diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 0c85a0293dd..f8b96d0537b 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -26,7 +26,7 @@ export default class TableModel { return; } - this.rows.sort(function(a, b) { + this.rows.sort((a, b) => { a = a[options.col]; b = b[options.col]; // Sort null or undefined seperately from comparable values diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 298a06c64fd..4fbdea0f953 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -84,7 +84,7 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU formatSpecialHeader(excel) + formatRow( ['Time'].concat( - seriesList.map(function(val) { + seriesList.map(val => { return val.alias; }) ) @@ -97,7 +97,7 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU const timestamp = moment(seriesList[0].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat); text += formatRow( [timestamp].concat( - seriesList.map(function(series) { + seriesList.map(series => { return series.datapoints[i][POINT_VALUE_INDEX]; }) ), diff --git a/public/app/core/utils/flatten.ts b/public/app/core/utils/flatten.ts index 3350f5f6c33..38601f463aa 100644 --- a/public/app/core/utils/flatten.ts +++ b/public/app/core/utils/flatten.ts @@ -10,7 +10,7 @@ export default function flatten(target, opts): any { const output = {}; function step(object, prev) { - Object.keys(object).forEach(function(key) { + Object.keys(object).forEach(key => { const value = object[key]; const isarray = opts.safe && Array.isArray(value); const type = Object.prototype.toString.call(value); diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 8b276acb539..bd69f2e89d9 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -5,13 +5,13 @@ const kbn: any = {}; kbn.valueFormats = {}; -kbn.regexEscape = function(value) { +kbn.regexEscape = value => { return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&'); }; ///// HELPER FUNCTIONS ///// -kbn.round_interval = function(interval) { +kbn.round_interval = interval => { switch (true) { // 0.015s case interval < 15: @@ -102,7 +102,7 @@ kbn.round_interval = function(interval) { } }; -kbn.secondsToHms = function(seconds) { +kbn.secondsToHms = seconds => { const numyears = Math.floor(seconds / 31536000); if (numyears) { return numyears + 'y'; @@ -131,7 +131,7 @@ kbn.secondsToHms = function(seconds) { return 'less than a millisecond'; //'just now' //or other string you like; }; -kbn.secondsToHhmmss = function(seconds) { +kbn.secondsToHhmmss = seconds => { const strings = []; const numhours = Math.floor(seconds / 3600); const numminutes = Math.floor((seconds % 3600) / 60); @@ -142,11 +142,11 @@ kbn.secondsToHhmmss = function(seconds) { return strings.join(':'); }; -kbn.to_percent = function(nr, outof) { +kbn.to_percent = (nr, outof) => { return Math.floor(nr / outof * 10000) / 100 + '%'; }; -kbn.addslashes = function(str) { +kbn.addslashes = str => { str = str.replace(/\\/g, '\\\\'); str = str.replace(/\'/g, "\\'"); str = str.replace(/\"/g, '\\"'); @@ -168,7 +168,7 @@ kbn.intervals_in_seconds = { ms: 0.001, }; -kbn.calculateInterval = function(range, resolution, lowLimitInterval) { +kbn.calculateInterval = (range, resolution, lowLimitInterval) => { let lowLimitMs = 1; // 1 millisecond default low limit let intervalMs; @@ -190,7 +190,7 @@ kbn.calculateInterval = function(range, resolution, lowLimitInterval) { }; }; -kbn.describe_interval = function(str) { +kbn.describe_interval = str => { const matches = str.match(kbn.interval_regex); if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) { throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"'); @@ -203,17 +203,17 @@ kbn.describe_interval = function(str) { } }; -kbn.interval_to_ms = function(str) { +kbn.interval_to_ms = str => { const info = kbn.describe_interval(str); return info.sec * 1000 * info.count; }; -kbn.interval_to_seconds = function(str) { +kbn.interval_to_seconds = str => { const info = kbn.describe_interval(str); return info.sec * info.count; }; -kbn.query_color_dot = function(color, diameter) { +kbn.query_color_dot = (color, diameter) => { return ( '
      - - Date: Tue, 4 Sep 2018 22:38:18 -0700 Subject: [PATCH 0092/2611] 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 72ab24f3008fdf15cc8d2c6622b58733ad6dd50e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 5 Sep 2018 07:47:30 +0200 Subject: [PATCH 0093/2611] Changed functions to arrow functions for only-arrow-functions rule. (#13131) --- public/app/app.ts | 8 +- .../app/containers/Explore/utils/debounce.ts | 2 +- public/app/core/components/gf_page.ts | 2 +- .../app/core/components/scroll/page_scroll.ts | 2 +- public/app/core/components/scroll/scroll.ts | 2 +- public/app/core/controllers/invited_ctrl.ts | 8 +- public/app/core/controllers/login_ctrl.ts | 24 +-- .../core/controllers/reset_password_ctrl.ts | 8 +- public/app/core/filters/filters.ts | 24 +-- public/app/core/lodash_extended.ts | 2 +- public/app/core/services/alert_srv.ts | 2 +- public/app/core/services/popover_srv.ts | 6 +- public/app/core/services/segment_srv.ts | 30 ++-- public/app/core/services/util_srv.ts | 6 +- public/app/core/specs/backend_srv.test.ts | 2 +- public/app/core/specs/datemath.test.ts | 12 +- public/app/core/specs/file_export.test.ts | 2 +- public/app/core/specs/kbn.test.ts | 162 +++++++++--------- public/app/core/specs/time_series.test.ts | 144 ++++++++-------- public/app/core/utils/css_loader.ts | 20 +-- .../app/features/dashboard/change_tracker.ts | 2 +- .../dashboard/dashboard_loader_srv.ts | 2 +- .../dashboard/repeat_option/repeat_option.ts | 4 +- .../app/features/dashboard/shareModalCtrl.ts | 8 +- .../features/dashboard/share_snapshot_ctrl.ts | 36 ++-- .../dashboard/timepicker/input_date.ts | 6 +- public/app/features/dashboard/upload.ts | 8 +- .../app/features/dashboard/view_state_srv.ts | 14 +- public/app/features/dashlinks/module.ts | 18 +- .../app/features/org/change_password_ctrl.ts | 4 +- public/app/features/org/new_org_ctrl.ts | 6 +- public/app/features/org/org_api_keys_ctrl.ts | 12 +- public/app/features/org/select_org_ctrl.ts | 10 +- public/app/features/panel/panel_directive.ts | 14 +- public/app/features/panel/panel_header.ts | 4 +- .../features/panel/query_troubleshooter.ts | 4 +- public/app/features/panel/solo_panel_ctrl.ts | 6 +- public/app/features/panellinks/module.ts | 16 +- .../app/features/playlist/playlist_routes.ts | 2 +- public/app/features/plugins/ds_edit_ctrl.ts | 8 +- .../app/features/plugins/plugin_component.ts | 22 +-- public/app/features/templating/all.ts | 2 +- .../features/templating/custom_variable.ts | 2 +- public/app/features/templating/editor_ctrl.ts | 32 ++-- .../features/templating/interval_variable.ts | 2 +- .../app/features/templating/query_variable.ts | 4 +- .../app/features/templating/template_srv.ts | 6 +- .../app/features/templating/variable_srv.ts | 6 +- public/app/routes/ReactContainer.tsx | 2 +- public/app/routes/dashboard_loaders.ts | 4 +- 50 files changed, 367 insertions(+), 367 deletions(-) diff --git a/public/app/app.ts b/public/app/app.ts index 77f56264504..8e30747072e 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -21,7 +21,7 @@ import _ from 'lodash'; import moment from 'moment'; // add move to lodash for backward compatabiltiy -_.move = function(array, fromIndex, toIndex) { +_.move = (array, fromIndex, toIndex) => { array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); return array; }; @@ -76,9 +76,9 @@ export class GrafanaApp { $provide.decorator('$http', [ '$delegate', '$templateCache', - function($delegate, $templateCache) { + ($delegate, $templateCache) => { const get = $delegate.get; - $delegate.get = function(url, config) { + $delegate.get = (url, config) => { if (url.match(/\.html$/)) { // some template's already exist in the cache if (!$templateCache.get(url)) { @@ -135,7 +135,7 @@ export class GrafanaApp { this.preBootModules = null; }); }) - .catch(function(err) { + .catch(err => { console.log('Application boot failed:', err); }); } diff --git a/public/app/containers/Explore/utils/debounce.ts b/public/app/containers/Explore/utils/debounce.ts index 5fda5a05f5f..a7c9450a6c1 100644 --- a/public/app/containers/Explore/utils/debounce.ts +++ b/public/app/containers/Explore/utils/debounce.ts @@ -4,7 +4,7 @@ export default function debounce(func, wait) { return function(this: any) { const context = this; const args = arguments; - const later = function() { + const later = () => { timeout = null; func.apply(context, args); }; diff --git a/public/app/core/components/gf_page.ts b/public/app/core/components/gf_page.ts index ad0770940ec..057a307f205 100644 --- a/public/app/core/components/gf_page.ts +++ b/public/app/core/components/gf_page.ts @@ -31,7 +31,7 @@ export function gfPageDirective() { header: '?gfPageHeader', body: 'gfPageBody', }, - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { console.log(scope); }, }; diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts index b6603f06175..2d6e27f8b22 100644 --- a/public/app/core/components/scroll/page_scroll.ts +++ b/public/app/core/components/scroll/page_scroll.ts @@ -4,7 +4,7 @@ import appEvents from 'app/core/app_events'; export function pageScrollbar() { return { restrict: 'A', - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { let lastPos = 0; appEvents.on( diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index 2d60825e739..bd355817f92 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -14,7 +14,7 @@ const scrollerClass = 'baron__scroller'; export function geminiScrollbar() { return { restrict: 'A', - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { let scrollRoot = elem.parent(); const scroller = elem; diff --git a/public/app/core/controllers/invited_ctrl.ts b/public/app/core/controllers/invited_ctrl.ts index e9127af26b7..63f9d975c1f 100644 --- a/public/app/core/controllers/invited_ctrl.ts +++ b/public/app/core/controllers/invited_ctrl.ts @@ -16,8 +16,8 @@ export class InvitedCtrl { }, }; - $scope.init = function() { - backendSrv.get('/api/user/invite/' + $routeParams.code).then(function(invite) { + $scope.init = () => { + backendSrv.get('/api/user/invite/' + $routeParams.code).then(invite => { $scope.formModel.name = invite.name; $scope.formModel.email = invite.email; $scope.formModel.username = invite.email; @@ -28,12 +28,12 @@ export class InvitedCtrl { }); }; - $scope.submit = function() { + $scope.submit = () => { if (!$scope.inviteForm.$valid) { return; } - backendSrv.post('/api/user/invite/complete', $scope.formModel).then(function() { + backendSrv.post('/api/user/invite/complete', $scope.formModel).then(() => { window.location.href = config.appSubUrl + '/'; }); }; diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 737596fcdf6..de4e3415dfb 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -29,7 +29,7 @@ export class LoginCtrl { $scope.loginMode = true; $scope.submitBtnText = 'Log in'; - $scope.init = function() { + $scope.init = () => { $scope.$watch('loginMode', $scope.loginModeChanged); if (config.loginError) { @@ -37,7 +37,7 @@ export class LoginCtrl { } }; - $scope.submit = function() { + $scope.submit = () => { if ($scope.loginMode) { $scope.login(); } else { @@ -45,7 +45,7 @@ export class LoginCtrl { } }; - $scope.changeView = function() { + $scope.changeView = () => { const loginView = document.querySelector('#login-view'); const changePasswordView = document.querySelector('#change-password-view'); @@ -65,7 +65,7 @@ export class LoginCtrl { }, 400); }; - $scope.changePassword = function() { + $scope.changePassword = () => { $scope.command.oldPassword = 'admin'; if ($scope.command.newPassword !== $scope.command.confirmNew) { @@ -73,25 +73,25 @@ export class LoginCtrl { return; } - backendSrv.put('/api/user/password', $scope.command).then(function() { + backendSrv.put('/api/user/password', $scope.command).then(() => { $scope.toGrafana(); }); }; - $scope.skip = function() { + $scope.skip = () => { $scope.toGrafana(); }; - $scope.loginModeChanged = function(newValue) { + $scope.loginModeChanged = newValue => { $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; }; - $scope.signUp = function() { + $scope.signUp = () => { if (!$scope.loginForm.$valid) { return; } - backendSrv.post('/api/user/signup', $scope.formModel).then(function(result) { + backendSrv.post('/api/user/signup', $scope.formModel).then(result => { if (result.status === 'SignUpCreated') { $location.path('/signup').search({ email: $scope.formModel.email }); } else { @@ -100,7 +100,7 @@ export class LoginCtrl { }); }; - $scope.login = function() { + $scope.login = () => { delete $scope.loginError; if (!$scope.loginForm.$valid) { @@ -110,7 +110,7 @@ export class LoginCtrl { backendSrv .post('/login', $scope.formModel) - .then(function(result) { + .then(result => { $scope.result = result; if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) { @@ -125,7 +125,7 @@ export class LoginCtrl { }); }; - $scope.toGrafana = function() { + $scope.toGrafana = () => { const params = $location.search(); if (params.redirect && params.redirect[0] === '/') { diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index 244f0307150..933655399e8 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -22,16 +22,16 @@ export class ResetPasswordCtrl { }, }; - $scope.sendResetEmail = function() { + $scope.sendResetEmail = () => { if (!$scope.sendResetForm.$valid) { return; } - backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() { + backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(() => { $scope.mode = 'email-sent'; }); }; - $scope.submitReset = function() { + $scope.submitReset = () => { if (!$scope.resetForm.$valid) { return; } @@ -41,7 +41,7 @@ export class ResetPasswordCtrl { return; } - backendSrv.post('/api/user/password/reset', $scope.formModel).then(function() { + backendSrv.post('/api/user/password/reset', $scope.formModel).then(() => { $location.path('login'); }); }; diff --git a/public/app/core/filters/filters.ts b/public/app/core/filters/filters.ts index 745873369c0..c4dbf6b7535 100644 --- a/public/app/core/filters/filters.ts +++ b/public/app/core/filters/filters.ts @@ -3,22 +3,22 @@ import angular from 'angular'; import moment from 'moment'; import coreModule from '../core_module'; -coreModule.filter('stringSort', function() { - return function(input) { +coreModule.filter('stringSort', () => { + return input => { return input.sort(); }; }); -coreModule.filter('slice', function() { - return function(arr, start, end) { +coreModule.filter('slice', () => { + return (arr, start, end) => { if (!_.isUndefined(arr)) { return arr.slice(start, end); } }; }); -coreModule.filter('stringify', function() { - return function(arr) { +coreModule.filter('stringify', () => { + return arr => { if (_.isObject(arr) && !_.isArray(arr)) { return angular.toJson(arr); } else { @@ -27,8 +27,8 @@ coreModule.filter('stringify', function() { }; }); -coreModule.filter('moment', function() { - return function(date, mode) { +coreModule.filter('moment', () => { + return (date, mode) => { switch (mode) { case 'ago': return moment(date).fromNow(); @@ -37,8 +37,8 @@ coreModule.filter('moment', function() { }; }); -coreModule.filter('noXml', function() { - const noXml = function(text) { +coreModule.filter('noXml', () => { + const noXml = text => { return _.isString(text) ? text .replace(/&/g, '&') @@ -48,14 +48,14 @@ coreModule.filter('noXml', function() { .replace(/"/g, '"') : text; }; - return function(text) { + return text => { return _.isArray(text) ? _.map(text, noXml) : noXml(text); }; }); /** @ngInject */ function interpolateTemplateVars(templateSrv) { - const filterFunc: any = function(text, scope) { + const filterFunc: any = (text, scope) => { let scopedVars; if (scope.ctrl) { scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars; diff --git a/public/app/core/lodash_extended.ts b/public/app/core/lodash_extended.ts index 1a8820fb0db..1fc7d7c341a 100644 --- a/public/app/core/lodash_extended.ts +++ b/public/app/core/lodash_extended.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; Mixins :) */ _.mixin({ - move: function(array, fromIndex, toIndex) { + move: (array, fromIndex, toIndex) => { array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); return array; }, diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 19ad81667d7..2d447651b75 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -70,7 +70,7 @@ export class AlertSrv { const newAlertJson = angular.toJson(newAlert); // remove same alert if it already exists - _.remove(this.list, function(value) { + _.remove(this.list, value => { return angular.toJson(value) === newAlertJson; }); diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts index 631cc274021..3a00589c251 100644 --- a/public/app/core/services/popover_srv.ts +++ b/public/app/core/services/popover_srv.ts @@ -6,13 +6,13 @@ import Drop from 'tether-drop'; function popoverSrv(this: any, $compile, $rootScope, $timeout) { let openDrop = null; - this.close = function() { + this.close = () => { if (openDrop) { openDrop.close(); } }; - this.show = function(options) { + this.show = options => { if (openDrop) { openDrop.close(); openDrop = null; @@ -68,7 +68,7 @@ function popoverSrv(this: any, $compile, $rootScope, $timeout) { }, 100); // return close function - return function() { + return () => { if (drop) { drop.close(); } diff --git a/public/app/core/services/segment_srv.ts b/public/app/core/services/segment_srv.ts index f03f5eca546..e7653ec5cf7 100644 --- a/public/app/core/services/segment_srv.ts +++ b/public/app/core/services/segment_srv.ts @@ -42,48 +42,48 @@ export function uiSegmentSrv(this: any, $sce, templateSrv) { } }; - this.newSelectMeasurement = function() { + this.newSelectMeasurement = () => { return new MetricSegment({ value: 'select measurement', fake: true }); }; - this.newFake = function(text, type, cssClass) { + this.newFake = (text, type, cssClass) => { return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass }); }; - this.newSegment = function(options) { + this.newSegment = options => { return new MetricSegment(options); }; - this.newKey = function(key) { + this.newKey = key => { return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' }); }; - this.newKeyValue = function(value) { + this.newKeyValue = value => { return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' }); }; - this.newCondition = function(condition) { + this.newCondition = condition => { return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' }); }; - this.newOperator = function(op) { + this.newOperator = op => { return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' }); }; - this.newOperators = function(ops) { - return _.map(ops, function(op) { + this.newOperators = ops => { + return _.map(ops, op => { return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' }); }); }; - this.transformToSegments = function(addTemplateVars, variableTypeFilter) { - return function(results) { - const segments = _.map(results, function(segment) { + this.transformToSegments = (addTemplateVars, variableTypeFilter) => { + return results => { + const segments = _.map(results, segment => { return self.newSegment({ value: segment.text, expandable: segment.expandable }); }); if (addTemplateVars) { - _.each(templateSrv.variables, function(variable) { + _.each(templateSrv.variables, variable => { if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) { segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true })); } @@ -94,11 +94,11 @@ export function uiSegmentSrv(this: any, $sce, templateSrv) { }; }; - this.newSelectMetric = function() { + this.newSelectMetric = () => { return new MetricSegment({ value: 'select metric', fake: true }); }; - this.newPlusButton = function() { + this.newPlusButton = () => { return new MetricSegment({ fake: true, html: '', diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index da598fbb127..2b7538f1be2 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -44,7 +44,7 @@ export class UtilSrv { backdrop: options.backdrop, }); - Promise.resolve(modal).then(function(modalEl) { + Promise.resolve(modal).then(modalEl => { modalEl.modal('show'); }); } @@ -52,12 +52,12 @@ export class UtilSrv { showConfirmModal(payload) { const scope = this.$rootScope.$new(); - scope.onConfirm = function() { + scope.onConfirm = () => { payload.onConfirm(); scope.dismiss(); }; - scope.updateConfirmText = function(value) { + scope.updateConfirmText = value => { scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase(); }; diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index e9cd5973d36..2e35b87deb4 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -1,7 +1,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('app/core/store'); -describe('backend_srv', function() { +describe('backend_srv', () => { const _httpBackend = options => { if (options.url === 'gateway-error') { return Promise.reject({ status: 502 }); diff --git a/public/app/core/specs/datemath.test.ts b/public/app/core/specs/datemath.test.ts index 8197f01c872..bbc54970411 100644 --- a/public/app/core/specs/datemath.test.ts +++ b/public/app/core/specs/datemath.test.ts @@ -91,11 +91,11 @@ describe('DateMath', () => { }); _.each(spans, span => { - it('should round now to the beginning of the ' + span, function() { + it('should round now to the beginning of the ' + span, () => { expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format)); }); - it('should round now to the end of the ' + span, function() { + it('should round now to the end of the ' + span, () => { expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format)); }); }); @@ -114,18 +114,18 @@ describe('DateMath', () => { }); }); - describe('relative time to date parsing', function() { - it('should handle negative time', function() { + describe('relative time to date parsing', () => { + it('should handle negative time', () => { const date = dateMath.parseDateMath('-2d', moment([2014, 1, 5])); expect(date.valueOf()).toEqual(moment([2014, 1, 3]).valueOf()); }); - it('should handle multiple math expressions', function() { + it('should handle multiple math expressions', () => { const date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5])); expect(date.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf()); }); - it('should return false when invalid expression', function() { + it('should return false when invalid expression', () => { const date = dateMath.parseDateMath('2', moment([2014, 1, 5])); expect(date).toEqual(undefined); }); diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts index ced94fcdbc0..52ec4ccea19 100644 --- a/public/app/core/specs/file_export.test.ts +++ b/public/app/core/specs/file_export.test.ts @@ -101,7 +101,7 @@ describe('file_export', () => { expect(returnedText).toBe(expectedText); }); - it('should decode HTML encoded characters', function() { + it('should decode HTML encoded characters', () => { const inputTable = { columns: [{ text: 'string_value' }], rows: [ diff --git a/public/app/core/specs/kbn.test.ts b/public/app/core/specs/kbn.test.ts index b4072adf7cf..dfa665e3205 100644 --- a/public/app/core/specs/kbn.test.ts +++ b/public/app/core/specs/kbn.test.ts @@ -2,27 +2,27 @@ import kbn from '../utils/kbn'; import * as dateMath from '../utils/datemath'; import moment from 'moment'; -describe('unit format menu', function() { +describe('unit format menu', () => { const menu = kbn.getUnitFormats(); - menu.map(function(submenu) { - describe('submenu ' + submenu.text, function() { - it('should have a title', function() { + menu.map(submenu => { + describe('submenu ' + submenu.text, () => { + it('should have a title', () => { expect(typeof submenu.text).toBe('string'); }); - it('should have a submenu', function() { + it('should have a submenu', () => { expect(Array.isArray(submenu.submenu)).toBe(true); }); - submenu.submenu.map(function(entry) { - describe('entry ' + entry.text, function() { - it('should have a title', function() { + submenu.submenu.map(entry => { + describe('entry ' + entry.text, () => { + it('should have a title', () => { expect(typeof entry.text).toBe('string'); }); - it('should have a format', function() { + it('should have a format', () => { expect(typeof entry.value).toBe('string'); }); - it('should have a valid format', function() { + it('should have a valid format', () => { expect(typeof kbn.valueFormats[entry.value]).toBe('function'); }); }); @@ -32,8 +32,8 @@ describe('unit format menu', function() { }); function describeValueFormat(desc, value, tickSize, tickDecimals, result) { - describe('value format: ' + desc, function() { - it('should translate ' + value + ' as ' + result, function() { + describe('value format: ' + desc, () => { + it('should translate ' + value + ' as ' + result, () => { const scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); const str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); expect(str).toBe(result); @@ -100,85 +100,85 @@ describeValueFormat('d', 3, 1, 0, '3 day'); describeValueFormat('d', 245, 100, 0, '35 week'); describeValueFormat('d', 2456, 10, 0, '6.73 year'); -describe('date time formats', function() { +describe('date time formats', () => { const epoch = 1505634997920; const utcTime = moment.utc(epoch); const browserTime = moment(epoch); - it('should format as iso date', function() { + it('should format as iso date', () => { const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); const actual = kbn.valueFormats.dateTimeAsIso(epoch); expect(actual).toBe(expected); }); - it('should format as iso date (in UTC)', function() { + it('should format as iso date (in UTC)', () => { const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); const actual = kbn.valueFormats.dateTimeAsIso(epoch, true); expect(actual).toBe(expected); }); - it('should format as iso date and skip date when today', function() { + it('should format as iso date and skip date when today', () => { const now = moment(); const expected = now.format('HH:mm:ss'); const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false); expect(actual).toBe(expected); }); - it('should format as iso date (in UTC) and skip date when today', function() { + it('should format as iso date (in UTC) and skip date when today', () => { const now = moment.utc(); const expected = now.format('HH:mm:ss'); const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true); expect(actual).toBe(expected); }); - it('should format as US date', function() { + it('should format as US date', () => { const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); const actual = kbn.valueFormats.dateTimeAsUS(epoch, false); expect(actual).toBe(expected); }); - it('should format as US date (in UTC)', function() { + it('should format as US date (in UTC)', () => { const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); const actual = kbn.valueFormats.dateTimeAsUS(epoch, true); expect(actual).toBe(expected); }); - it('should format as US date and skip date when today', function() { + it('should format as US date and skip date when today', () => { const now = moment(); const expected = now.format('h:mm:ss a'); const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false); expect(actual).toBe(expected); }); - it('should format as US date (in UTC) and skip date when today', function() { + it('should format as US date (in UTC) and skip date when today', () => { const now = moment.utc(); const expected = now.format('h:mm:ss a'); const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true); expect(actual).toBe(expected); }); - it('should format as from now with days', function() { + it('should format as from now with days', () => { const daysAgo = moment().add(-7, 'd'); const expected = '7 days ago'; const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); expect(actual).toBe(expected); }); - it('should format as from now with days (in UTC)', function() { + it('should format as from now with days (in UTC)', () => { const daysAgo = moment.utc().add(-7, 'd'); const expected = '7 days ago'; const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); expect(actual).toBe(expected); }); - it('should format as from now with minutes', function() { + it('should format as from now with minutes', () => { const daysAgo = moment().add(-2, 'm'); const expected = '2 minutes ago'; const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); expect(actual).toBe(expected); }); - it('should format as from now with minutes (in UTC)', function() { + it('should format as from now with minutes (in UTC)', () => { const daysAgo = moment.utc().add(-2, 'm'); const expected = '2 minutes ago'; const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); @@ -186,92 +186,92 @@ describe('date time formats', function() { }); }); -describe('kbn.toFixed and negative decimals', function() { - it('should treat as zero decimals', function() { +describe('kbn.toFixed and negative decimals', () => { + it('should treat as zero decimals', () => { const str = kbn.toFixed(186.123, -2); expect(str).toBe('186'); }); }); -describe('kbn ms format when scaled decimals is null do not use it', function() { - it('should use specified decimals', function() { +describe('kbn ms format when scaled decimals is null do not use it', () => { + it('should use specified decimals', () => { const str = kbn.valueFormats['ms'](10000086.123, 1, null); expect(str).toBe('2.8 hour'); }); }); -describe('kbn kbytes format when scaled decimals is null do not use it', function() { - it('should use specified decimals', function() { +describe('kbn kbytes format when scaled decimals is null do not use it', () => { + it('should use specified decimals', () => { const str = kbn.valueFormats['kbytes'](10000000, 3, null); expect(str).toBe('9.537 GiB'); }); }); -describe('kbn deckbytes format when scaled decimals is null do not use it', function() { - it('should use specified decimals', function() { +describe('kbn deckbytes format when scaled decimals is null do not use it', () => { + it('should use specified decimals', () => { const str = kbn.valueFormats['deckbytes'](10000000, 3, null); expect(str).toBe('10.000 GB'); }); }); -describe('kbn roundValue', function() { - it('should should handle null value', function() { +describe('kbn roundValue', () => { + it('should should handle null value', () => { const str = kbn.roundValue(null, 2); expect(str).toBe(null); }); - it('should round value', function() { + it('should round value', () => { const str = kbn.roundValue(200.877, 2); expect(str).toBe(200.88); }); }); -describe('calculateInterval', function() { - it('1h 100 resultion', function() { +describe('calculateInterval', () => { + it('1h 100 resultion', () => { const range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 100, null); expect(res.interval).toBe('30s'); }); - it('10m 1600 resolution', function() { + it('10m 1600 resolution', () => { const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 1600, null); expect(res.interval).toBe('500ms'); expect(res.intervalMs).toBe(500); }); - it('fixed user min interval', function() { + it('fixed user min interval', () => { const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 1600, '10s'); expect(res.interval).toBe('10s'); expect(res.intervalMs).toBe(10000); }); - it('short time range and user low limit', function() { + it('short time range and user low limit', () => { const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 1600, '>10s'); expect(res.interval).toBe('10s'); }); - it('large time range and user low limit', function() { + it('large time range and user low limit', () => { const range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 1000, '>10s'); expect(res.interval).toBe('20m'); }); - it('10s 900 resolution and user low limit in ms', function() { + it('10s 900 resolution and user low limit in ms', () => { const range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 900, '>15ms'); expect(res.interval).toBe('15ms'); }); - it('1d 1 resolution', function() { + it('1d 1 resolution', () => { const range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; const res = kbn.calculateInterval(range, 1, null); expect(res.interval).toBe('1d'); expect(res.intervalMs).toBe(86400000); }); - it('86399s 1 resolution', function() { + it('86399s 1 resolution', () => { const range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now'), @@ -282,140 +282,140 @@ describe('calculateInterval', function() { }); }); -describe('hex', function() { - it('positive integer', function() { +describe('hex', () => { + it('positive integer', () => { const str = kbn.valueFormats.hex(100, 0); expect(str).toBe('64'); }); - it('negative integer', function() { + it('negative integer', () => { const str = kbn.valueFormats.hex(-100, 0); expect(str).toBe('-64'); }); - it('null', function() { + it('null', () => { const str = kbn.valueFormats.hex(null, 0); expect(str).toBe(''); }); - it('positive float', function() { + it('positive float', () => { const str = kbn.valueFormats.hex(50.52, 1); expect(str).toBe('32.8'); }); - it('negative float', function() { + it('negative float', () => { const str = kbn.valueFormats.hex(-50.333, 2); expect(str).toBe('-32.547AE147AE14'); }); }); -describe('hex 0x', function() { - it('positive integeter', function() { +describe('hex 0x', () => { + it('positive integeter', () => { const str = kbn.valueFormats.hex0x(7999, 0); expect(str).toBe('0x1F3F'); }); - it('negative integer', function() { + it('negative integer', () => { const str = kbn.valueFormats.hex0x(-584, 0); expect(str).toBe('-0x248'); }); - it('null', function() { + it('null', () => { const str = kbn.valueFormats.hex0x(null, 0); expect(str).toBe(''); }); - it('positive float', function() { + it('positive float', () => { const str = kbn.valueFormats.hex0x(74.443, 3); expect(str).toBe('0x4A.716872B020C4'); }); - it('negative float', function() { + it('negative float', () => { const str = kbn.valueFormats.hex0x(-65.458, 1); expect(str).toBe('-0x41.8'); }); }); -describe('duration', function() { - it('null', function() { +describe('duration', () => { + it('null', () => { const str = kbn.toDuration(null, 0, 'millisecond'); expect(str).toBe(''); }); - it('0 milliseconds', function() { + it('0 milliseconds', () => { const str = kbn.toDuration(0, 0, 'millisecond'); expect(str).toBe('0 milliseconds'); }); - it('1 millisecond', function() { + it('1 millisecond', () => { const str = kbn.toDuration(1, 0, 'millisecond'); expect(str).toBe('1 millisecond'); }); - it('-1 millisecond', function() { + it('-1 millisecond', () => { const str = kbn.toDuration(-1, 0, 'millisecond'); expect(str).toBe('1 millisecond ago'); }); - it('seconds', function() { + it('seconds', () => { const str = kbn.toDuration(1, 0, 'second'); expect(str).toBe('1 second'); }); - it('minutes', function() { + it('minutes', () => { const str = kbn.toDuration(1, 0, 'minute'); expect(str).toBe('1 minute'); }); - it('hours', function() { + it('hours', () => { const str = kbn.toDuration(1, 0, 'hour'); expect(str).toBe('1 hour'); }); - it('days', function() { + it('days', () => { const str = kbn.toDuration(1, 0, 'day'); expect(str).toBe('1 day'); }); - it('weeks', function() { + it('weeks', () => { const str = kbn.toDuration(1, 0, 'week'); expect(str).toBe('1 week'); }); - it('months', function() { + it('months', () => { const str = kbn.toDuration(1, 0, 'month'); expect(str).toBe('1 month'); }); - it('years', function() { + it('years', () => { const str = kbn.toDuration(1, 0, 'year'); expect(str).toBe('1 year'); }); - it('decimal days', function() { + it('decimal days', () => { const str = kbn.toDuration(1.5, 2, 'day'); expect(str).toBe('1 day, 12 hours, 0 minutes'); }); - it('decimal months', function() { + it('decimal months', () => { const str = kbn.toDuration(1.5, 3, 'month'); expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); }); - it('no decimals', function() { + it('no decimals', () => { const str = kbn.toDuration(38898367008, 0, 'millisecond'); expect(str).toBe('1 year'); }); - it('1 decimal', function() { + it('1 decimal', () => { const str = kbn.toDuration(38898367008, 1, 'millisecond'); expect(str).toBe('1 year, 2 months'); }); - it('too many decimals', function() { + it('too many decimals', () => { const str = kbn.toDuration(38898367008, 20, 'millisecond'); expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); }); - it('floating point error', function() { + it('floating point error', () => { const str = kbn.toDuration(36993906007, 8, 'millisecond'); expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); }); }); -describe('volume', function() { - it('1000m3', function() { +describe('volume', () => { + it('1000m3', () => { const str = kbn.valueFormats['m3'](1000, 1, null); expect(str).toBe('1000.0 m³'); }); }); -describe('hh:mm:ss', function() { - it('00:04:06', function() { +describe('hh:mm:ss', () => { + it('00:04:06', () => { const str = kbn.valueFormats['dthms'](246, 1); expect(str).toBe('00:04:06'); }); - it('24:00:00', function() { + it('24:00:00', () => { const str = kbn.valueFormats['dthms'](86400, 1); expect(str).toBe('24:00:00'); }); - it('6824413:53:20', function() { + it('6824413:53:20', () => { const str = kbn.valueFormats['dthms'](24567890000, 1); expect(str).toBe('6824413:53:20'); }); diff --git a/public/app/core/specs/time_series.test.ts b/public/app/core/specs/time_series.test.ts index 3c0fc42be65..6cf7399c059 100644 --- a/public/app/core/specs/time_series.test.ts +++ b/public/app/core/specs/time_series.test.ts @@ -1,33 +1,33 @@ import TimeSeries from 'app/core/time_series2'; import { updateLegendValues } from 'app/core/time_series2'; -describe('TimeSeries', function() { +describe('TimeSeries', () => { let points, series; const yAxisFormats = ['short', 'ms']; let testData; - beforeEach(function() { + beforeEach(() => { testData = { alias: 'test', datapoints: [[1, 2], [null, 3], [10, 4], [8, 5]], }; }); - describe('when getting flot pairs', function() { - it('with connected style, should ignore nulls', function() { + describe('when getting flot pairs', () => { + it('with connected style, should ignore nulls', () => { series = new TimeSeries(testData); points = series.getFlotPairs('connected', yAxisFormats); expect(points.length).toBe(3); }); - it('with null as zero style, should replace nulls with zero', function() { + it('with null as zero style, should replace nulls with zero', () => { series = new TimeSeries(testData); points = series.getFlotPairs('null as zero', yAxisFormats); expect(points.length).toBe(4); expect(points[1][1]).toBe(0); }); - it('if last is null current should pick next to last', function() { + it('if last is null current should pick next to last', () => { series = new TimeSeries({ datapoints: [[10, 1], [null, 2]], }); @@ -35,7 +35,7 @@ describe('TimeSeries', function() { expect(series.stats.current).toBe(10); }); - it('max value should work for negative values', function() { + it('max value should work for negative values', () => { series = new TimeSeries({ datapoints: [[-10, 1], [-4, 2]], }); @@ -43,13 +43,13 @@ describe('TimeSeries', function() { expect(series.stats.max).toBe(-4); }); - it('average value should ignore nulls', function() { + it('average value should ignore nulls', () => { series = new TimeSeries(testData); series.getFlotPairs('null', yAxisFormats); expect(series.stats.avg).toBe(6.333333333333333); }); - it('the delta value should account for nulls', function() { + it('the delta value should account for nulls', () => { series = new TimeSeries({ datapoints: [[1, 2], [3, 3], [null, 4], [10, 5], [15, 6]], }); @@ -57,7 +57,7 @@ describe('TimeSeries', function() { expect(series.stats.delta).toBe(14); }); - it('the delta value should account for nulls on first', function() { + it('the delta value should account for nulls on first', () => { series = new TimeSeries({ datapoints: [[null, 2], [1, 3], [10, 4], [15, 5]], }); @@ -65,7 +65,7 @@ describe('TimeSeries', function() { expect(series.stats.delta).toBe(14); }); - it('the delta value should account for nulls on last', function() { + it('the delta value should account for nulls on last', () => { series = new TimeSeries({ datapoints: [[1, 2], [5, 3], [10, 4], [null, 5]], }); @@ -73,7 +73,7 @@ describe('TimeSeries', function() { expect(series.stats.delta).toBe(9); }); - it('the delta value should account for resets', function() { + it('the delta value should account for resets', () => { series = new TimeSeries({ datapoints: [[1, 2], [5, 3], [10, 4], [0, 5], [10, 6]], }); @@ -81,7 +81,7 @@ describe('TimeSeries', function() { expect(series.stats.delta).toBe(19); }); - it('the delta value should account for resets on last', function() { + it('the delta value should account for resets on last', () => { series = new TimeSeries({ datapoints: [[1, 2], [2, 3], [10, 4], [8, 5]], }); @@ -89,13 +89,13 @@ describe('TimeSeries', function() { expect(series.stats.delta).toBe(17); }); - it('the range value should be max - min', function() { + it('the range value should be max - min', () => { series = new TimeSeries(testData); series.getFlotPairs('null', yAxisFormats); expect(series.stats.range).toBe(9); }); - it('first value should ingone nulls', function() { + it('first value should ingone nulls', () => { series = new TimeSeries(testData); series.getFlotPairs('null', yAxisFormats); expect(series.stats.first).toBe(1); @@ -106,13 +106,13 @@ describe('TimeSeries', function() { expect(series.stats.first).toBe(1); }); - it('with null as zero style, average value should treat nulls as 0', function() { + it('with null as zero style, average value should treat nulls as 0', () => { series = new TimeSeries(testData); series.getFlotPairs('null as zero', yAxisFormats); expect(series.stats.avg).toBe(4.75); }); - it('average value should be null if all values is null', function() { + it('average value should be null if all values is null', () => { series = new TimeSeries({ datapoints: [[null, 2], [null, 3], [null, 4], [null, 5]], }); @@ -120,7 +120,7 @@ describe('TimeSeries', function() { expect(series.stats.avg).toBe(null); }); - it('calculates timeStep', function() { + it('calculates timeStep', () => { series = new TimeSeries({ datapoints: [[null, 1], [null, 2], [null, 3]], }); @@ -135,190 +135,190 @@ describe('TimeSeries', function() { }); }); - describe('When checking if ms resolution is needed', function() { - describe('msResolution with second resolution timestamps', function() { - beforeEach(function() { + describe('When checking if ms resolution is needed', () => { + describe('msResolution with second resolution timestamps', () => { + beforeEach(() => { series = new TimeSeries({ datapoints: [[45, 1234567890], [60, 1234567899]], }); }); - it('should set hasMsResolution to false', function() { + it('should set hasMsResolution to false', () => { expect(series.hasMsResolution).toBe(false); }); }); - describe('msResolution with millisecond resolution timestamps', function() { - beforeEach(function() { + describe('msResolution with millisecond resolution timestamps', () => { + beforeEach(() => { series = new TimeSeries({ datapoints: [[55, 1236547890001], [90, 1234456709000]], }); }); - it('should show millisecond resolution tooltip', function() { + it('should show millisecond resolution tooltip', () => { expect(series.hasMsResolution).toBe(true); }); }); - describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { - beforeEach(function() { + describe('msResolution with millisecond resolution timestamps but with trailing zeroes', () => { + beforeEach(() => { series = new TimeSeries({ datapoints: [[45, 1234567890000], [60, 1234567899000]], }); }); - it('should not show millisecond resolution tooltip', function() { + it('should not show millisecond resolution tooltip', () => { expect(series.hasMsResolution).toBe(false); }); }); }); - describe('can detect if series contains ms precision', function() { + describe('can detect if series contains ms precision', () => { let fakedata; - beforeEach(function() { + beforeEach(() => { fakedata = testData; }); - it('missing datapoint with ms precision', function() { + it('missing datapoint with ms precision', () => { fakedata.datapoints[0] = [1337, 1234567890000]; series = new TimeSeries(fakedata); expect(series.isMsResolutionNeeded()).toBe(false); }); - it('contains datapoint with ms precision', function() { + it('contains datapoint with ms precision', () => { fakedata.datapoints[0] = [1337, 1236547890001]; series = new TimeSeries(fakedata); expect(series.isMsResolutionNeeded()).toBe(true); }); }); - describe('series overrides', function() { + describe('series overrides', () => { let series; - beforeEach(function() { + beforeEach(() => { series = new TimeSeries(testData); }); - describe('fill & points', function() { - beforeEach(function() { + describe('fill & points', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]); }); - it('should set fill zero, and enable points', function() { + it('should set fill zero, and enable points', () => { expect(series.lines.fill).toBe(0.001); expect(series.points.show).toBe(true); }); }); - describe('series option overrides, bars, true & lines false', function() { - beforeEach(function() { + describe('series option overrides, bars, true & lines false', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]); }); - it('should disable lines, and enable bars', function() { + it('should disable lines, and enable bars', () => { expect(series.lines.show).toBe(false); expect(series.bars.show).toBe(true); }); }); - describe('series option overrides, linewidth, stack', function() { - beforeEach(function() { + describe('series option overrides, linewidth, stack', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]); }); - it('should disable stack, and set lineWidth', function() { + it('should disable stack, and set lineWidth', () => { expect(series.stack).toBe(false); expect(series.lines.lineWidth).toBe(5); }); }); - describe('series option overrides, dashes and lineWidth', function() { - beforeEach(function() { + describe('series option overrides, dashes and lineWidth', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', linewidth: 5, dashes: true }]); }); - it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() { + it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', () => { expect(series.dashes.show).toBe(true); expect(series.dashes.lineWidth).toBe(5); expect(series.lines.lineWidth).toBe(0); }); }); - describe('series option overrides, fill below to', function() { - beforeEach(function() { + describe('series option overrides, fill below to', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]); }); - it('should disable line fill and add fillBelowTo', function() { + it('should disable line fill and add fillBelowTo', () => { expect(series.fillBelowTo).toBe('min'); }); }); - describe('series option overrides, pointradius, steppedLine', function() { - beforeEach(function() { + describe('series option overrides, pointradius, steppedLine', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]); }); - it('should set pointradius, and set steppedLine', function() { + it('should set pointradius, and set steppedLine', () => { expect(series.points.radius).toBe(5); expect(series.lines.steps).toBe(true); }); }); - describe('override match on regex', function() { - beforeEach(function() { + describe('override match on regex', () => { + beforeEach(() => { series.alias = 'test_01'; series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]); }); - it('should match second series', function() { + it('should match second series', () => { expect(series.lines.show).toBe(false); }); }); - describe('override series y-axis, and z-index', function() { - beforeEach(function() { + describe('override series y-axis, and z-index', () => { + beforeEach(() => { series.alias = 'test'; series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]); }); - it('should set yaxis', function() { + it('should set yaxis', () => { expect(series.yaxis).toBe(2); }); - it('should set zindex', function() { + it('should set zindex', () => { expect(series.zindex).toBe(2); }); }); - describe('override color', function() { - beforeEach(function() { + describe('override color', () => { + beforeEach(() => { series.applySeriesOverrides([{ alias: 'test', color: '#112233' }]); }); - it('should set color', function() { + it('should set color', () => { expect(series.color).toBe('#112233'); }); - it('should set bars.fillColor', function() { + it('should set bars.fillColor', () => { expect(series.bars.fillColor).toBe('#112233'); }); }); }); - describe('value formatter', function() { + describe('value formatter', () => { let series; - beforeEach(function() { + beforeEach(() => { series = new TimeSeries(testData); }); - it('should format non-numeric values as empty string', function() { + it('should format non-numeric values as empty string', () => { expect(series.formatValue(null)).toBe(''); expect(series.formatValue(undefined)).toBe(''); expect(series.formatValue(NaN)).toBe(''); @@ -327,10 +327,10 @@ describe('TimeSeries', function() { }); }); - describe('legend decimals', function() { + describe('legend decimals', () => { let series, panel; const height = 200; - beforeEach(function() { + beforeEach(() => { testData = { alias: 'test', datapoints: [[1, 2], [0, 3], [10, 4], [8, 5]], @@ -347,14 +347,14 @@ describe('TimeSeries', function() { }; }); - it('should set decimals based on Y axis (expect calculated decimals = 1)', function() { + it('should set decimals based on Y axis (expect calculated decimals = 1)', () => { const data = [series]; // Expect ticks with this data will have decimals = 1 updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(2); }); - it('should set decimals based on Y axis to 0 if calculated decimals = 0)', function() { + it('should set decimals based on Y axis to 0 if calculated decimals = 0)', () => { testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]]; series = new TimeSeries(testData); series.getFlotPairs(); @@ -363,14 +363,14 @@ describe('TimeSeries', function() { expect(data[0].decimals).toBe(0); }); - it('should set decimals to Y axis decimals + 1', function() { + it('should set decimals to Y axis decimals + 1', () => { panel.yaxes[0].decimals = 2; const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); - it('should set decimals to legend decimals value if it was set explicitly', function() { + it('should set decimals to legend decimals value if it was set explicitly', () => { panel.decimals = 3; const data = [series]; updateLegendValues(data, panel, height); diff --git a/public/app/core/utils/css_loader.ts b/public/app/core/utils/css_loader.ts index 19dd84a6087..b4c26293bb8 100644 --- a/public/app/core/utils/css_loader.ts +++ b/public/app/core/utils/css_loader.ts @@ -9,8 +9,8 @@ for (let i = 0; i < links.length; i++) { } const isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/); -const webkitLoadCheck = function(link, callback) { - setTimeout(function() { +const webkitLoadCheck = (link, callback) => { + setTimeout(() => { for (let i = 0; i < document.styleSheets.length; i++) { const sheet = document.styleSheets[i]; if (sheet.href === link.href) { @@ -21,19 +21,19 @@ const webkitLoadCheck = function(link, callback) { }, 10); }; -const noop = function() {}; +const noop = () => {}; -const loadCSS = function(url) { - return new Promise(function(resolve, reject) { +const loadCSS = url => { + return new Promise((resolve, reject) => { const link = document.createElement('link'); - const timeout = setTimeout(function() { + const timeout = setTimeout(() => { reject('Unable to load CSS'); }, waitSeconds * 1000); - const _callback = function(error) { + const _callback = error => { clearTimeout(timeout); link.onload = link.onerror = noop; - setTimeout(function() { + setTimeout(() => { if (error) { reject(error); } else { @@ -47,14 +47,14 @@ const loadCSS = function(url) { link.href = url; if (!isWebkit) { - link.onload = function() { + link.onload = () => { _callback(undefined); }; } else { webkitLoadCheck(link, _callback); } - link.onerror = function(evt: any) { + link.onerror = (evt: any) => { _callback(evt.error || new Error('Error loading CSS file.')); }; diff --git a/public/app/features/dashboard/change_tracker.ts b/public/app/features/dashboard/change_tracker.ts index 5047aefa199..aa71ac2e306 100644 --- a/public/app/features/dashboard/change_tracker.ts +++ b/public/app/features/dashboard/change_tracker.ts @@ -128,7 +128,7 @@ export class ChangeTracker { }); // ignore template variable values - _.each(dash.templating.list, function(value) { + _.each(dash.templating.list, value => { value.current = null; value.options = null; value.filters = null; diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/dashboard_loader_srv.ts index b7705dd9497..572a92f07bc 100644 --- a/public/app/features/dashboard/dashboard_loader_srv.ts +++ b/public/app/features/dashboard/dashboard_loader_srv.ts @@ -59,7 +59,7 @@ export class DashboardLoaderSrv { }); } - promise.then(function(result) { + promise.then(result => { if (result.meta.dashboardNotFound !== true) { impressionSrv.addDashboardImpression(result.dashboard.id); } diff --git a/public/app/features/dashboard/repeat_option/repeat_option.ts b/public/app/features/dashboard/repeat_option/repeat_option.ts index 19c28607640..4e04a7b3ecc 100644 --- a/public/app/features/dashboard/repeat_option/repeat_option.ts +++ b/public/app/features/dashboard/repeat_option/repeat_option.ts @@ -15,7 +15,7 @@ function dashRepeatOptionDirective(variableSrv) { scope: { panel: '=', }, - link: function(scope, element) { + link: (scope, element) => { element.css({ display: 'block', width: '100%' }); scope.variables = variableSrv.variables.map(item => { @@ -36,7 +36,7 @@ function dashRepeatOptionDirective(variableSrv) { scope.panel.repeatDirection = 'h'; } - scope.optionChanged = function() { + scope.optionChanged = () => { if (scope.panel.repeat) { scope.panel.repeatDirection = 'h'; } diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index 27f0e0073a9..c00a6d8d57f 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -11,7 +11,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, }; $scope.editor = { index: $scope.tabIndex || 0 }; - $scope.init = function() { + $scope.init = () => { $scope.modeSharePanel = $scope.panel ? true : false; $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; @@ -34,7 +34,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, $scope.buildUrl(); }; - $scope.buildUrl = function() { + $scope.buildUrl = () => { let baseUrl = $location.absUrl(); const queryStart = baseUrl.indexOf('?'); @@ -90,7 +90,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, // This function will try to return the proper full name of the local timezone // Chrome does not handle the timezone offset (but phantomjs does) - $scope.getLocalTimeZone = function() { + $scope.getLocalTimeZone = () => { const utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); // Older browser does not the internationalization API @@ -111,7 +111,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, return '&tz=' + encodeURIComponent(options.timeZone); }; - $scope.getShareUrl = function() { + $scope.getShareUrl = () => { return $scope.shareUrl; }; } diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/share_snapshot_ctrl.ts index 2cda493838a..ec487801948 100644 --- a/public/app/features/dashboard/share_snapshot_ctrl.ts +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -25,8 +25,8 @@ export class ShareSnapshotCtrl { { text: 'Public on the web', value: 3 }, ]; - $scope.init = function() { - backendSrv.get('/api/snapshot/shared-options').then(function(options) { + $scope.init = () => { + backendSrv.get('/api/snapshot/shared-options').then(options => { $scope.externalUrl = options['externalSnapshotURL']; $scope.sharingButtonText = options['externalSnapshotName']; $scope.externalEnabled = options['externalEnabled']; @@ -35,7 +35,7 @@ export class ShareSnapshotCtrl { $scope.apiUrl = '/api/snapshots'; - $scope.createSnapshot = function(external) { + $scope.createSnapshot = external => { $scope.dashboard.snapshot = { timestamp: new Date(), }; @@ -49,12 +49,12 @@ export class ShareSnapshotCtrl { $rootScope.$broadcast('refresh'); - $timeout(function() { + $timeout(() => { $scope.saveSnapshot(external); }, $scope.snapshot.timeoutSeconds * 1000); }; - $scope.saveSnapshot = function(external) { + $scope.saveSnapshot = external => { const dash = $scope.dashboard.getSaveModelClone(); $scope.scrubDashboard(dash); @@ -67,7 +67,7 @@ export class ShareSnapshotCtrl { const postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; backendSrv.post(postUrl, cmdData).then( - function(results) { + results => { $scope.loading = false; if (external) { @@ -88,17 +88,17 @@ export class ShareSnapshotCtrl { $scope.step = 2; }, - function() { + () => { $scope.loading = false; } ); }; - $scope.getSnapshotUrl = function() { + $scope.getSnapshotUrl = () => { return $scope.snapshotUrl; }; - $scope.scrubDashboard = function(dash) { + $scope.scrubDashboard = dash => { // change title dash.title = $scope.snapshot.name; @@ -106,7 +106,7 @@ export class ShareSnapshotCtrl { dash.time = timeSrv.timeRange(); // remove panel queries & links - _.each(dash.panels, function(panel) { + _.each(dash.panels, panel => { panel.targets = []; panel.links = []; panel.datasource = null; @@ -114,10 +114,10 @@ export class ShareSnapshotCtrl { // remove annotation queries dash.annotations.list = _.chain(dash.annotations.list) - .filter(function(annotation) { + .filter(annotation => { return annotation.enable; }) - .map(function(annotation) { + .map(annotation => { return { name: annotation.name, enable: annotation.enable, @@ -131,7 +131,7 @@ export class ShareSnapshotCtrl { .value(); // remove template queries - _.each(dash.templating.list, function(variable) { + _.each(dash.templating.list, variable => { variable.query = ''; variable.options = variable.current; variable.refresh = false; @@ -149,21 +149,21 @@ export class ShareSnapshotCtrl { // cleanup snapshotData delete $scope.dashboard.snapshot; - $scope.dashboard.forEachPanel(function(panel) { + $scope.dashboard.forEachPanel(panel => { delete panel.snapshotData; }); - _.each($scope.dashboard.annotations.list, function(annotation) { + _.each($scope.dashboard.annotations.list, annotation => { delete annotation.snapshotData; }); }; - $scope.deleteSnapshot = function() { - backendSrv.get($scope.deleteUrl).then(function() { + $scope.deleteSnapshot = () => { + backendSrv.get($scope.deleteUrl).then(() => { $scope.step = 3; }); }; - $scope.saveExternalSnapshotRef = function(cmdData, results) { + $scope.saveExternalSnapshotRef = (cmdData, results) => { // save external in local instance as well cmdData.external = true; cmdData.key = results.key; diff --git a/public/app/features/dashboard/timepicker/input_date.ts b/public/app/features/dashboard/timepicker/input_date.ts index 1e016ab0896..7de39dfacb2 100644 --- a/public/app/features/dashboard/timepicker/input_date.ts +++ b/public/app/features/dashboard/timepicker/input_date.ts @@ -5,10 +5,10 @@ export function inputDateDirective() { return { restrict: 'A', require: 'ngModel', - link: function($scope, $elem, attrs, ngModel) { + link: ($scope, $elem, attrs, ngModel) => { const format = 'YYYY-MM-DD HH:mm:ss'; - const fromUser = function(text) { + const fromUser = text => { if (text.indexOf('now') !== -1) { if (!dateMath.isValid(text)) { ngModel.$setValidity('error', false); @@ -34,7 +34,7 @@ export function inputDateDirective() { return parsed; }; - const toUser = function(currentValue) { + const toUser = currentValue => { if (moment.isMoment(currentValue)) { return currentValue.format(format); } else { diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 7f7a177ac4b..10d35d1f300 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -16,11 +16,11 @@ function uploadDashboardDirective(timer, alertSrv, $location) { scope: { onUpload: '&', }, - link: function(scope) { + link: scope => { function file_selected(evt) { const files = evt.target.files; // FileList object - const readerOnload = function() { - return function(e) { + const readerOnload = () => { + return e => { let dash; try { dash = JSON.parse(e.target.result); @@ -30,7 +30,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) { return; } - scope.$apply(function() { + scope.$apply(() => { scope.onUpload({ dash: dash }); }); }; diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 773ec6ec711..521de4ecbad 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -22,18 +22,18 @@ export class DashboardViewState { self.$scope = $scope; self.dashboard = $scope.dashboard; - $scope.onAppEvent('$routeUpdate', function() { + $scope.onAppEvent('$routeUpdate', () => { const urlState = self.getQueryStringState(); if (self.needsSync(urlState)) { self.update(urlState, true); } }); - $scope.onAppEvent('panel-change-view', function(evt, payload) { + $scope.onAppEvent('panel-change-view', (evt, payload) => { self.update(payload); }); - $scope.onAppEvent('panel-initialized', function(evt, payload) { + $scope.onAppEvent('panel-initialized', (evt, payload) => { self.registerPanel(payload.scope); }); @@ -156,7 +156,7 @@ export class DashboardViewState { } getPanelScope(id) { - return _.find(this.panelScopes, function(panelScope) { + return _.find(this.panelScopes, panelScope => { return panelScope.ctrl.panel.id === id; }); } @@ -176,7 +176,7 @@ export class DashboardViewState { return false; } - this.$timeout(function() { + this.$timeout(() => { if (self.oldTimeRange !== ctrl.range) { self.$rootScope.$broadcast('refresh'); } else { @@ -216,7 +216,7 @@ export class DashboardViewState { } } - const unbind = panelScope.$on('$destroy', function() { + const unbind = panelScope.$on('$destroy', () => { self.panelScopes = _.without(self.panelScopes, panelScope); unbind(); }); @@ -226,7 +226,7 @@ export class DashboardViewState { /** @ngInject */ export function dashboardViewStateSrv($location, $timeout, $rootScope) { return { - create: function($scope) { + create: $scope => { return new DashboardViewState($scope, $location, $timeout, $rootScope); }, }; diff --git a/public/app/features/dashlinks/module.ts b/public/app/features/dashlinks/module.ts index aab6817b327..fde41a08d52 100644 --- a/public/app/features/dashlinks/module.ts +++ b/public/app/features/dashlinks/module.ts @@ -10,7 +10,7 @@ function dashLinksContainer() { restrict: 'E', controller: 'DashLinksContainerCtrl', template: '', - link: function() {}, + link: () => {}, }; } @@ -18,7 +18,7 @@ function dashLinksContainer() { function dashLink($compile, $sanitize, linkSrv) { return { restrict: 'E', - link: function(scope, elem) { + link: (scope, elem) => { const link = scope.link; let template = '
      ' + @@ -130,16 +130,16 @@ export class DashLinksContainerCtrl { function updateDashLinks() { const promises = _.map($scope.links, buildLinks); - $q.all(promises).then(function(results) { + $q.all(promises).then(results => { $scope.generatedLinks = _.flatten(results); }); } - $scope.searchDashboards = function(link, limit) { - return backendSrv.search({ tag: link.tags, limit: limit }).then(function(results) { + $scope.searchDashboards = (link, limit) => { + return backendSrv.search({ tag: link.tags, limit: limit }).then(results => { return _.reduce( results, - function(memo, dash) { + (memo, dash) => { // do not add current dashboard if (dash.id !== currentDashId) { memo.push({ @@ -158,9 +158,9 @@ export class DashLinksContainerCtrl { }); }; - $scope.fillDropdown = function(link) { - $scope.searchDashboards(link, 100).then(function(results) { - _.each(results, function(hit) { + $scope.fillDropdown = link => { + $scope.searchDashboards(link, 100).then(results => { + _.each(results, hit => { hit.url = linkSrv.getLinkUrl(hit); }); link.searchHits = results; diff --git a/public/app/features/org/change_password_ctrl.ts b/public/app/features/org/change_password_ctrl.ts index 033ff807721..7a4ba0f031a 100644 --- a/public/app/features/org/change_password_ctrl.ts +++ b/public/app/features/org/change_password_ctrl.ts @@ -9,7 +9,7 @@ export class ChangePasswordCtrl { $scope.ldapEnabled = config.ldapEnabled; $scope.navModel = navModelSrv.getNav('profile', 'change-password', 0); - $scope.changePassword = function() { + $scope.changePassword = () => { if (!$scope.userForm.$valid) { return; } @@ -19,7 +19,7 @@ export class ChangePasswordCtrl { return; } - backendSrv.put('/api/user/password', $scope.command).then(function() { + backendSrv.put('/api/user/password', $scope.command).then(() => { $location.path('profile'); }); }; diff --git a/public/app/features/org/new_org_ctrl.ts b/public/app/features/org/new_org_ctrl.ts index bc87010c100..6a8808abfac 100644 --- a/public/app/features/org/new_org_ctrl.ts +++ b/public/app/features/org/new_org_ctrl.ts @@ -7,9 +7,9 @@ export class NewOrgCtrl { $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1); $scope.newOrg = { name: '' }; - $scope.createOrg = function() { - backendSrv.post('/api/orgs/', $scope.newOrg).then(function(result) { - backendSrv.post('/api/user/using/' + result.orgId).then(function() { + $scope.createOrg = () => { + backendSrv.post('/api/orgs/', $scope.newOrg).then(result => { + backendSrv.post('/api/user/using/' + result.orgId).then(() => { window.location.href = config.appSubUrl + '/org'; }); }); diff --git a/public/app/features/org/org_api_keys_ctrl.ts b/public/app/features/org/org_api_keys_ctrl.ts index 668d6a86841..1ead0a350b9 100644 --- a/public/app/features/org/org_api_keys_ctrl.ts +++ b/public/app/features/org/org_api_keys_ctrl.ts @@ -8,22 +8,22 @@ export class OrgApiKeysCtrl { $scope.roleTypes = ['Viewer', 'Editor', 'Admin']; $scope.token = { role: 'Viewer' }; - $scope.init = function() { + $scope.init = () => { $scope.getTokens(); }; - $scope.getTokens = function() { - backendSrv.get('/api/auth/keys').then(function(tokens) { + $scope.getTokens = () => { + backendSrv.get('/api/auth/keys').then(tokens => { $scope.tokens = tokens; }); }; - $scope.removeToken = function(id) { + $scope.removeToken = id => { backendSrv.delete('/api/auth/keys/' + id).then($scope.getTokens); }; - $scope.addToken = function() { - backendSrv.post('/api/auth/keys', $scope.token).then(function(result) { + $scope.addToken = () => { + backendSrv.post('/api/auth/keys', $scope.token).then(result => { const modalScope = $scope.$new(true); modalScope.key = result.key; modalScope.rootPath = window.location.origin + $scope.$root.appSubUrl; diff --git a/public/app/features/org/select_org_ctrl.ts b/public/app/features/org/select_org_ctrl.ts index 34cfc9b7df4..cd5166771ac 100644 --- a/public/app/features/org/select_org_ctrl.ts +++ b/public/app/features/org/select_org_ctrl.ts @@ -14,18 +14,18 @@ export class SelectOrgCtrl { }, }; - $scope.init = function() { + $scope.init = () => { $scope.getUserOrgs(); }; - $scope.getUserOrgs = function() { - backendSrv.get('/api/user/orgs').then(function(orgs) { + $scope.getUserOrgs = () => { + backendSrv.get('/api/user/orgs').then(orgs => { $scope.orgs = orgs; }); }; - $scope.setUsingOrg = function(org) { - backendSrv.post('/api/user/using/' + org.orgId).then(function() { + $scope.setUsingOrg = org => { + backendSrv.post('/api/user/using/' + org.orgId).then(() => { window.location.href = config.appSubUrl + '/'; }); }; diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 065bd1aa791..8b742e17952 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -54,13 +54,13 @@ const panelTemplate = `
      `; -module.directive('grafanaPanel', function($rootScope, $document, $timeout) { +module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { return { restrict: 'E', template: panelTemplate, transclude: true, scope: { ctrl: '=' }, - link: function(scope, elem) { + link: (scope, elem) => { const panelContainer = elem.find('.panel-container'); const panelContent = elem.find('.panel-content'); const cornerInfoElem = elem.find('.panel-info-corner'); @@ -184,7 +184,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { infoDrop = new Drop({ target: cornerInfoElem[0], - content: function() { + content: () => { return ctrl.getInfoContent({ mode: 'tooltip' }); }, classes: ctrl.error ? 'drop-error' : 'drop-help', @@ -208,7 +208,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { scope.$watchGroup(['ctrl.error', 'ctrl.panel.description'], updatePanelCornerInfo); scope.$watchCollection('ctrl.panel.links', updatePanelCornerInfo); - cornerInfoElem.on('click', function() { + cornerInfoElem.on('click', () => { infoDrop.close(); scope.$apply(ctrl.openInspector.bind(ctrl)); }); @@ -216,7 +216,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { elem.on('mouseenter', mouseEnter); elem.on('mouseleave', mouseLeave); - scope.$on('$destroy', function() { + scope.$on('$destroy', () => { elem.off(); cornerInfoElem.off(); @@ -232,7 +232,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { }; }); -module.directive('panelHelpCorner', function($rootScope) { +module.directive('panelHelpCorner', $rootScope => { return { restrict: 'E', template: ` @@ -242,6 +242,6 @@ module.directive('panelHelpCorner', function($rootScope) { `, - link: function(scope, elem) {}, + link: (scope, elem) => {}, }; }); diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index 102f065cff2..5fa20c4714b 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -85,12 +85,12 @@ function panelHeader($compile) { return { restrict: 'E', template: template, - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { const menuElem = elem.find('.panel-menu'); let menuScope; let isDragged; - elem.click(function(evt) { + elem.click(evt => { const targetClass = evt.target.className; // remove existing scope diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index e4d2eb5a302..c19efd4d065 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -170,8 +170,8 @@ export function queryTroubleshooter() { panelCtrl: '=', isOpen: '=', }, - link: function(scope, elem, attrs, ctrl) { - ctrl.renderJsonExplorer = function(data) { + link: (scope, elem, attrs, ctrl) => { + ctrl.renderJsonExplorer = data => { const jsonElem = elem.find('.query-troubleshooter-json'); ctrl.jsonExplorer = new JsonExplorer(data, 3, { diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 53bacd68fe7..0e45fe48c4d 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -7,7 +7,7 @@ export class SoloPanelCtrl { constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) { let panelId; - $scope.init = function() { + $scope.init = () => { contextSrv.sidemenu = false; appEvents.emit('toggle-sidemenu-hidden'); @@ -27,13 +27,13 @@ export class SoloPanelCtrl { return; } - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(result => { result.meta.soloMode = true; $scope.initDashboard(result, $scope); }); }; - $scope.initPanelScope = function() { + $scope.initPanelScope = () => { const panelInfo = $scope.dashboard.getPanelInfoById(panelId); // fake row ctrl scope diff --git a/public/app/features/panellinks/module.ts b/public/app/features/panellinks/module.ts index c8b6041a3e1..a5605c1d641 100644 --- a/public/app/features/panellinks/module.ts +++ b/public/app/features/panellinks/module.ts @@ -10,7 +10,7 @@ function panelLinksEditor() { restrict: 'E', controller: 'PanelLinksEditorCtrl', templateUrl: 'public/app/features/panellinks/module.html', - link: function() {}, + link: () => {}, }; } @@ -19,15 +19,15 @@ export class PanelLinksEditorCtrl { constructor($scope, backendSrv) { $scope.panel.links = $scope.panel.links || []; - $scope.addLink = function() { + $scope.addLink = () => { $scope.panel.links.push({ type: 'dashboard', }); }; - $scope.searchDashboards = function(queryStr, callback) { - backendSrv.search({ query: queryStr }).then(function(hits) { - const dashboards = _.map(hits, function(dash) { + $scope.searchDashboards = (queryStr, callback) => { + backendSrv.search({ query: queryStr }).then(hits => { + const dashboards = _.map(hits, dash => { return dash.title; }); @@ -35,8 +35,8 @@ export class PanelLinksEditorCtrl { }); }; - $scope.dashboardChanged = function(link) { - backendSrv.search({ query: link.dashboard }).then(function(hits) { + $scope.dashboardChanged = link => { + backendSrv.search({ query: link.dashboard }).then(hits => { const dashboard = _.find(hits, { title: link.dashboard }); if (dashboard) { if (dashboard.url) { @@ -50,7 +50,7 @@ export class PanelLinksEditorCtrl { }); }; - $scope.deleteLink = function(link) { + $scope.deleteLink = link => { $scope.panel.links = _.without($scope.panel.links, link); }; } diff --git a/public/app/features/playlist/playlist_routes.ts b/public/app/features/playlist/playlist_routes.ts index 8e9913cbc60..6e907ea0858 100644 --- a/public/app/features/playlist/playlist_routes.ts +++ b/public/app/features/playlist/playlist_routes.ts @@ -21,7 +21,7 @@ function grafanaRoutes($routeProvider) { .when('/playlists/play/:id', { template: '', resolve: { - init: function(playlistSrv, $route) { + init: (playlistSrv, $route) => { const playlistId = $route.current.params.id; playlistSrv.start(playlistId); }, diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 3c1a1860ac3..19889d3e26e 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -200,7 +200,7 @@ export class DataSourceEditCtrl { coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl); -coreModule.directive('datasourceHttpSettings', function() { +coreModule.directive('datasourceHttpSettings', () => { return { scope: { current: '=', @@ -209,15 +209,15 @@ coreModule.directive('datasourceHttpSettings', function() { }, templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html', link: { - pre: function($scope, elem, attrs) { + pre: ($scope, elem, attrs) => { // do not show access option if direct access is disabled $scope.showAccessOption = $scope.noDirectAccess !== 'true'; $scope.showAccessHelp = false; - $scope.toggleAccessHelp = function() { + $scope.toggleAccessHelp = () => { $scope.showAccessHelp = !$scope.showAccessHelp; }; - $scope.getSuggestUrls = function() { + $scope.getSuggestUrls = () => { return [$scope.suggestUrl]; }; }, diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index dc55ee2a181..41d1b6f1deb 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -36,7 +36,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // handle relative template urls for plugin templates options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); - return function() { + return () => { return { templateUrl: options.Component.templateUrl, template: options.Component.template, @@ -71,12 +71,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ const panelInfo = config.panels[scope.panel.type]; let panelCtrlPromise = Promise.resolve(UnknownPanelCtrl); if (panelInfo) { - panelCtrlPromise = importPluginModule(panelInfo.module).then(function(panelModule) { + panelCtrlPromise = importPluginModule(panelInfo.module).then(panelModule => { return panelModule.PanelCtrl; }); } - return panelCtrlPromise.then(function(PanelCtrl: any) { + return panelCtrlPromise.then((PanelCtrl: any) => { componentInfo.Component = PanelCtrl; if (!PanelCtrl || PanelCtrl.registered) { @@ -128,7 +128,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // Annotations case 'annotations-query-ctrl': { - return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) { + return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(dsModule => { return { baseUrl: scope.ctrl.currentDatasource.meta.baseUrl, name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id, @@ -144,7 +144,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // Datasource ConfigCtrl case 'datasource-config-ctrl': { const dsMeta = scope.ctrl.datasourceMeta; - return importPluginModule(dsMeta.module).then(function(dsModule): any { + return importPluginModule(dsMeta.module).then((dsModule): any => { if (!dsModule.ConfigCtrl) { return { notFound: true }; } @@ -161,7 +161,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // AppConfigCtrl case 'app-config-ctrl': { const model = scope.ctrl.model; - return importPluginModule(model.module).then(function(appModule) { + return importPluginModule(model.module).then(appModule => { return { baseUrl: model.baseUrl, name: 'app-config-' + model.id, @@ -174,7 +174,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // App Page case 'app-page': { const appModel = scope.ctrl.appModel; - return importPluginModule(appModel.module).then(function(appModule) { + return importPluginModule(appModel.module).then(appModule => { return { baseUrl: appModel.baseUrl, name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug, @@ -206,9 +206,9 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ elem.empty(); // let a binding digest cycle complete before adding to dom - setTimeout(function() { + setTimeout(() => { elem.append(child); - scope.$applyAsync(function() { + scope.$applyAsync(() => { scope.$broadcast('component-did-mount'); scope.$broadcast('refresh'); }); @@ -239,9 +239,9 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ return { restrict: 'E', - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { getModule(scope, attrs) - .then(function(componentInfo) { + .then(componentInfo => { registerPluginComponent(scope, elem, attrs, componentInfo); }) .catch(err => { diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index c970b73fe59..16465740642 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -10,7 +10,7 @@ import { CustomVariable } from './custom_variable'; import { ConstantVariable } from './constant_variable'; import { AdhocVariable } from './adhoc_variable'; -coreModule.factory('templateSrv', function() { +coreModule.factory('templateSrv', () => { return templateSrv; }); diff --git a/public/app/features/templating/custom_variable.ts b/public/app/features/templating/custom_variable.ts index fe383c68077..bc946458705 100644 --- a/public/app/features/templating/custom_variable.ts +++ b/public/app/features/templating/custom_variable.ts @@ -39,7 +39,7 @@ export class CustomVariable implements Variable { updateOptions() { // extract options in comma separated string - this.options = _.map(this.query.split(/[,]+/), function(text) { + this.options = _.map(this.query.split(/[,]+/), text => { return { text: text.trim(), value: text.trim() }; }); diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 9dc6468415a..cef7c9cc912 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -30,31 +30,31 @@ export class VariableEditorCtrl { $scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }]; - $scope.init = function() { + $scope.init = () => { $scope.mode = 'list'; $scope.variables = variableSrv.variables; $scope.reset(); - $scope.$watch('mode', function(val) { + $scope.$watch('mode', val => { if (val === 'new') { $scope.reset(); } }); }; - $scope.setMode = function(mode) { + $scope.setMode = mode => { $scope.mode = mode; }; - $scope.add = function() { + $scope.add = () => { if ($scope.isValid()) { variableSrv.addVariable($scope.current); $scope.update(); } }; - $scope.isValid = function() { + $scope.isValid = () => { if (!$scope.ctrl.form.$valid) { return false; } @@ -84,7 +84,7 @@ export class VariableEditorCtrl { return true; }; - $scope.validate = function() { + $scope.validate = () => { $scope.infoText = ''; if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) { $scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource'; @@ -96,7 +96,7 @@ export class VariableEditorCtrl { } }; - $scope.runQuery = function() { + $scope.runQuery = () => { $scope.optionsLimit = 20; return variableSrv.updateOptions($scope.current).catch(err => { if (err.data && err.data.message) { @@ -106,23 +106,23 @@ export class VariableEditorCtrl { }); }; - $scope.edit = function(variable) { + $scope.edit = variable => { $scope.current = variable; $scope.currentIsNew = false; $scope.mode = 'edit'; $scope.validate(); }; - $scope.duplicate = function(variable) { + $scope.duplicate = variable => { const clone = _.cloneDeep(variable.getSaveModel()); $scope.current = variableSrv.createVariableFromModel(clone); $scope.current.name = 'copy_of_' + variable.name; variableSrv.addVariable($scope.current); }; - $scope.update = function() { + $scope.update = () => { if ($scope.isValid()) { - $scope.runQuery().then(function() { + $scope.runQuery().then(() => { $scope.reset(); $scope.mode = 'list'; templateSrv.updateTemplateData(); @@ -130,18 +130,18 @@ export class VariableEditorCtrl { } }; - $scope.reset = function() { + $scope.reset = () => { $scope.currentIsNew = true; $scope.current = variableSrv.createVariableFromModel({ type: 'query' }); // this is done here in case a new data source type variable was added - $scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) { + $scope.datasources = _.filter(datasourceSrv.getMetricSources(), ds => { return !ds.meta.mixed && ds.value !== null; }); $scope.datasourceTypes = _($scope.datasources) .uniqBy('meta.id') - .map(function(ds) { + .map(ds => { return { text: ds.meta.name, value: ds.meta.id }; }) .value(); @@ -164,11 +164,11 @@ export class VariableEditorCtrl { $scope.validate(); }; - $scope.removeVariable = function(variable) { + $scope.removeVariable = variable => { variableSrv.removeVariable(variable); }; - $scope.showMoreOptions = function() { + $scope.showMoreOptions = () => { $scope.optionsLimit += 20; }; } diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index 7f12b1d3a77..57e5ae8eec3 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -65,7 +65,7 @@ export class IntervalVariable implements Variable { updateOptions() { // extract options between quotes and/or comma - this.options = _.map(this.query.match(/(["'])(.*?)\1|\w+/g), function(text) { + this.options = _.map(this.query.match(/(["'])(.*?)\1|\w+/g), text => { text = text.replace(/["']+/g, ''); return { text: text.trim(), value: text.trim() }; }); diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index e1ffcb837cb..d3f39023cfb 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -106,8 +106,8 @@ export class QueryVariable implements Variable { getValuesForTag(tagKey) { return this.datasourceSrv.get(this.datasource).then(datasource => { const query = this.tagValuesQuery.replace('$tag', tagKey); - return this.metricFindQuery(datasource, query).then(function(results) { - return _.map(results, function(value) { + return this.metricFindQuery(datasource, query).then(results => { + return _.map(results, value => { return value.text; }); }); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 3510c729cdb..6eab51abbfa 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -77,7 +77,7 @@ export class TemplateSrv { if (value instanceof Array && value.length === 0) { return '__empty__'; } - const quotedValues = _.map(value, function(val) { + const quotedValues = _.map(value, val => { return '"' + luceneEscape(val) + '"'; }); return '(' + quotedValues.join(' OR ') + ')'; @@ -248,7 +248,7 @@ export class TemplateSrv { } fillVariableValuesForUrl(params, scopedVars) { - _.each(this.variables, function(variable) { + _.each(this.variables, variable => { if (scopedVars && scopedVars[variable.name] !== void 0) { if (scopedVars[variable.name].skipUrlSync) { return; @@ -264,7 +264,7 @@ export class TemplateSrv { } distributeVariable(value, variable) { - value = _.map(value, function(val, index) { + value = _.map(value, (val, index) => { if (index !== 0) { return variable + '=' + val; } else { diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index a30c10d7ddc..8c0f1f11f77 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -175,10 +175,10 @@ export class VariableSrv { selected = variable.options[0]; } else { selected = { - value: _.map(selected, function(val) { + value: _.map(selected, val => { return val.value; }), - text: _.map(selected, function(val) { + text: _.map(selected, val => { return val.text; }).join(' + '), }; @@ -250,7 +250,7 @@ export class VariableSrv { const params = this.$location.search(); // remove variable params - _.each(params, function(value, key) { + _.each(params, (value, key) => { if (key.indexOf('var-') === 0) { delete params[key]; } diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index b161a5e7a87..438cdaa9137 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -50,7 +50,7 @@ export function reactContainer( ReactDOM.render(WrapInProvider(store, component, props), elem[0]); - scope.$on('$destroy', function() { + scope.$on('$destroy', () => { ReactDOM.unmountComponentAtNode(elem[0]); }); }, diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index a14efd3c508..b3e329bd4f2 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -7,7 +7,7 @@ export class LoadDashboardCtrl { $scope.appEvent('dashboard-fetch-start'); if (!$routeParams.uid && !$routeParams.slug) { - backendSrv.get('/api/dashboards/home').then(function(homeDash) { + backendSrv.get('/api/dashboards/home').then(homeDash => { if (homeDash.redirectUri) { const newUrl = locationUtil.stripBaseFromUrl(homeDash.redirectUri); $location.path(newUrl); @@ -30,7 +30,7 @@ export class LoadDashboardCtrl { return; } - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(result => { if (result.meta.url) { const url = locationUtil.stripBaseFromUrl(result.meta.url); From a95453036b5f9a0f07e5e438d8ebee0e7e72e262 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 5 Sep 2018 09:46:22 +0200 Subject: [PATCH 0094/2611] Add min time interval to postgres datasource --- public/app/plugins/datasource/postgres/datasource.ts | 2 ++ .../plugins/datasource/postgres/partials/config.html | 10 ++++++++++ public/app/plugins/datasource/postgres/plugin.json | 6 +++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 6522032b39f..7d4274c8d50 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -8,6 +8,7 @@ export class PostgresDatasource { jsonData: any; responseParser: ResponseParser; queryModel: PostgresQuery; + interval: string; /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) { @@ -16,6 +17,7 @@ export class PostgresDatasource { this.jsonData = instanceSettings.jsonData; this.responseParser = new ResponseParser(this.$q); this.queryModel = new PostgresQuery({}); + this.interval = instanceSettings.jsonData.timeInterval; } interpolateVariable(value, variable) { diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index a4df858db7e..c8b551c2aa8 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -61,6 +61,16 @@
      +
      +
      + Min time interval + + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, + for example 1m if your data is written every minute. + +
      +

      diff --git a/public/app/plugins/datasource/postgres/plugin.json b/public/app/plugins/datasource/postgres/plugin.json index 2c2e1690a65..f236aa01b06 100644 --- a/public/app/plugins/datasource/postgres/plugin.json +++ b/public/app/plugins/datasource/postgres/plugin.json @@ -18,6 +18,10 @@ "alerting": true, "annotations": true, - "metrics": true + "metrics": true, + + "queryOptions": { + "minInterval": true + } } From fd269945c960cd85a7c7ebc4ebe9a4449bc8d063 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 5 Sep 2018 09:54:05 +0200 Subject: [PATCH 0095/2611] document postgres min time interval --- docs/sources/administration/provisioning.md | 2 +- docs/sources/features/datasources/postgres.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index f3d4091defa..41a7ee7f2af 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -154,7 +154,7 @@ Since not all datasources have the same configuration settings we only have the | tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | -| timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source | +| timeInterval | string | Elastic, InfluxDB, PostgreSQL & Prometheus | Lowest interval/step value that should be used for this data source | | esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) | | timeField | string | Elastic | Which field that should be used as timestamp | | interval | string | Elastic | Index date time format | diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 4dfe6929bc1..630823cf781 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -34,6 +34,21 @@ Name | Description *Version* | This option determines which functions are available in the query builder (only available in Grafana 5.3+). *TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+). +### Min time interval +A lower limit for the `$__interval` variable. Recommended to be set to write frequency, for example `1m` if your data is written every minute. +This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a +number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported: + +Identifier | Description +------------ | ------------- +`y` | year +`M` | month +`w` | week +`d` | day +`h` | hour +`m` | minute +`s` | second +`ms` | millisecond ### Database User Permissions (Important!) From e2c7b010acd23910abff5dfc6fc9b49c60159377 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 5 Sep 2018 10:03:16 +0200 Subject: [PATCH 0096/2611] fix test failures for timeInterval --- public/app/plugins/datasource/postgres/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 7d4274c8d50..f1db05cabe8 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -17,7 +17,7 @@ export class PostgresDatasource { this.jsonData = instanceSettings.jsonData; this.responseParser = new ResponseParser(this.$q); this.queryModel = new PostgresQuery({}); - this.interval = instanceSettings.jsonData.timeInterval; + this.interval = (instanceSettings.jsonData || {}).timeInterval; } interpolateVariable(value, variable) { From 777010b20b52a2a95e9bf47f2ef472ebfbc6e49f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 5 Sep 2018 10:53:58 +0200 Subject: [PATCH 0097/2611] added no-conditional-assignment rule and changed files to follow new rule --- public/app/containers/Explore/utils/dom.ts | 3 ++- public/app/core/components/search/search.ts | 3 ++- public/app/core/directives/rebuild_on_change.ts | 4 +++- public/app/features/dashboard/upload.ts | 5 ++++- public/app/plugins/datasource/graphite/datasource.ts | 10 ++++++---- public/app/plugins/datasource/graphite/lexer.ts | 5 +++-- .../plugins/datasource/logging/result_transformer.ts | 5 +++-- public/app/plugins/datasource/prometheus/datasource.ts | 6 ++++-- tslint.json | 1 + 9 files changed, 28 insertions(+), 14 deletions(-) diff --git a/public/app/containers/Explore/utils/dom.ts b/public/app/containers/Explore/utils/dom.ts index 6ab3de39923..381c150e3f4 100644 --- a/public/app/containers/Explore/utils/dom.ts +++ b/public/app/containers/Explore/utils/dom.ts @@ -9,7 +9,8 @@ if ('Element' in window && !Element.prototype.closest) { i = matches.length; // eslint-disable-next-line while (--i >= 0 && matches.item(i) !== el) {} - } while (i < 0 && (el = el.parentElement)); + el = el.parentElement; + } while (i < 0 && el); return el; }; } diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index b8e3ea5d3e2..d459c497521 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -131,7 +131,8 @@ export class SearchCtrl { const max = flattenedResult.length; let newIndex = this.selectedIndex + direction; - this.selectedIndex = (newIndex %= max) < 0 ? newIndex + max : newIndex; + const something = (newIndex %= max); + this.selectedIndex = something < 0 ? newIndex + max : newIndex; const selectedItem = flattenedResult[this.selectedIndex]; if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) { diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts index 72b9c05064a..12034874bf9 100644 --- a/public/app/core/directives/rebuild_on_change.ts +++ b/public/app/core/directives/rebuild_on_change.ts @@ -5,14 +5,16 @@ function getBlockNodes(nodes) { let node = nodes[0]; const endNode = nodes[nodes.length - 1]; let blockNodes; + node = node.nextSibling; - for (let i = 1; node !== endNode && (node = node.nextSibling); i++) { + for (let i = 1; node !== endNode && node; i++) { if (blockNodes || nodes[i] !== node) { if (!blockNodes) { blockNodes = $([].slice.call(nodes, 0, i)); } blockNodes.push(node); } + node = node.nextSibling; } return blockNodes || nodes; diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 10d35d1f300..ee77c6e5394 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -36,10 +36,13 @@ function uploadDashboardDirective(timer, alertSrv, $location) { }; }; - for (let i = 0, f; (f = files[i]); i++) { + let i = 0; + let f = files[i]; + for (i; f; i++) { const reader = new FileReader(); reader.onload = readerOnload(); reader.readAsText(f); + f = files[i]; } } diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index e07212491b0..78ce36f74a3 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -218,9 +218,10 @@ export function GraphiteDatasource(this: any, instanceSettings, $q, backendSrv, if (matches) { const expressions = []; const exprRegex = /, *([^,]+)/g; - let match; - while ((match = exprRegex.exec(matches[2])) !== null) { + let match = exprRegex.exec(matches[2]); + while (match !== null) { expressions.push(match[1]); + match = exprRegex.exec(matches[2]); } options.limit = 10000; return this.getTagValuesAutoComplete(expressions, matches[1], undefined, options); @@ -233,9 +234,10 @@ export function GraphiteDatasource(this: any, instanceSettings, $q, backendSrv, if (matches[1]) { expressions.push(matches[1]); const exprRegex = /, *([^,]+)/g; - let match; - while ((match = exprRegex.exec(matches[2])) !== null) { + let match = exprRegex.exec(matches[2]); + while (match !== null) { expressions.push(match[1]); + match = exprRegex.exec(matches[2]); } } options.limit = 10000; diff --git a/public/app/plugins/datasource/graphite/lexer.ts b/public/app/plugins/datasource/graphite/lexer.ts index 1f2da854991..8999751f08b 100644 --- a/public/app/plugins/datasource/graphite/lexer.ts +++ b/public/app/plugins/datasource/graphite/lexer.ts @@ -941,9 +941,10 @@ Lexer.prototype = { tokenize: function() { const list = []; - let token; - while ((token = this.next())) { + let token = this.next(); + while (token) { list.push(token); + token = this.next(); } return list; }, diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts index e238778614c..891f9268068 100644 --- a/public/app/plugins/datasource/logging/result_transformer.ts +++ b/public/app/plugins/datasource/logging/result_transformer.ts @@ -26,13 +26,14 @@ export function getSearchMatches(line: string, search: string) { } const regexp = new RegExp(`(?:${search})`, 'g'); const matches = []; - let match; - while ((match = regexp.exec(line))) { + let match = regexp.exec(line); + while (match) { matches.push({ text: match[0], start: match.index, length: match[0].length, }); + match = regexp.exec(line); } return matches; } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 356322cf369..e3158e64d18 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -55,11 +55,12 @@ export function addLabelToQuery(query: string, key: string, value: string): stri // Adding label to existing selectors const selectorRegexp = /{([^{]*)}/g; - let match = null; + let match = selectorRegexp.exec(query); const parts = []; let lastIndex = 0; let suffix = ''; - while ((match = selectorRegexp.exec(query))) { + + while (match) { const prefix = query.slice(lastIndex, match.index); const selectorParts = match[1].split(','); const labels = selectorParts.reduce((acc, label) => { @@ -77,6 +78,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri 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(''); diff --git a/tslint.json b/tslint.json index 0d525bea3f0..13323068ec1 100644 --- a/tslint.json +++ b/tslint.json @@ -33,6 +33,7 @@ "no-angle-bracket-type-assertion": true, "no-arg": true, "no-bitwise": false, + "no-conditional-assignment": true, "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], "no-construct": true, "no-debugger": true, From 8ea2f7f8581cc821e8e1b82ac3cd9dc8a2946026 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 5 Sep 2018 11:55:28 +0200 Subject: [PATCH 0098/2611] build: updated build-container with go1.11. --- .circleci/config.yml | 4 ++-- scripts/build/build-all.sh | 4 ++++ scripts/build/build.sh | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b4480b4bade..fbc45e6abea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,7 +125,7 @@ jobs: build-all: docker: - - image: grafana/build-container:1.0.0 + - image: grafana/build-container:build working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -168,7 +168,7 @@ jobs: build: docker: - - image: grafana/build-container:1.0.0 + - image: grafana/build-container:build working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 6029b14605a..0aaab2ce4a6 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -45,6 +45,10 @@ else fi echo "Building frontend" go run build.go ${OPT} build-frontend + +# Load ruby, needed for packing with fpm +source /etc/profile.d/rvm.sh + echo "Packaging" go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest #removing amd64 phantomjs bin for armv7/arm64 packages diff --git a/scripts/build/build.sh b/scripts/build/build.sh index a02f079dd72..d4c1c788b30 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -33,5 +33,8 @@ fi echo "Building frontend" go run build.go ${OPT} build-frontend +# Load ruby, needed for packing with fpm +source /etc/profile.d/rvm.sh + echo "Packaging" go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest From cb526d4557cf99bc038dbd5973607b3ced0d83be Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 5 Sep 2018 12:02:57 +0200 Subject: [PATCH 0099/2611] Add min time interval to mysql and mssql --- docs/sources/administration/provisioning.md | 2 +- docs/sources/features/datasources/mssql.md | 16 ++++++++++++++++ docs/sources/features/datasources/mysql.md | 16 ++++++++++++++++ .../app/plugins/datasource/mssql/datasource.ts | 2 ++ .../datasource/mssql/partials/config.html | 15 +++++++++++++++ public/app/plugins/datasource/mssql/plugin.json | 7 ++++++- .../app/plugins/datasource/mysql/datasource.ts | 2 ++ .../datasource/mysql/partials/config.html | 15 +++++++++++++++ public/app/plugins/datasource/mysql/plugin.json | 7 ++++++- 9 files changed, 79 insertions(+), 3 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 41a7ee7f2af..b2310378f16 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -154,7 +154,7 @@ Since not all datasources have the same configuration settings we only have the | tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | -| timeInterval | string | Elastic, InfluxDB, PostgreSQL & Prometheus | Lowest interval/step value that should be used for this data source | +| timeInterval | string | Elastic, InfluxDB, MSSQL, MySQL, PostgreSQL & Prometheus | Lowest interval/step value that should be used for this data source | | esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) | | timeField | string | Elastic | Which field that should be used as timestamp | | interval | string | Elastic | Index date time format | diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index da0c9581e99..869f25f70cf 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -33,6 +33,22 @@ Name | Description *User* | Database user's login/username *Password* | Database user's password +### Min time interval +A lower limit for the `$__interval` variable. Recommended to be set to write frequency, for example `1m` if your data is written every minute. +This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a +number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported: + +Identifier | Description +------------ | ------------- +`y` | year +`M` | month +`w` | week +`d` | day +`h` | hour +`m` | minute +`s` | second +`ms` | millisecond + ### Database User Permissions (Important!) The database user you specify when you add the data source should only be granted SELECT permissions on diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index afac746b050..91986866a85 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -36,6 +36,22 @@ Name | Description *User* | Database user's login/username *Password* | Database user's password +### Min time interval +A lower limit for the `$__interval` variable. Recommended to be set to write frequency, for example `1m` if your data is written every minute. +This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a +number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported: + +Identifier | Description +------------ | ------------- +`y` | year +`M` | month +`w` | week +`d` | day +`h` | hour +`m` | minute +`s` | second +`ms` | millisecond + ### Database User Permissions (Important!) The database user you specify when you add the data source should only be granted SELECT permissions on diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index fc497b2c274..3dce972d241 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -5,12 +5,14 @@ export class MssqlDatasource { id: any; name: any; responseParser: ResponseParser; + interval: string; /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(this.$q); + this.interval = (instanceSettings.jsonData || {}).timeInterval; } interpolateVariable(value, variable) { diff --git a/public/app/plugins/datasource/mssql/partials/config.html b/public/app/plugins/datasource/mssql/partials/config.html index 7f9dc03f286..f8a36502009 100644 --- a/public/app/plugins/datasource/mssql/partials/config.html +++ b/public/app/plugins/datasource/mssql/partials/config.html @@ -29,6 +29,21 @@

      +

      MSSQL details

      + +
      +
      +
      + Min time interval + + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, + for example 1m if your data is written every minute. + +
      +
      +
      +
      User Permission
      diff --git a/public/app/plugins/datasource/mssql/plugin.json b/public/app/plugins/datasource/mssql/plugin.json index ac5ea49ebe9..a3df148bc2b 100644 --- a/public/app/plugins/datasource/mssql/plugin.json +++ b/public/app/plugins/datasource/mssql/plugin.json @@ -17,5 +17,10 @@ "alerting": true, "annotations": true, - "metrics": true + "metrics": true, + + "queryOptions": { + "minInterval": true + } + } diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index eca223f2d6d..e09c18bb25a 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -5,12 +5,14 @@ export class MysqlDatasource { id: any; name: any; responseParser: ResponseParser; + interval: string; /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(this.$q); + this.interval = (instanceSettings.jsonData || {}).timeInterval; } interpolateVariable(value, variable) { diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index 8cbeece71dd..6bc9cceb8f1 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -24,6 +24,21 @@
      +

      MySQL details

      + +
      +
      +
      + Min time interval + + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, + for example 1m if your data is written every minute. + +
      +
      +
      +
      User Permission
      diff --git a/public/app/plugins/datasource/mysql/plugin.json b/public/app/plugins/datasource/mysql/plugin.json index 363b9364016..f3a8efe267e 100644 --- a/public/app/plugins/datasource/mysql/plugin.json +++ b/public/app/plugins/datasource/mysql/plugin.json @@ -18,5 +18,10 @@ "alerting": true, "annotations": true, - "metrics": true + "metrics": true, + + "queryOptions": { + "minInterval": true + } + } From dc236b506343a3d1d065c7c6a1c0d200c3f89e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 5 Sep 2018 12:09:16 +0200 Subject: [PATCH 0100/2611] refatoring: minor changes to PR #13149 --- public/app/core/components/search/search.ts | 5 ++--- public/app/features/dashboard/dashboard_import_ctrl.ts | 4 +++- public/app/features/dashboard/upload.ts | 10 ++++++---- public/sass/components/_search.scss | 4 ++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index d459c497521..e347fcd829a 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -130,9 +130,8 @@ export class SearchCtrl { } const max = flattenedResult.length; - let newIndex = this.selectedIndex + direction; - const something = (newIndex %= max); - this.selectedIndex = something < 0 ? newIndex + max : newIndex; + const newIndex = (this.selectedIndex + direction) % max; + this.selectedIndex = newIndex < 0 ? newIndex + max : newIndex; const selectedItem = flattenedResult[this.selectedIndex]; if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) { diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index 3dfae1250dd..455fa682edd 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import config from 'app/core/config'; +import locationUtil from 'app/core/utils/location_util'; export class DashboardImportCtrl { navModel: any; @@ -179,7 +180,8 @@ export class DashboardImportCtrl { folderId: this.folderId, }) .then(res => { - this.$location.url(res.importedUrl); + const dashUrl = locationUtil.stripBaseFromUrl(res.importedUrl); + this.$location.url(dashUrl); }); } diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index ee77c6e5394..974a0c35cd2 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -37,12 +37,14 @@ function uploadDashboardDirective(timer, alertSrv, $location) { }; let i = 0; - let f = files[i]; - for (i; f; i++) { + let file = files[i]; + + while (file) { const reader = new FileReader(); reader.onload = readerOnload(); - reader.readAsText(f); - f = files[i]; + reader.readAsText(file); + i += 1; + file = files[i]; } } diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 8338a5d72ae..a27fc830317 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -192,6 +192,10 @@ &:hover, &.selected { background: $list-item-hover-bg; + + .search-item__body-title { + color: $text-color-strong; + } } } From 275f6130503a92476766c827359bdcf428908671 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 5 Sep 2018 12:12:46 +0200 Subject: [PATCH 0101/2611] Only authenticate logins when password is set (#13147) * auth: never authenticate passwords shorter than 4 chars. * auth: refactoring password length check. * auth: does not authenticate when password is empty. * auth: removes unneccesary change. --- pkg/login/auth.go | 13 ++++++++++++- pkg/login/auth_test.go | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 215a22cde33..991fa72fd54 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -2,7 +2,6 @@ package login import ( "errors" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -14,6 +13,7 @@ var ( ErrProviderDeniedRequest = errors.New("Login provider denied login request") ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter") ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked") + ErrPasswordEmpty = errors.New("No password provided.") ErrUsersQuotaReached = errors.New("Users quota reached") ErrGettingUserQuota = errors.New("Error getting user quota") ) @@ -28,6 +28,10 @@ func AuthenticateUser(query *m.LoginUserQuery) error { return err } + if err := validatePasswordSet(query.Password); err != nil { + return err + } + err := loginUsingGrafanaDB(query) if err == nil || (err != m.ErrUserNotFound && err != ErrInvalidCredentials) { return err @@ -52,3 +56,10 @@ func AuthenticateUser(query *m.LoginUserQuery) error { return err } +func validatePasswordSet(password string) error { + if len(password) == 0 { + return ErrPasswordEmpty + } + + return nil +} diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go index 932125c410e..a4cd8284cdd 100644 --- a/pkg/login/auth_test.go +++ b/pkg/login/auth_test.go @@ -10,6 +10,24 @@ import ( func TestAuthenticateUser(t *testing.T) { Convey("Authenticate user", t, func() { + authScenario("When a user authenticates without setting a password", func(sc *authScenarioContext) { + mockLoginAttemptValidation(nil, sc) + mockLoginUsingGrafanaDB(nil, sc) + mockLoginUsingLdap(false, nil, sc) + + loginQuery := m.LoginUserQuery{ + Username: "user", + Password: "", + } + err := AuthenticateUser(&loginQuery) + + Convey("login should fail", func() { + So(sc.grafanaLoginWasCalled, ShouldBeFalse) + So(sc.ldapLoginWasCalled, ShouldBeFalse) + So(err, ShouldEqual, ErrPasswordEmpty) + }) + }) + authScenario("When a user authenticates having too many login attempts", func(sc *authScenarioContext) { mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc) mockLoginUsingGrafanaDB(nil, sc) From 306c3e6c10fe755e58ddfe622749f4bd0f2c11bd Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 12:34:32 +0200 Subject: [PATCH 0102/2611] 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 eed141fb54162116d05589f3e72bd601c348e034 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 5 Sep 2018 15:10:57 +0200 Subject: [PATCH 0103/2611] build: uses 1.1.0 of the build container. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fbc45e6abea..eb8724bed3c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,7 +125,7 @@ jobs: build-all: docker: - - image: grafana/build-container:build + - image: grafana/build-container:1.1.0 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -168,7 +168,7 @@ jobs: build: docker: - - image: grafana/build-container:build + - image: grafana/build-container:1.1.0 working_directory: /go/src/github.com/grafana/grafana steps: - checkout From 167f0098193475a0ece4c280d26d1dfa4fee5d1a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:13:21 +0200 Subject: [PATCH 0104/2611] 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))}
    {LEGEND_STATS.map( - statName => seriesValuesProps[statName] && + statName => + seriesValuesProps[statName] && ( + + ) )}
    + {statName} + {sort === statName && } + - {props.statName} - -
    + props.onClick(e)}> {statName} {sort === statName && }
    - + this.props.onLabelClick(e)} + />
    @@ -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 25f13bd3adafe1adceeee4542b7f33a4ea3d4e57 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 5 Sep 2018 15:28:30 +0200 Subject: [PATCH 0105/2611] added only-arrow-functions rule and changed files to follow new rule (#13154) --- .../components/sql_part/sql_part_editor.ts | 20 ++++++++-------- public/app/core/services/analytics.ts | 2 +- public/app/core/services/ng_react.ts | 2 +- .../plugins/datasource/postgres/query_ctrl.ts | 12 +++++----- .../postgres/specs/postgres_query.test.ts | 24 +++++++++---------- tslint.json | 1 + 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts index 5d0f63a6953..8097dddeb3b 100644 --- a/public/app/core/components/sql_part/sql_part_editor.ts +++ b/public/app/core/components/sql_part/sql_part_editor.ts @@ -55,7 +55,7 @@ export function sqlPartEditorDirective($compile, templateSrv) { } function inputBlur($input, paramIndex) { - cancelBlur = setTimeout(function() { + cancelBlur = setTimeout(() => { switchToLink($input, paramIndex); }, 200); } @@ -95,20 +95,20 @@ export function sqlPartEditorDirective($compile, templateSrv) { return; } - const typeaheadSource = function(query, callback) { + const typeaheadSource = (query, callback) => { if (param.options) { let options = param.options; if (param.type === 'int') { - options = _.map(options, function(val) { + options = _.map(options, val => { return val.toString(); }); } return options; } - $scope.$apply(function() { - $scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(function(result) { - const dynamicOptions = _.map(result, function(op) { + $scope.$apply(() => { + $scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => { + const dynamicOptions = _.map(result, op => { return op.value; }); @@ -128,7 +128,7 @@ export function sqlPartEditorDirective($compile, templateSrv) { source: typeaheadSource, minLength: 0, items: 1000, - updater: function(value) { + updater: value => { if (value === part.params[paramIndex]) { clearTimeout(cancelBlur); $input.focus(); @@ -150,18 +150,18 @@ export function sqlPartEditorDirective($compile, templateSrv) { } } - $scope.showActionsMenu = function() { + $scope.showActionsMenu = () => { $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => { $scope.partActions = res; }); }; - $scope.triggerPartAction = function(action) { + $scope.triggerPartAction = action => { $scope.handleEvent({ $event: { name: 'action', action: action } }); }; function addElementsAndCompile() { - _.each(partDef.params, function(param, index) { + _.each(partDef.params, (param, index) => { if (param.optional && part.params.length <= index) { return; } diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index d50140bbd75..be4371adb26 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -14,8 +14,8 @@ export class Analytics { }); const ga = ((window as any).ga = (window as any).ga || + //tslint:disable-next-line:only-arrow-functions function() { - //tslint:disable-line:only-arrow-functions (ga.q = ga.q || []).push(arguments); }); ga.l = +new Date(); diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 643e34dd62e..6a712b29dab 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -52,8 +52,8 @@ function applied(fn, scope) { if (fn.wrappedInApply) { return fn; } + //tslint:disable-next-line:only-arrow-functions const wrapped: any = function() { - //tslint:disable-line:only-arrow-functions const args = arguments; const phase = scope.$root.$$phase; if (phase === '$apply' || phase === '$digest') { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 1cb8bfa5a05..9343a260a9e 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -96,7 +96,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } updateProjection() { - this.selectParts = _.map(this.target.select, function(parts: any) { + this.selectParts = _.map(this.target.select, (parts: any) => { return _.map(parts, sqlPart.create).filter(n => n); }); this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n); @@ -104,15 +104,15 @@ export class PostgresQueryCtrl extends QueryCtrl { } updatePersistedParts() { - this.target.select = _.map(this.selectParts, function(selectParts) { - return _.map(selectParts, function(part: any) { + this.target.select = _.map(this.selectParts, selectParts => { + return _.map(selectParts, (part: any) => { return { type: part.def.type, datatype: part.datatype, params: part.params }; }); }); - this.target.where = _.map(this.whereParts, function(part: any) { + this.target.where = _.map(this.whereParts, (part: any) => { return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params }; }); - this.target.group = _.map(this.groupParts, function(part: any) { + this.target.group = _.map(this.groupParts, (part: any) => { return { type: part.def.type, datatype: part.datatype, params: part.params }; }); } @@ -355,7 +355,7 @@ export class PostgresQueryCtrl extends QueryCtrl { switch (partType) { case 'column': - const parts = _.map(selectParts, function(part: any) { + const parts = _.map(selectParts, (part: any) => { return sqlPart.create({ type: part.def.type, params: _.clone(part.params) }); }); this.selectParts.push(parts); 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 877bd47618b..0d6f61a8748 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts @@ -1,22 +1,22 @@ import PostgresQuery from '../postgres_query'; -describe('PostgresQuery', function() { +describe('PostgresQuery', () => { const templateSrv = { replace: jest.fn(text => text), }; - describe('When initializing', function() { - it('should not be in SQL mode', function() { + describe('When initializing', () => { + it('should not be in SQL mode', () => { const query = new PostgresQuery({}, templateSrv); expect(query.target.rawQuery).toBe(false); }); - it('should be in SQL mode for pre query builder queries', function() { + it('should be in SQL mode for pre query builder queries', () => { const query = new PostgresQuery({ rawSql: 'SELECT 1' }, templateSrv); expect(query.target.rawQuery).toBe(true); }); }); - describe('When generating time column SQL', function() { + describe('When generating time column SQL', () => { const query = new PostgresQuery({}, templateSrv); query.target.timeColumn = 'time'; @@ -25,7 +25,7 @@ describe('PostgresQuery', function() { expect(query.buildTimeColumn()).toBe('"time" AS "time"'); }); - describe('When generating time column SQL with group by time', function() { + describe('When generating time column SQL with group by time', () => { let query = new PostgresQuery( { timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'none'] }] }, templateSrv @@ -44,7 +44,7 @@ describe('PostgresQuery', function() { expect(query.buildTimeColumn(false)).toBe('$__unixEpochGroup(time,5m)'); }); - describe('When generating metric column SQL', function() { + describe('When generating metric column SQL', () => { const query = new PostgresQuery({}, templateSrv); query.target.metricColumn = 'host'; @@ -53,7 +53,7 @@ describe('PostgresQuery', function() { expect(query.buildMetricColumn()).toBe('"host" AS metric'); }); - describe('When generating value column SQL', function() { + describe('When generating value column SQL', () => { const query = new PostgresQuery({}, templateSrv); let column = [{ type: 'column', params: ['value'] }]; @@ -76,7 +76,7 @@ describe('PostgresQuery', function() { ); }); - describe('When generating value column SQL with metric column', function() { + describe('When generating value column SQL with metric column', () => { const query = new PostgresQuery({}, templateSrv); query.target.metricColumn = 'host'; @@ -110,7 +110,7 @@ describe('PostgresQuery', function() { ); }); - describe('When generating WHERE clause', function() { + describe('When generating WHERE clause', () => { const query = new PostgresQuery({ where: [] }, templateSrv); expect(query.buildWhereClause()).toBe(''); @@ -126,7 +126,7 @@ describe('PostgresQuery', function() { expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t) AND\n v = 1'); }); - describe('When generating GROUP BY clause', function() { + describe('When generating GROUP BY clause', () => { const query = new PostgresQuery({ group: [], metricColumn: 'none' }, templateSrv); expect(query.buildGroupClause()).toBe(''); @@ -136,7 +136,7 @@ describe('PostgresQuery', function() { expect(query.buildGroupClause()).toBe('\nGROUP BY 1,2'); }); - describe('When generating complete statement', function() { + describe('When generating complete statement', () => { const target = { timeColumn: 't', table: 'table', diff --git a/tslint.json b/tslint.json index 13323068ec1..15dc83ec575 100644 --- a/tslint.json +++ b/tslint.json @@ -49,6 +49,7 @@ "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [true, "check-open-brace", "check-catch", "check-else"], + "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], "prefer-const": true, "radix": false, "typedef-whitespace": [ From 7e340b7aa5c1016c934a68d33597672e38e36584 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:32:51 +0200 Subject: [PATCH 0106/2611] 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 500a4b5f354d4fc7119117f82485dc4257f817f1 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 5 Sep 2018 16:46:22 +0200 Subject: [PATCH 0107/2611] docs: default paths in the docker container. --- docs/sources/installation/docker.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index c71dc105ad4..ba0d6199ba4 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -20,7 +20,7 @@ $ docker run -d -p 3000:3000 grafana/grafana ## Configuration -All options defined in conf/grafana.ini can be overridden using environment +All options defined in `conf/grafana.ini` can be overridden using environment variables by using the syntax `GF__`. For example: @@ -40,6 +40,19 @@ those options. > For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container. +### Default Paths + +The following settings are hard-coded when launching the Grafana Docker container and can only be overridden using environment variables, not in `conf/grafana.ini`. + +Setting | Default value +----------------------|--------------------------- +GF_PATHS_CONFIG | /etc/grafana/grafana.ini +GF_PATHS_DATA | /var/lib/grafana +GF_PATHS_HOME | /usr/share/grafana +GF_PATHS_LOGS | /var/log/grafana +GF_PATHS_PLUGINS | /var/lib/grafana/plugins +GF_PATHS_PROVISIONING | /etc/grafana/provisioning + ## Running a Specific Version of Grafana ```bash From f68ac2021873bcc827de00423a5af90daab34faa Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 16:49:36 +0200 Subject: [PATCH 0108/2611] 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 2b74b1c4d615ca19f7f789ea245b809d4261fb6d Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 5 Sep 2018 16:51:31 +0200 Subject: [PATCH 0109/2611] added radix rule and changed files to follow rule (#13153) --- public/app/containers/Explore/TimePicker.tsx | 2 +- public/app/core/utils/rangeutil.ts | 2 +- public/app/features/dashboard/time_srv.ts | 2 +- public/app/features/dashboard/view_state_srv.ts | 2 +- public/app/features/panel/solo_panel_ctrl.ts | 2 +- public/app/features/playlist/playlist_edit_ctrl.ts | 2 +- public/app/plugins/datasource/elasticsearch/bucket_agg.ts | 2 +- public/app/plugins/datasource/elasticsearch/metric_agg.ts | 2 +- public/app/plugins/datasource/influxdb/datasource.ts | 2 +- public/app/plugins/datasource/opentsdb/datasource.ts | 4 ++-- .../app/plugins/datasource/prometheus/result_transformer.ts | 2 +- public/app/plugins/panel/graph/threshold_manager.ts | 2 +- public/app/plugins/panel/heatmap/heatmap_data_converter.ts | 2 +- public/app/plugins/panel/singlestat/module.ts | 2 +- tslint.json | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/public/app/containers/Explore/TimePicker.tsx b/public/app/containers/Explore/TimePicker.tsx index 3ae4ea4a83c..08867f8d0fc 100644 --- a/public/app/containers/Explore/TimePicker.tsx +++ b/public/app/containers/Explore/TimePicker.tsx @@ -16,7 +16,7 @@ export function parseTime(value, isUtc = false, asString = false) { return value; } if (!isNaN(value)) { - const epoch = parseInt(value); + const epoch = parseInt(value, 10); const m = isUtc ? moment.utc(epoch) : moment(epoch); return asString ? m.format(DATE_FORMAT) : m; } diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 852e2ed3c50..484dd0e3327 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -111,7 +111,7 @@ export function describeTextRange(expr: any) { const parts = /^now([-+])(\d+)(\w)/.exec(expr); if (parts) { const unit = parts[3]; - const amount = parseInt(parts[2]); + const amount = parseInt(parts[2], 10); const span = spans[unit]; if (span) { opt.display = isLast ? 'Last ' : 'Next '; diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 4bd78ce776d..dd5a0ba758f 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -70,7 +70,7 @@ export class TimeSrv { } if (!isNaN(value)) { - const epoch = parseInt(value); + const epoch = parseInt(value, 10); return moment.utc(epoch); } diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 521de4ecbad..d9ad6827567 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -49,7 +49,7 @@ export class DashboardViewState { getQueryStringState() { const state = this.$location.search(); - state.panelId = parseInt(state.panelId) || null; + state.panelId = parseInt(state.panelId, 10) || null; state.fullscreen = state.fullscreen ? true : null; state.edit = state.edit === 'true' || state.edit === true || null; state.editview = state.editview || null; diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 0e45fe48c4d..15d35188d6d 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -12,7 +12,7 @@ export class SoloPanelCtrl { appEvents.emit('toggle-sidemenu-hidden'); const params = $location.search(); - panelId = parseInt(params.panelId); + panelId = parseInt(params.panelId, 10); $scope.onAppEvent('dashboard-initialized', $scope.initPanelScope); diff --git a/public/app/features/playlist/playlist_edit_ctrl.ts b/public/app/features/playlist/playlist_edit_ctrl.ts index 3c81aff1093..16da9a0a209 100644 --- a/public/app/features/playlist/playlist_edit_ctrl.ts +++ b/public/app/features/playlist/playlist_edit_ctrl.ts @@ -37,7 +37,7 @@ export class PlaylistEditCtrl { filterFoundPlaylistItems() { this.filteredDashboards = _.reject(this.dashboardresult, playlistItem => { return _.find(this.playlistItems, listPlaylistItem => { - return parseInt(listPlaylistItem.value) === playlistItem.id; + return parseInt(listPlaylistItem.value, 10) === playlistItem.id; }); }); diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts index e17a34778ee..8963f2c3f4b 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts @@ -208,7 +208,7 @@ export class ElasticBucketAggCtrl { const id = _.reduce( $scope.target.bucketAggs.concat($scope.target.metrics), (max, val) => { - return parseInt(val.id) > max ? parseInt(val.id) : max; + return parseInt(val.id, 10) > max ? parseInt(val.id, 10) : max; }, 0 ); diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 7e5300b43e1..623eed68914 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -177,7 +177,7 @@ export class ElasticMetricAggCtrl { const id = _.reduce( $scope.target.bucketAggs.concat($scope.target.metrics), (max, val) => { - return parseInt(val.id) > max ? parseInt(val.id) : max; + return parseInt(val.id, 10) > max ? parseInt(val.id, 10) : max; }, 0 ); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index ec995de630a..5ffbf7cf418 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -314,7 +314,7 @@ export default class InfluxDatasource { const parts = /^now-(\d+)([d|h|m|s])$/.exec(date); if (parts) { - const amount = parseInt(parts[1]); + const amount = parseInt(parts[1], 10); const unit = parts[2]; return 'now() - ' + amount + unit; } diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index 08bd1585b42..7cb0806359d 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -408,11 +408,11 @@ export default class OpenTsDatasource { }; if (target.counterMax && target.counterMax.length) { - query.rateOptions.counterMax = parseInt(target.counterMax); + query.rateOptions.counterMax = parseInt(target.counterMax, 10); } if (target.counterResetValue && target.counterResetValue.length) { - query.rateOptions.resetValue = parseInt(target.counterResetValue); + query.rateOptions.resetValue = parseInt(target.counterResetValue, 10); } if (tsdbVersion >= 2) { diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 96b8e0d4137..bf916bebf04 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -37,7 +37,7 @@ export class ResultTransformer { metricLabel = this.createMetricLabel(metricData.metric, options); - const stepMs = parseInt(options.step) * 1000; + const stepMs = parseInt(options.step, 10) * 1000; let baseTimestamp = start * 1000; if (metricData.values === undefined) { diff --git a/public/app/plugins/panel/graph/threshold_manager.ts b/public/app/plugins/panel/graph/threshold_manager.ts index 46ec9e61854..e7d874e7451 100644 --- a/public/app/plugins/panel/graph/threshold_manager.ts +++ b/public/app/plugins/panel/graph/threshold_manager.ts @@ -53,7 +53,7 @@ export class ThresholdManager { function stopped() { // calculate graph level let graphValue = plot.c2p({ left: 0, top: posTop }).y; - graphValue = parseInt(graphValue.toFixed(0)); + graphValue = parseInt(graphValue.toFixed(0), 10); model.value = graphValue; handleElem.off('mousemove', dragging); diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 68ab6fee92f..99b61be40dc 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -271,7 +271,7 @@ function pushToYBuckets(buckets, bucketNum, value, point, bounds) { let count = 1; // Use the 3rd argument as scale/count if (point.length > 3) { - count = parseInt(point[2]); + count = parseInt(point[2], 10); } if (buckets[bucketNum]) { buckets[bucketNum].values.push(value); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index fe79b5f5043..b10eb68b87e 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -493,7 +493,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { const bgColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; - const fontScale = parseInt(panel.valueFontSize) / 100; + const fontScale = parseInt(panel.valueFontSize, 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; // Reduce gauge width if threshold labels enabled const gaugeWidthReduceRatio = panel.gauge.thresholdLabels ? 1.5 : 1; diff --git a/tslint.json b/tslint.json index 15dc83ec575..4c7ea71366c 100644 --- a/tslint.json +++ b/tslint.json @@ -51,7 +51,7 @@ "one-line": [true, "check-open-brace", "check-catch", "check-else"], "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], "prefer-const": true, - "radix": false, + "radix": true, "typedef-whitespace": [ true, { From 42d26400b1c810de25ef645d44a2dd6198246554 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 5 Sep 2018 18:47:26 +0200 Subject: [PATCH 0110/2611] changelog: add notes about closing #13030 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a623d5ea8e..e8afd55c222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ These are new features that's still being worked on and are in an experimental p ### Tech * **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.3 (2018-08-29) From d76dad86c801aef31c7811e0fdcf1bcb3fcbe547 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 5 Sep 2018 18:49:08 +0200 Subject: [PATCH 0111/2611] changelog: order changes by group (ocd) [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8afd55c222..d8470adc81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,6 @@ ### Minor -* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125) * **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) * **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme) * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) @@ -58,6 +57,7 @@ * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) +* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125) * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) From e983f8f54b85a4b7ca6543a1f68a82a98fcab47b Mon Sep 17 00:00:00 2001 From: Henrique Oliveira Date: Wed, 5 Sep 2018 17:35:22 -0300 Subject: [PATCH 0112/2611] Adding Action to view the graph by its public URL. --- pkg/services/alerting/notifiers/teams.go | 28 +++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 09bcf600533..06fc8c1c5c2 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -96,14 +96,26 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { }, }, "text": message, - "potentialAction": []map[string]interface{}{ - { - "@context": "http://schema.org", - "@type": "ViewAction", - "name": "View Rule", - "target": []string{ - ruleUrl, - }, + }, + }, + "potentialAction": []map[string]interface{}{ + { + "@context": "http://schema.org", + "@type": "OpenUri", + "name": "View Rule", + "targets": []map[string]interface{}{ + { + "os": "default", "uri": ruleUrl, + }, + }, + }, + { + "@context": "http://schema.org", + "@type": "OpenUri", + "name": "View Graph", + "targets": []map[string]interface{}{ + { + "os": "default", "uri": evalContext.ImagePublicUrl, }, }, }, From 3ce89cad71d58bb4cdd0cc63ba838706a512c313 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 6 Sep 2018 11:20:38 +0200 Subject: [PATCH 0113/2611] make default values for alerting configurable --- conf/defaults.ini | 6 ++++ conf/sample.ini | 6 ++++ docs/sources/installation/configuration.md | 8 +++++ pkg/api/frontendsettings.go | 30 ++++++++++--------- pkg/setting/setting.go | 8 +++-- public/app/core/config.ts | 2 ++ .../app/features/alerting/alert_tab_ctrl.ts | 4 +-- 7 files changed, 46 insertions(+), 18 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index fff9f630690..cf924be3f9f 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -467,6 +467,12 @@ enabled = true # Makes it possible to turn off alert rule execution but alerting UI is visible execute_alerts = true +# Default setting for new alert rules. Defaults to categories error and timeouts as alerting. (alerting, keep_state) +error_or_timeout = alerting + +# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) +nodata_or_nullvalues = no_data + #################################### Explore ############################# [explore] # Enable the Explore section diff --git a/conf/sample.ini b/conf/sample.ini index 2b2ae497e36..67072814347 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -387,6 +387,12 @@ log_queries = # Makes it possible to turn off alert rule execution but alerting UI is visible ;execute_alerts = true +# Default setting for new alert rules. Defaults to categories error and timeouts as alerting. (alerting, keep_state) +;error_or_timeout = alerting + +# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) +;nodata_or_nullvalues = no_data + #################################### Explore ############################# [explore] # Enable the Explore section diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 3394dfe16bc..1cd4d5d3893 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -1009,3 +1009,11 @@ Defaults to true. Set to false to disable alerting engine and hide Alerting from ### execute_alerts Makes it possible to turn off alert rule execution. + +### error_or_timeout + +Default setting for new alert rules. Defaults to categories error and timeouts as alerting. (alerting, keep_state) + +### nodata_or_nullvalues + +Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index da3c88566c1..a58be38781e 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -132,20 +132,22 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { } jsonObj := map[string]interface{}{ - "defaultDatasource": defaultDatasource, - "datasources": datasources, - "panels": panels, - "appSubUrl": setting.AppSubUrl, - "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, - "authProxyEnabled": setting.AuthProxyEnabled, - "ldapEnabled": setting.LdapEnabled, - "alertingEnabled": setting.AlertingEnabled, - "exploreEnabled": setting.ExploreEnabled, - "googleAnalyticsId": setting.GoogleAnalyticsId, - "disableLoginForm": setting.DisableLoginForm, - "externalUserMngInfo": setting.ExternalUserMngInfo, - "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl, - "externalUserMngLinkName": setting.ExternalUserMngLinkName, + "defaultDatasource": defaultDatasource, + "datasources": datasources, + "panels": panels, + "appSubUrl": setting.AppSubUrl, + "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, + "authProxyEnabled": setting.AuthProxyEnabled, + "ldapEnabled": setting.LdapEnabled, + "alertingEnabled": setting.AlertingEnabled, + "alertingErrorOrTimeout": setting.AlertingErrorOrTimeout, + "alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues, + "exploreEnabled": setting.ExploreEnabled, + "googleAnalyticsId": setting.GoogleAnalyticsId, + "disableLoginForm": setting.DisableLoginForm, + "externalUserMngInfo": setting.ExternalUserMngInfo, + "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl, + "externalUserMngLinkName": setting.ExternalUserMngLinkName, "buildInfo": map[string]interface{}{ "version": setting.BuildVersion, "commit": setting.BuildCommit, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 789622ca0dd..d16fd4955d5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -164,8 +164,10 @@ var ( Quota QuotaSettings // Alerting - AlertingEnabled bool - ExecuteAlerts bool + AlertingEnabled bool + ExecuteAlerts bool + AlertingErrorOrTimeout string + AlertingNoDataOrNullValues string // Explore UI ExploreEnabled bool @@ -672,6 +674,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { alerting := iniFile.Section("alerting") AlertingEnabled = alerting.Key("enabled").MustBool(true) ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) + AlertingErrorOrTimeout = alerting.Key("error_or_timeout").MustString("alerting") + AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data") explore := iniFile.Section("explore") ExploreEnabled = explore.Key("enabled").MustBool(false) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 3b8a087132c..bf5abe37d7f 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -22,6 +22,8 @@ export class Settings { disableLoginForm: boolean; defaultDatasource: string; alertingEnabled: boolean; + alertingErrorOrTimeout: string; + alertingNoDataOrNullValues: string; authProxyEnabled: boolean; exploreEnabled: boolean; ldapEnabled: boolean; diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index ef8e37483cc..53f7e57fd69 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -164,8 +164,8 @@ export class AlertTabCtrl { alert.conditions.push(this.buildDefaultCondition()); } - alert.noDataState = alert.noDataState || 'no_data'; - alert.executionErrorState = alert.executionErrorState || 'alerting'; + alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues; + alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout; alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; From 1e33a3780fbf4f29e4e9d4bc25264c387402682a Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 6 Sep 2018 11:51:24 +0200 Subject: [PATCH 0114/2611] spelling errors --- conf/defaults.ini | 2 +- conf/sample.ini | 2 +- docs/sources/installation/configuration.md | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index cf924be3f9f..85d0953c6af 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -467,7 +467,7 @@ enabled = true # Makes it possible to turn off alert rule execution but alerting UI is visible execute_alerts = true -# Default setting for new alert rules. Defaults to categories error and timeouts as alerting. (alerting, keep_state) +# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state) error_or_timeout = alerting # Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) diff --git a/conf/sample.ini b/conf/sample.ini index 67072814347..2ef254f79b9 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -387,7 +387,7 @@ log_queries = # Makes it possible to turn off alert rule execution but alerting UI is visible ;execute_alerts = true -# Default setting for new alert rules. Defaults to categories error and timeouts as alerting. (alerting, keep_state) +# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state) ;error_or_timeout = alerting # Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 1cd4d5d3893..6e3d36cc6d3 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -1011,9 +1011,11 @@ Defaults to true. Set to false to disable alerting engine and hide Alerting from Makes it possible to turn off alert rule execution. ### error_or_timeout +> Available in 5.3 and above -Default setting for new alert rules. Defaults to categories error and timeouts as alerting. (alerting, keep_state) +Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state) ### nodata_or_nullvalues +> Available in 5.3 and above Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) From a25b5945064b46a46b6d6828a84f9b1962726d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Sep 2018 12:11:56 +0200 Subject: [PATCH 0115/2611] docs: updated --- docs/sources/auth/anonymous.md | 30 -- docs/sources/auth/auth-proxy.md | 105 ++---- docs/sources/auth/generic-oauth.md | 172 +++++++++ docs/sources/auth/github.md | 98 +++++ docs/sources/auth/gitlab.md | 115 ++++++ docs/sources/auth/google.md | 55 +++ docs/sources/auth/index.md | 2 + docs/sources/auth/ldap.md | 18 +- docs/sources/auth/oauth.md | 399 --------------------- docs/sources/auth/overview.md | 81 ++++- docs/sources/installation/configuration.md | 65 +--- docs/sources/tutorials/authproxy.md | 247 ------------- 12 files changed, 547 insertions(+), 840 deletions(-) delete mode 100644 docs/sources/auth/anonymous.md create mode 100644 docs/sources/auth/generic-oauth.md create mode 100644 docs/sources/auth/github.md create mode 100644 docs/sources/auth/gitlab.md create mode 100644 docs/sources/auth/google.md delete mode 100644 docs/sources/auth/oauth.md delete mode 100644 docs/sources/tutorials/authproxy.md diff --git a/docs/sources/auth/anonymous.md b/docs/sources/auth/anonymous.md deleted file mode 100644 index 39d1059e92e..00000000000 --- a/docs/sources/auth/anonymous.md +++ /dev/null @@ -1,30 +0,0 @@ -+++ -title = "Anonymous Authentication" -description = "Anonymous authentication " -keywords = ["grafana", "configuration", "documentation", "anonymous"] -type = "docs" -[menu.docs] -name = "Anonymous" -identifier = "anonymous-auth" -parent = "authentication" -weight = 4 -+++ - -# Anonymous Authentication - -## [auth.anonymous] - -### enabled - -Set to `true` to enable anonymous access. Defaults to `false` - -### org_name - -Set the organization name that should be used for anonymous users. If -you change your organization name in the Grafana UI this setting needs -to be updated to match the new name. - -### org_role - -Specify role for anonymous users. Defaults to `Viewer`, other valid -options are `Editor` and `Admin`. diff --git a/docs/sources/auth/auth-proxy.md b/docs/sources/auth/auth-proxy.md index 8ff61a8c40b..e066eed9190 100644 --- a/docs/sources/auth/auth-proxy.md +++ b/docs/sources/auth/auth-proxy.md @@ -3,6 +3,7 @@ title = "Auth Proxy" description = "Grafana Auth Proxy Guide " keywords = ["grafana", "configuration", "documentation", "proxy"] type = "docs" +aliases = ["/tutorials/authproxy/"] [menu.docs] name = "Auth Proxy" identifier = "auth-proxy" @@ -12,66 +13,31 @@ weight = 2 # Auth Proxy Authentication -## [auth.proxy] +You can configure Grafana to let a http reverse proxy handling authentication. Popular web servers have a very +extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature. +Below we detail the configuration options for auth proxy. -This feature allows you to handle authentication in a http reverse proxy. - -### enabled - -Defaults to `false` - -### header_name - -Defaults to X-WEBAUTH-USER - -#### header_property - -Defaults to username but can also be set to email - -### auto_sign_up - -Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`. - -### whitelist - -Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. - -### headers - -Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. - -
    - -# Grafana Authproxy - -AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet). - -Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature. - -The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful. - -## Interacting with Grafana’s AuthProxy via curl - -The AuthProxy feature can be configured through the Grafana configuration file with the following options: - -```js +```bash [auth.proxy] +# Defaults to false, but set to true to enable this feature enabled = true +# HTTP Header name that will contain the username or email header_name = X-WEBAUTH-USER +# HTTP Header property, defaults to `username` but can also be `email` header_property = username +# Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`. auto_sign_up = true +# If combined with Grafana LDAP integration define sync interval ldap_sync_ttl = 60 +# Limit where auth proxy requests come from by configuring a list of IP addresses. +# This can be used to prevent users spoofing the X-WEBAUTH-USER header. whitelist = +# Optionally define more headers to sync other user attributes +# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`` +headers = ``` -* **enabled**: this is to toggle the feature on or off -* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in. -* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email) -* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid. -* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes. -* **whitelist**: Comma separated list of trusted authentication proxies IP. - -With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header. +## Interacting with Grafana’s AuthProxy via curl ```bash curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users @@ -106,7 +72,8 @@ I’ll demonstrate how to use Apache for authenticating users. In this example w ### Apache BasicAuth -In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service. +In this example we use Apache as a reverse proxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service. + #### Apache configuration @@ -151,38 +118,7 @@ In this example we use Apache as a reverseProxy in front of Grafana. Apache hand * The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000. -#### Grafana configuration - -```bash -############# Users ################ -[users] - # disable user signup / registration -allow_sign_up = false - -# Set to true to automatically assign new users to the default organization (id 1) -auto_assign_org = true - -# Default role new users will be automatically assigned (if auto_assign_org above is set to true) - auto_assign_org_role = Editor - - -############ Auth Proxy ######## -[auth.proxy] -enabled = true - -# the Header name that contains the authenticated user. -header_name = X-WEBAUTH-USER - -# does the user authenticate against the proxy using a 'username' or an 'email' -header_property = username - -# automatically add the user to the system if they don't already exist. -auto_sign_up = true -``` - -#### Full walk through using Docker. - -##### Grafana Container +## Full walk through using Docker. For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/) @@ -201,7 +137,8 @@ header_property = username auto_sign_up = true ``` -* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container. +Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose +any ports for this container as it will only be connected to by our Apache container. ```bash docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md new file mode 100644 index 00000000000..70c1b937427 --- /dev/null +++ b/docs/sources/auth/generic-oauth.md @@ -0,0 +1,172 @@ ++++ +title = "OAuth authentication" +description = "Grafana OAuthentication Guide " +keywords = ["grafana", "configuration", "documentation", "oauth"] +type = "docs" +[menu.docs] +name = "Generic OAuth2" +identifier = "generic_oauth" +parent = "authentication" +weight = 3 ++++ + +# Generic OAuth Authentication + +You can configure many different oauth2 authentication services with Grafana using the generic oauth2 feature. Below you +can find examples using Okta, BitBucket, OneLogin and Azure. + +This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/generic_oauth`. + +Example config: + +```bash +[auth.generic_oauth] +enabled = true +client_id = YOUR_APP_CLIENT_ID +client_secret = YOUR_APP_CLIENT_SECRET +scopes = +auth_url = +token_url = +api_url = +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` + +Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. + +## Set up OAuth2 with Okta + +First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. + +Finally set up the generic oauth module like this: +```bash +[auth.generic_oauth] +name = Okta +enabled = true +scopes = openid profile email +client_id = +client_secret = +auth_url = https:///oauth2/v1/authorize +token_url = https:///oauth2/v1/token +api_url = https:///oauth2/v1/userinfo +``` + +## Set up OAuth2 with Bitbucket + +```bash +[auth.generic_oauth] +name = BitBucket +enabled = true +allow_sign_up = true +client_id = +client_secret = +scopes = account email +auth_url = https://bitbucket.org/site/oauth2/authorize +token_url = https://bitbucket.org/site/oauth2/access_token +api_url = https://api.bitbucket.org/2.0/user +team_ids = +allowed_organizations = +``` + +## Set up OAuth2 with OneLogin + +1. Create a new Custom Connector with the following settings: + - Name: Grafana + - Sign On Method: OpenID Connect + - Redirect URI: `https:///login/generic_oauth` + - Signing Algorithm: RS256 + - Login URL: `https:///login/generic_oauth` + + then: +2. Add an App to the Grafana Connector: + - Display Name: Grafana + + then: +3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. + + Your OneLogin Domain will match the url you use to access OneLogin. + + Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = OneLogin + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://.onelogin.com/oidc/auth + token_url = https://.onelogin.com/oidc/token + api_url = https://.onelogin.com/oidc/me + team_ids = + allowed_organizations = + ``` + +### Set up OAuth2 with Auth0 + +1. Create a new Client in Auth0 + - Name: Grafana + - Type: Regular Web Application + +2. Go to the Settings tab and set: + - Allowed Callback URLs: `https:///login/generic_oauth` + +3. Click Save Changes, then use the values at the top of the page to configure Grafana: + + ```bash + [auth.generic_oauth] + enabled = true + allow_sign_up = true + team_ids = + allowed_organizations = + name = Auth0 + client_id = + client_secret = + scopes = openid profile email + auth_url = https:///authorize + token_url = https:///oauth/token + api_url = https:///userinfo + ``` + +### Set up OAuth2 with Azure Active Directory + +1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item. + +2. Copy the "Directory ID", this is needed for setting URLs later + +3. Click "App Registrations" and add a new application registration: + - Name: Grafana + - Application type: Web app / API + - Sign-on URL: `https:///login/generic_oauth` + +4. Click the name of the new application to open the application details page. + +5. Note down the "Application ID", this will be the OAuth client id. + +6. Click "Settings", then click "Keys" and add a new entry under Passwords + - Key Description: Grafana OAuth + - Duration: Never Expires + +7. Click Save then copy the key value, this will be the OAuth client secret. + +8. Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = Azure AD + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://login.microsoftonline.com//oauth2/authorize + token_url = https://login.microsoftonline.com//oauth2/token + api_url = + team_ids = + allowed_organizations = + ``` + +
    + + diff --git a/docs/sources/auth/github.md b/docs/sources/auth/github.md new file mode 100644 index 00000000000..0e14798d45e --- /dev/null +++ b/docs/sources/auth/github.md @@ -0,0 +1,98 @@ ++++ +title = "Google OAuth2 Authentication" +description = "Grafana OAuthentication Guide " +keywords = ["grafana", "configuration", "documentation", "oauth"] +type = "docs" +[menu.docs] +name = "GitHub OAuth2" +identifier = "github_oauth2" +parent = "authentication" +weight = 4 ++++ + +# GitHub OAuth2 Authentication + +To enable the GitHub OAuth2 you must register your application with GitHub. GitHub will generate a client ID and secret key for you to use. + +## Configure GitHub OAuth application + +You need to create a GitHub OAuth application (you find this under the GitHub +settings page). When you create the application you will need to specify +a callback URL. Specify this as callback: + +```bash +http://:/login/github +``` + +This callback URL must match the full HTTP address that you use in your +browser to access Grafana, but with the prefix path of `/login/github`. +When the GitHub OAuth application is created you will get a Client ID and a +Client Secret. Specify these in the Grafana configuration file. For +example: + +## Enable GitHub in Grafana + +```bash +[auth.github] +enabled = true +allow_sign_up = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +team_ids = +allowed_organizations = +``` + +Restart the Grafana back-end. You should now see a GitHub login button +on the login page. You can now login or sign up with your GitHub +accounts. + +You may allow users to sign-up via GitHub authentication by setting the +`allow_sign_up` option to `true`. When this option is set to `true`, any +user successfully authenticating via GitHub authentication will be +automatically signed up. + +### team_ids + +Require an active team membership for at least one of the given teams on +GitHub. If the authenticated user isn't a member of at least one of the +teams they will not be able to register or authenticate with your +Grafana instance. For example: + +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +team_ids = 150,300 +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +allow_sign_up = true +``` + +### allowed_organizations + +Require an active organization membership for at least one of the given +organizations on GitHub. If the authenticated user isn't a member of at least +one of the organizations they will not be able to register or authenticate with +your Grafana instance. For example + +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +allow_sign_up = true +# space-delimited organization names +allowed_organizations = github google +``` + diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md new file mode 100644 index 00000000000..6d587353aae --- /dev/null +++ b/docs/sources/auth/gitlab.md @@ -0,0 +1,115 @@ ++++ +title = "Google OAuth2 Authentication" +description = "Grafana OAuthentication Guide " +keywords = ["grafana", "configuration", "documentation", "oauth"] +type = "docs" +[menu.docs] +name = "GitLab OAuth2" +identifier = "gitlab_oauth" +parent = "authentication" +weight = 5 ++++ + +# GitLab OAuth2 Authentication + +To enable the GitLab OAuth2 you must register an application in GitLab. GitLab will generate a client ID and secret key for you to use. + +## Create GitLab OAuth keys + +You need to [create a GitLab OAuth application](https://docs.gitlab.com/ce/integration/oauth_provider.html). +Choose a descriptive *Name*, and use the following *Redirect URI*: + +``` +https://grafana.example.com/login/gitlab +``` + +where `https://grafana.example.com` is the URL you use to connect to Grafana. +Adjust it as needed if you don't use HTTPS or if you use a different port; for +instance, if you access Grafana at `http://203.0.113.31:3000`, you should use + +``` +http://203.0.113.31:3000/login/gitlab +``` + +Finally, select *api* as the *Scope* and submit the form. Note that if you're +not going to use GitLab groups for authorization (i.e. not setting +`allowed_groups`, see below), you can select *read_user* instead of *api* as +the *Scope*, thus giving a more restricted access to your GitLab API. + +You'll get an *Application Id* and a *Secret* in return; we'll call them +`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this +section. + +## Enable GitLab in Grafana + +Add the following to your Grafana configuration file to enable GitLab +authentication: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = false +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = +``` + +Restart the Grafana backend for your changes to take effect. + +If you use your own instance of GitLab instead of `gitlab.com`, adjust +`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` +hostname with your own. + +With `allow_sign_up` set to `false`, only existing users will be able to login +using their GitLab account, but with `allow_sign_up` set to `true`, *any* user +who can authenticate on GitLab will be able to login on your Grafana instance; +if you use the public `gitlab.com`, it means anyone in the world would be able +to login on your Grafana instance. + +You can can however limit access to only members of a given group or list of +groups by setting the `allowed_groups` option. + +### allowed_groups + +To limit access to authenticated users that are members of one or more [GitLab +groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` +to a comma- or space-separated list of groups. For instance, if you want to +only give access to members of the `example` group, set + + +```ini +allowed_groups = example +``` + +If you want to also give access to members of the subgroup `bar`, which is in +the group `foo`, set + +```ini +allowed_groups = example, foo/bar +``` + +Note that in GitLab, the group or subgroup name doesn't always match its +display name, especially if the display name contains spaces or special +characters. Make sure you always use the group or subgroup name as it appears +in the URL of the group or subgroup. + +Here's a complete example with `alloed_sign_up` enabled, and access limited to +the `example` and `foo/bar` groups: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = example, foo/bar +``` + diff --git a/docs/sources/auth/google.md b/docs/sources/auth/google.md new file mode 100644 index 00000000000..c11983829f1 --- /dev/null +++ b/docs/sources/auth/google.md @@ -0,0 +1,55 @@ ++++ +title = "Google OAuth2 Authentication" +description = "Grafana OAuthentication Guide " +keywords = ["grafana", "configuration", "documentation", "oauth"] +type = "docs" +[menu.docs] +name = "Google OAuth2" +identifier = "ggogle_oauth2" +parent = "authentication" +weight = 3 ++++ + +# Google OAuth2 Authentication + +To enable the Google OAuth2 you must register your application with Google. Google will generate a client ID and secret key for you to use. + +## Create Google OAuth keys + +First, you need to create a Google OAuth Client: + +1. Go to https://console.developers.google.com/apis/credentials +2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the menu that drops down +3. Enter the following: + - Application Type: Web Application + - Name: Grafana + - Authorized Javascript Origins: https://grafana.mycompany.com + - Authorized Redirect URLs: https://grafana.mycompany.com/login/google + - Replace https://grafana.mycompany.com with the URL of your Grafana instance. +4. Click Create +5. Copy the Client ID and Client Secret from the 'OAuth Client' modal + +## Enable Google OAuth in Grafana + +Specify the Client ID and Secret in the [Grafana configuration file]({{< relref "installation/configuration.md/#config-file-locations" >}}). For example: + +```bash +[auth.google] +enabled = true +client_id = CLIENT_ID +client_secret = CLIENT_SECRET +scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +auth_url = https://accounts.google.com/o/oauth2/auth +token_url = https://accounts.google.com/o/oauth2/token +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` + +Restart the Grafana back-end. You should now see a Google login button +on the login page. You can now login or sign up with your Google +accounts. The `allowed_domains` option is optional, and domains were separated by space. + +You may allow users to sign-up via Google authentication by setting the +`allow_sign_up` option to `true`. When this option is set to `true`, any +user successfully authenticating via Google authentication will be +automatically signed up. diff --git a/docs/sources/auth/index.md b/docs/sources/auth/index.md index 455c361369a..7fdcc082319 100644 --- a/docs/sources/auth/index.md +++ b/docs/sources/auth/index.md @@ -8,3 +8,5 @@ identifier = "authentication" parent = "admin" weight = 3 +++ + + diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index f9208213aef..6e0cf5606b4 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -4,13 +4,20 @@ description = "Grafana LDAP Authentication Guide " keywords = ["grafana", "configuration", "documentation", "ldap"] type = "docs" [menu.docs] -name = "LDAP Auth" +name = "LDAP" identifier = "ldap" parent = "authentication" weight = 2 +++ +# LDAP + +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. + ## [auth.ldap] + ### enabled Set to `true` to enable LDAP integration (default: `false`) @@ -22,16 +29,9 @@ Path to the LDAP specific configuration file (default: `/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). -> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page. -
    -# LDAP Authentication - -Grafana (2.1 and newer) ships with a strong LDAP integration feature. 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. - +Grafana (2.1 and newer) ships with a strong LDAP integration feature. ## Configuration You turn on LDAP in the [main config file]({{< relref "configuration.md#auth-ldap" >}}) as well as specify the path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`). diff --git a/docs/sources/auth/oauth.md b/docs/sources/auth/oauth.md deleted file mode 100644 index 0fe60196ffa..00000000000 --- a/docs/sources/auth/oauth.md +++ /dev/null @@ -1,399 +0,0 @@ -+++ -title = "OAuth authentication" -description = "Grafana OAuthentication Guide " -keywords = ["grafana", "configuration", "documentation", "oauth"] -type = "docs" -[menu.docs] -name = "OAuth" -identifier = "oauth" -parent = "authentication" -weight = 2 -+++ - -# OAuth Authentication - -## [auth.generic_oauth] - -This option could be used if have your own oauth service. - -This callback URL must match the full HTTP address that you use in your -browser to access Grafana, but with the prefix path of `/login/generic_oauth`. - -```bash -[auth.generic_oauth] -enabled = true -client_id = YOUR_APP_CLIENT_ID -client_secret = YOUR_APP_CLIENT_SECRET -scopes = -auth_url = -token_url = -api_url = -allowed_domains = mycompany.com mycompany.org -allow_sign_up = true -``` - -Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. - -### Set up oauth2 with Okta - -First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. - -Finally set up the generic oauth module like this: -```bash -[auth.generic_oauth] -name = Okta -enabled = true -scopes = openid profile email -client_id = -client_secret = -auth_url = https:///oauth2/v1/authorize -token_url = https:///oauth2/v1/token -api_url = https:///oauth2/v1/userinfo -``` - -### Set up oauth2 with Bitbucket - -```bash -[auth.generic_oauth] -name = BitBucket -enabled = true -allow_sign_up = true -client_id = -client_secret = -scopes = account email -auth_url = https://bitbucket.org/site/oauth2/authorize -token_url = https://bitbucket.org/site/oauth2/access_token -api_url = https://api.bitbucket.org/2.0/user -team_ids = -allowed_organizations = -``` - -### Set up oauth2 with OneLogin - -1. Create a new Custom Connector with the following settings: - - Name: Grafana - - Sign On Method: OpenID Connect - - Redirect URI: `https:///login/generic_oauth` - - Signing Algorithm: RS256 - - Login URL: `https:///login/generic_oauth` - - then: -2. Add an App to the Grafana Connector: - - Display Name: Grafana - - then: -3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. - - Your OneLogin Domain will match the url you use to access OneLogin. - - Configure Grafana as follows: - - ```bash - [auth.generic_oauth] - name = OneLogin - enabled = true - allow_sign_up = true - client_id = - client_secret = - scopes = openid email name - auth_url = https://.onelogin.com/oidc/auth - token_url = https://.onelogin.com/oidc/token - api_url = https://.onelogin.com/oidc/me - team_ids = - allowed_organizations = - ``` - -### Set up oauth2 with Auth0 - -1. Create a new Client in Auth0 - - Name: Grafana - - Type: Regular Web Application - -2. Go to the Settings tab and set: - - Allowed Callback URLs: `https:///login/generic_oauth` - -3. Click Save Changes, then use the values at the top of the page to configure Grafana: - - ```bash - [auth.generic_oauth] - enabled = true - allow_sign_up = true - team_ids = - allowed_organizations = - name = Auth0 - client_id = - client_secret = - scopes = openid profile email - auth_url = https:///authorize - token_url = https:///oauth/token - api_url = https:///userinfo - ``` - -### Set up oauth2 with Azure Active Directory - -1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item. - -2. Copy the "Directory ID", this is needed for setting URLs later - -3. Click "App Registrations" and add a new application registration: - - Name: Grafana - - Application type: Web app / API - - Sign-on URL: `https:///login/generic_oauth` - -4. Click the name of the new application to open the application details page. - -5. Note down the "Application ID", this will be the OAuth client id. - -6. Click "Settings", then click "Keys" and add a new entry under Passwords - - Key Description: Grafana OAuth - - Duration: Never Expires - -7. Click Save then copy the key value, this will be the OAuth client secret. - -8. Configure Grafana as follows: - - ```bash - [auth.generic_oauth] - name = Azure AD - enabled = true - allow_sign_up = true - client_id = - client_secret = - scopes = openid email name - auth_url = https://login.microsoftonline.com//oauth2/authorize - token_url = https://login.microsoftonline.com//oauth2/token - api_url = - team_ids = - allowed_organizations = - ``` - -
    - -## [auth.github] - -You need to create a GitHub OAuth application (you find this under the GitHub -settings page). When you create the application you will need to specify -a callback URL. Specify this as callback: - -```bash -http://:/login/github -``` - -This callback URL must match the full HTTP address that you use in your -browser to access Grafana, but with the prefix path of `/login/github`. -When the GitHub OAuth application is created you will get a Client ID and a -Client Secret. Specify these in the Grafana configuration file. For -example: - -```bash -[auth.github] -enabled = true -allow_sign_up = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -team_ids = -allowed_organizations = -``` - -Restart the Grafana back-end. You should now see a GitHub login button -on the login page. You can now login or sign up with your GitHub -accounts. - -You may allow users to sign-up via GitHub authentication by setting the -`allow_sign_up` option to `true`. When this option is set to `true`, any -user successfully authenticating via GitHub authentication will be -automatically signed up. - -### team_ids - -Require an active team membership for at least one of the given teams on -GitHub. If the authenticated user isn't a member of at least one of the -teams they will not be able to register or authenticate with your -Grafana instance. For example: - -```bash -[auth.github] -enabled = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -team_ids = 150,300 -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -allow_sign_up = true -``` - -### allowed_organizations - -Require an active organization membership for at least one of the given -organizations on GitHub. If the authenticated user isn't a member of at least -one of the organizations they will not be able to register or authenticate with -your Grafana instance. For example - -```bash -[auth.github] -enabled = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -allow_sign_up = true -# space-delimited organization names -allowed_organizations = github google -``` - -
    - -## [auth.gitlab] - -> Only available in Grafana v5.3+. - -You need to [create a GitLab OAuth -application](https://docs.gitlab.com/ce/integration/oauth_provider.html). -Choose a descriptive *Name*, and use the following *Redirect URI*: - -``` -https://grafana.example.com/login/gitlab -``` - -where `https://grafana.example.com` is the URL you use to connect to Grafana. -Adjust it as needed if you don't use HTTPS or if you use a different port; for -instance, if you access Grafana at `http://203.0.113.31:3000`, you should use - -``` -http://203.0.113.31:3000/login/gitlab -``` - -Finally, select *api* as the *Scope* and submit the form. Note that if you're -not going to use GitLab groups for authorization (i.e. not setting -`allowed_groups`, see below), you can select *read_user* instead of *api* as -the *Scope*, thus giving a more restricted access to your GitLab API. - -You'll get an *Application Id* and a *Secret* in return; we'll call them -`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this -section. - -Add the following to your Grafana configuration file to enable GitLab -authentication: - -```ini -[auth.gitlab] -enabled = false -allow_sign_up = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = api -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -api_url = https://gitlab.com/api/v4 -allowed_groups = -``` - -Restart the Grafana backend for your changes to take effect. - -If you use your own instance of GitLab instead of `gitlab.com`, adjust -`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` -hostname with your own. - -With `allow_sign_up` set to `false`, only existing users will be able to login -using their GitLab account, but with `allow_sign_up` set to `true`, *any* user -who can authenticate on GitLab will be able to login on your Grafana instance; -if you use the public `gitlab.com`, it means anyone in the world would be able -to login on your Grafana instance. - -You can can however limit access to only members of a given group or list of -groups by setting the `allowed_groups` option. - -### allowed_groups - -To limit access to authenticated users that are members of one or more [GitLab -groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` -to a comma- or space-separated list of groups. For instance, if you want to -only give access to members of the `example` group, set - - -```ini -allowed_groups = example -``` - -If you want to also give access to members of the subgroup `bar`, which is in -the group `foo`, set - -```ini -allowed_groups = example, foo/bar -``` - -Note that in GitLab, the group or subgroup name doesn't always match its -display name, especially if the display name contains spaces or special -characters. Make sure you always use the group or subgroup name as it appears -in the URL of the group or subgroup. - -Here's a complete example with `alloed_sign_up` enabled, and access limited to -the `example` and `foo/bar` groups: - -```ini -[auth.gitlab] -enabled = false -allow_sign_up = true -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = api -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -api_url = https://gitlab.com/api/v4 -allowed_groups = example, foo/bar -``` - -
    - -## [auth.google] - -First, you need to create a Google OAuth Client: - -1. Go to https://console.developers.google.com/apis/credentials - -2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the -menu that drops down - -3. Enter the following: - - - Application Type: Web Application - - Name: Grafana - - Authorized Javascript Origins: https://grafana.mycompany.com - - Authorized Redirect URLs: https://grafana.mycompany.com/login/google - - Replace https://grafana.mycompany.com with the URL of your Grafana instance. - -4. Click Create - -5. Copy the Client ID and Client Secret from the 'OAuth Client' modal - -Specify the Client ID and Secret in the Grafana configuration file. For example: - -```bash -[auth.google] -enabled = true -client_id = CLIENT_ID -client_secret = CLIENT_SECRET -scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email -auth_url = https://accounts.google.com/o/oauth2/auth -token_url = https://accounts.google.com/o/oauth2/token -allowed_domains = mycompany.com mycompany.org -allow_sign_up = true -``` - -Restart the Grafana back-end. You should now see a Google login button -on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains were separated by space. - -You may allow users to sign-up via Google authentication by setting the -`allow_sign_up` option to `true`. When this option is set to `true`, any -user successfully authenticating via Google authentication will be -automatically signed up. \ No newline at end of file diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index 03a7a0e9fe4..fc01a713ca8 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -9,30 +9,79 @@ parent = "authentication" weight = 1 +++ -# Authentication +# User Authentication Overview -Grafana provides many ways to authenticate users. By default it will use local users & passwords stored in the Grafana -database. +Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user +permissions and org memberships. -## Settings +## OAuth2 Integrations -Via the [server ini config file]({{< relref "installation/debian.md" >}}) you can setup many different authentication methods. Auth settings -are documented below. +- [Google OAuth]({{< relref "auth/google.md" >}}) +- [GitHub OAuth]({{< relref "auth/github.md" >}}) +- [Gitlab OAuth]({{< relref "auth/gitlab.md" >}}) +- [Generic OAuth]({{< relref "auth/oauth.md" >}}) (Okta2, BitBucket, Azure, OneLogin, Auth0) -### [auth] +## LDAP integrations -#### disable_login_form +- [LDAP Authentication]({{< relref "auth/ldap.md" >}}) (OpenLDAP, ActiveDirectory, etc) -Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false. +## Auth proxy -#### disable_signout_menu +- [Auth Proxy]({{< relref "auth/auth-proxy.md" >}}) If you want to handle authentication outside Grafana using a reverse + proxy. -Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false. +## Grafana Auth -
    +Grafana of course has a built in user authentication system with password authenticaten enabled by default. You can +disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth +provider (listed above). There is also options for allowing self sign up. -### [auth.basic] -#### enabled -When enabled is `true` (default) the http api will accept basic authentication. +### Anonymous authenticaten + +You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. + +Example: + +```bash +[auth.anonymous] +enabled = true + +# Organization name that should be used for unauthenticated users +org_name = Main Org. + +# Role for unauthenticated users, other valid values are `Editor` and `Admin` +org_role = Viewer +``` + +If you change your organization name in the Grafana UI this setting needs to be updated to match the new name. + +### Basic authentication + +Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP +authenticaten integration. + +To disable basic auth: + +```bash +[auth.basic] +enabled = false +``` + +### Disable login form + +You can hide the Grafana login form using the below configuration settings. + +```bash +[auth] +disable_login_form ⁼ true +``` + +### Hide sign-out menu + +Set to the option detailed below to true to hide sign-out menu link. Useful if you use an auth proxy. + +```bash +[auth] +disable_signout_menu = true +``` -
    diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index f61274e36fa..5aee2c5f594 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -321,62 +321,17 @@ Defaults to `false`. ## [auth] -### disable_login_form +Grafana provides many ways to authenticate users. The docs for authentication has been split in to many differnet pages +below. -Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false. - -### disable_signout_menu - -Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false. - -
    - -## [auth.anonymous] - -[Read guide here.](/administration/authentication/anonymous-auth) - -
    - -## [auth.github] - -[Read guide here.](/administration/authentication/oauth/#auth-github) - -
    - -## [auth.gitlab] - -[Read guide here.](/administration/authentication/oauth/#auth-gitlab) - -
    - -## [auth.google] - -[Read guide here.](/administration/authentication/oauth/#auth-google) - -
    - -## [auth.generic_oauth] - -[Read guide here.](/administration/authentication/oauth/#auth-generic-oauth) -
    - -## [auth.basic] -### enabled -When enabled is `true` (default) the http api will accept basic authentication. - -
    - -## [auth.ldap] - -[Read guide here.](/administration/authentication/ldap/) - -
    - -## [auth.proxy] - -[Read guide here.](/administration/authentication/auth-proxy/) - -
    +- [Anonymous access]({{< relref "auth/overview.md" >}}) (auth.anonymous) +- [Google OAuth]({{< relref "auth/google.md" >}}) (auth.google) +- [GitHub OAuth]({{< relref "auth/github.md" >}}) (auth.github) +- [Gitlab OAuth]({{< relref "auth/gitlab.md" >}}) (auth.gitlab) +- [Generic OAuth]({{< relref "auth/generic-oauth.md" >}}) (auth.generic_oauth, okta2, auth0, bitbucket, azure) +- [Basic Authentication]({{< relref "auth/overview.md" >}}) (auth.basic) +- [LDAP Authentication]({{< relref "auth/ldap.md" >}}) (auth.ldap) +- [Auth Proxy]({{< relref "auth/auth-proxy.md" >}}) (auth.proxy) ## [session] diff --git a/docs/sources/tutorials/authproxy.md b/docs/sources/tutorials/authproxy.md deleted file mode 100644 index 6f13de85c18..00000000000 --- a/docs/sources/tutorials/authproxy.md +++ /dev/null @@ -1,247 +0,0 @@ -+++ -title = "Grafana Authproxy" -type = "docs" -keywords = ["grafana", "tutorials", "authproxy"] -[menu.docs] -parent = "tutorials" -weight = 10 -+++ - -# Grafana Authproxy - -AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet). - -Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature. - -The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful. - -## Interacting with Grafana’s AuthProxy via curl - -The AuthProxy feature can be configured through the Grafana configuration file with the following options: - -```js -[auth.proxy] -enabled = true -header_name = X-WEBAUTH-USER -header_property = username -auto_sign_up = true -ldap_sync_ttl = 60 -whitelist = -``` - -* **enabled**: this is to toggle the feature on or off -* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in. -* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email) -* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid. -* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes. -* **whitelist**: Comma separated list of trusted authentication proxies IP. - -With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header. - -```bash -curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users -[ - { - "id":1, - "name":"", - "login":"admin", - "email":"admin@localhost", - "isAdmin":true - } -] -``` - -We can then send a second request to the `/api/user` method which will return the details of the logged in user. We will use this request to show how Grafana automatically adds the new user we specify to the system. Here we create a new user called “anthony”. - -```bash -curl -H "X-WEBAUTH-USER: anthony" http://localhost:3000/api/user -{ - "email":"anthony", - "name":"", - "login":"anthony", - "theme":"", - "orgId":1, - "isGrafanaAdmin":false -} -``` - -## Making Apache’s auth work together with Grafana’s AuthProxy - -I’ll demonstrate how to use Apache for authenticating users. In this example we use BasicAuth with Apache’s text file based authentication handler, i.e. htpasswd files. However, any available Apache authentication capabilities could be used. - -### Apache BasicAuth - -In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service. - -#### Apache configuration - -```bash - - ServerAdmin webmaster@authproxy - ServerName authproxy - ErrorLog "logs/authproxy-error_log" - CustomLog "logs/authproxy-access_log" common - - - AuthType Basic - AuthName GrafanaAuthProxy - AuthBasicProvider file - AuthUserFile /etc/apache2/grafana_htpasswd - Require valid-user - - RewriteEngine On - RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] - RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" - - - RequestHeader unset Authorization - - ProxyRequests Off - ProxyPass / http://localhost:3000/ - ProxyPassReverse / http://localhost:3000/ - -``` - -* The first 4 lines of the virtualhost configuration are standard, so we won’t go into detail on what they do. - -* We use a **\** configuration block for applying our authentication rules to every proxied request. These rules include requiring basic authentication where user:password credentials are stored in the **/etc/apache2/grafana_htpasswd** file. This file can be created with the `htpasswd` command. - - * The next part of the configuration is the tricky part. We use Apache’s rewrite engine to create our **X-WEBAUTH-USER header**, populated with the authenticated user. - - * **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is necessary as the REMOTE_USER variable is not available to the RequestHeader function. - - * **RequestHeader set X-WEBAUTH-USER “%{PROXY_USER}e”**: With the authenticated username now stored in the PROXY_USER variable, we create a new HTTP request header that will be sent to our backend Grafana containing the username. - -* The **RequestHeader unset Authorization** removes the Authorization header from the HTTP request before it is forwarded to Grafana. This ensures that Grafana does not try to authenticate the user using these credentials (BasicAuth is a supported authentication handler in Grafana). - -* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000. - -#### Grafana configuration - -```bash -############# Users ################ -[users] - # disable user signup / registration -allow_sign_up = false - -# Set to true to automatically assign new users to the default organization (id 1) -auto_assign_org = true - -# Default role new users will be automatically assigned (if auto_assign_org above is set to true) - auto_assign_org_role = Editor - - -############ Auth Proxy ######## -[auth.proxy] -enabled = true - -# the Header name that contains the authenticated user. -header_name = X-WEBAUTH-USER - -# does the user authenticate against the proxy using a 'username' or an 'email' -header_property = username - -# automatically add the user to the system if they don't already exist. -auto_sign_up = true -``` - -#### Full walk through using Docker. - -##### Grafana Container - -For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/) - -* Create a file `grafana.ini` with the following contents - -```bash -[users] -allow_sign_up = false -auto_assign_org = true -auto_assign_org_role = Editor - -[auth.proxy] -enabled = true -header_name = X-WEBAUTH-USER -header_property = username -auto_sign_up = true -``` - -* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container. - -```bash -docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana -``` - -### Apache Container - -For this example we use the official Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/) - -* Create a file `httpd.conf` with the following contents - -```bash -ServerRoot "/usr/local/apache2" -Listen 80 -LoadModule authn_file_module modules/mod_authn_file.so -LoadModule authn_core_module modules/mod_authn_core.so -LoadModule authz_host_module modules/mod_authz_host.so -LoadModule authz_user_module modules/mod_authz_user.so -LoadModule authz_core_module modules/mod_authz_core.so -LoadModule auth_basic_module modules/mod_auth_basic.so -LoadModule log_config_module modules/mod_log_config.so -LoadModule env_module modules/mod_env.so -LoadModule headers_module modules/mod_headers.so -LoadModule unixd_module modules/mod_unixd.so -LoadModule rewrite_module modules/mod_rewrite.so -LoadModule proxy_module modules/mod_proxy.so -LoadModule proxy_http_module modules/mod_proxy_http.so - -User daemon -Group daemon - -ServerAdmin you@example.com - - AllowOverride none - Require all denied - -DocumentRoot "/usr/local/apache2/htdocs" -ErrorLog /proc/self/fd/2 -LogLevel error - - LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined - LogFormat "%h %l %u %t \"%r\" %>s %b" common - - LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio - - CustomLog /proc/self/fd/1 common - - - AuthType Basic - AuthName GrafanaAuthProxy - AuthBasicProvider file - AuthUserFile /tmp/htpasswd - Require valid-user - RewriteEngine On - RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] - RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" - -RequestHeader unset Authorization -ProxyRequests Off -ProxyPass / http://grafana:3000/ -ProxyPassReverse / http://grafana:3000/ -``` - -* Create a htpasswd file. We create a new user **anthony** with the password **password** - - ```bash - htpasswd -bc htpasswd anthony password - ``` - -* Launch the httpd container using our custom httpd.conf and our htpasswd file. The container will listen on port 80, and we create a link to the **grafana** container so that this container can resolve the hostname **grafana** to the grafana container’s ip address. - - ```bash - docker run -i -p 80:80 --link grafana:grafana -v $(pwd)/httpd.conf:/usr/local/apache2/conf/httpd.conf -v $(pwd)/htpasswd:/tmp/htpasswd httpd:2.4 - ``` - -### Use grafana. - -With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file. From d6f9ebab63478e347997f6d51559c7a17521566e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Sep 2018 13:15:36 +0200 Subject: [PATCH 0116/2611] docs: Updated auth docs --- docs/sources/auth/github.md | 2 +- docs/sources/auth/gitlab.md | 4 +-- docs/sources/auth/google.md | 2 +- docs/sources/auth/ldap.md | 36 ++++++++++------------ docs/sources/auth/overview.md | 2 +- docs/sources/installation/configuration.md | 2 +- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/docs/sources/auth/github.md b/docs/sources/auth/github.md index 0e14798d45e..263b3cc5d4d 100644 --- a/docs/sources/auth/github.md +++ b/docs/sources/auth/github.md @@ -4,7 +4,7 @@ description = "Grafana OAuthentication Guide " keywords = ["grafana", "configuration", "documentation", "oauth"] type = "docs" [menu.docs] -name = "GitHub OAuth2" +name = "GitHub" identifier = "github_oauth2" parent = "authentication" weight = 4 diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md index 6d587353aae..32910167f16 100644 --- a/docs/sources/auth/gitlab.md +++ b/docs/sources/auth/gitlab.md @@ -4,7 +4,7 @@ description = "Grafana OAuthentication Guide " keywords = ["grafana", "configuration", "documentation", "oauth"] type = "docs" [menu.docs] -name = "GitLab OAuth2" +name = "GitLab" identifier = "gitlab_oauth" parent = "authentication" weight = 5 @@ -45,7 +45,7 @@ section. Add the following to your Grafana configuration file to enable GitLab authentication: -```ini +```bash [auth.gitlab] enabled = false allow_sign_up = false diff --git a/docs/sources/auth/google.md b/docs/sources/auth/google.md index c11983829f1..2a79037c430 100644 --- a/docs/sources/auth/google.md +++ b/docs/sources/auth/google.md @@ -4,7 +4,7 @@ description = "Grafana OAuthentication Guide " keywords = ["grafana", "configuration", "documentation", "oauth"] type = "docs" [menu.docs] -name = "Google OAuth2" +name = "Google" identifier = "ggogle_oauth2" parent = "authentication" weight = 3 diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index 6e0cf5606b4..f63a44e1750 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -16,29 +16,25 @@ The LDAP integration in Grafana allows your Grafana users to login with their LD group memberships and Grafana Organization user roles. Below we detail grafana.ini config file settings and ldap.toml config file options. -## [auth.ldap] +## Enable LDAP -### enabled -Set to `true` to enable LDAP integration (default: `false`) - -### config_file -Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) - -### allow_sign_up - -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). - -
    - -Grafana (2.1 and newer) ships with a strong LDAP integration feature. -## Configuration -You turn on LDAP in the [main config file]({{< relref "configuration.md#auth-ldap" >}}) as well as specify the path to the LDAP +You turn on 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`). -### Example config +```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` +# 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 +``` -```toml +## LDAP Configuration + +```bash # To troubleshoot and get more log info enable ldap debug logging in grafana.ini # [log] # filters = ldap:debug @@ -135,7 +131,7 @@ The search filter and search bases settings are still needed to perform the LDAP ## POSIX schema (no memberOf attribute) If your ldap server does not support the memberOf attribute add these options: -```toml +```bash ## 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 diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index fc01a713ca8..a9f682ec000 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -14,7 +14,7 @@ weight = 1 Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user permissions and org memberships. -## OAuth2 Integrations +## OAuth Integrations - [Google OAuth]({{< relref "auth/google.md" >}}) - [GitHub OAuth]({{< relref "auth/github.md" >}}) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5aee2c5f594..9882d1073cf 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -324,7 +324,7 @@ Defaults to `false`. Grafana provides many ways to authenticate users. The docs for authentication has been split in to many differnet pages below. -- [Anonymous access]({{< relref "auth/overview.md" >}}) (auth.anonymous) +- [Authentication Overview]({{< relref "auth/overview.md" >}}) (anonymous access options, hide login and more) - [Google OAuth]({{< relref "auth/google.md" >}}) (auth.google) - [GitHub OAuth]({{< relref "auth/github.md" >}}) (auth.github) - [Gitlab OAuth]({{< relref "auth/gitlab.md" >}}) (auth.gitlab) From e3641197749ba07198a5fa53887f9fd887e6078d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Sep 2018 13:21:11 +0200 Subject: [PATCH 0117/2611] docs: minor fixes --- docs/sources/auth/generic-oauth.md | 2 +- docs/sources/auth/google.md | 2 +- docs/sources/auth/overview.md | 2 +- docs/sources/installation/configuration.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 70c1b937427..bec5a98e04a 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -4,7 +4,7 @@ description = "Grafana OAuthentication Guide " keywords = ["grafana", "configuration", "documentation", "oauth"] type = "docs" [menu.docs] -name = "Generic OAuth2" +name = "Generic OAuth" identifier = "generic_oauth" parent = "authentication" weight = 3 diff --git a/docs/sources/auth/google.md b/docs/sources/auth/google.md index 2a79037c430..eeb78044d3e 100644 --- a/docs/sources/auth/google.md +++ b/docs/sources/auth/google.md @@ -31,7 +31,7 @@ First, you need to create a Google OAuth Client: ## Enable Google OAuth in Grafana -Specify the Client ID and Secret in the [Grafana configuration file]({{< relref "installation/configuration.md/#config-file-locations" >}}). For example: +Specify the Client ID and Secret in the [Grafana configuration file]({{< relref "installation/configuration.md#config-file-locations" >}}). For example: ```bash [auth.google] diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index a9f682ec000..3a38ed83988 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -19,7 +19,7 @@ permissions and org memberships. - [Google OAuth]({{< relref "auth/google.md" >}}) - [GitHub OAuth]({{< relref "auth/github.md" >}}) - [Gitlab OAuth]({{< relref "auth/gitlab.md" >}}) -- [Generic OAuth]({{< relref "auth/oauth.md" >}}) (Okta2, BitBucket, Azure, OneLogin, Auth0) +- [Generic OAuth]({{< relref "auth/generic-oauth.md" >}}) (Okta2, BitBucket, Azure, OneLogin, Auth0) ## LDAP integrations diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index d62b949ead4..cbcea1df1f1 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -322,7 +322,7 @@ Defaults to `false`. ## [auth] -Grafana provides many ways to authenticate users. The docs for authentication has been split in to many differnet pages +Grafana provides many ways to authenticate users. The docs for authentication has been split in to many different pages below. - [Authentication Overview]({{< relref "auth/overview.md" >}}) (anonymous access options, hide login and more) From 4ce41c16fc23e41c314f5ee01dc1c6a5df428414 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 6 Sep 2018 13:33:38 +0200 Subject: [PATCH 0118/2611] changelog: note about closing #10424 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8470adc81a..1f5360035fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Minor +* **Alerting**: Its now possible to configure the default value for how to handle errors and no data in alerting. [#10424](https://github.com/grafana/grafana/issues/10424) * **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) * **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme) * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) From db639684bb9df8669e56da62ad4ba5bbfe612469 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 6 Sep 2018 13:52:16 +0200 Subject: [PATCH 0119/2611] docs: sql datasources min time interval --- docs/sources/administration/provisioning.md | 2 +- docs/sources/features/datasources/mssql.md | 6 ++++-- docs/sources/features/datasources/mysql.md | 4 +++- docs/sources/features/datasources/postgres.md | 4 +++- docs/sources/reference/templating.md | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index b2310378f16..c8d83ea1c54 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -154,7 +154,7 @@ Since not all datasources have the same configuration settings we only have the | tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | -| timeInterval | string | Elastic, InfluxDB, MSSQL, MySQL, PostgreSQL & Prometheus | Lowest interval/step value that should be used for this data source | +| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source | | esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) | | timeField | string | Elastic | Which field that should be used as timestamp | | interval | string | Elastic | Index date time format | diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 869f25f70cf..6bfcfd807f1 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -6,7 +6,7 @@ type = "docs" [menu.docs] name = "Microsoft SQL Server" parent = "datasources" -weight = 7 +weight = 8 +++ # Using Microsoft SQL Server in Grafana @@ -34,7 +34,9 @@ Name | Description *Password* | Database user's password ### Min time interval -A lower limit for the `$__interval` variable. Recommended to be set to write frequency, for example `1m` if your data is written every minute. + +A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables. +Recommended to be set to write frequency, for example `1m` if your data is written every minute. This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported: diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 91986866a85..e13abcf80a2 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -37,7 +37,9 @@ Name | Description *Password* | Database user's password ### Min time interval -A lower limit for the `$__interval` variable. Recommended to be set to write frequency, for example `1m` if your data is written every minute. + +A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables. +Recommended to be set to write frequency, for example `1m` if your data is written every minute. This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported: diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 630823cf781..013d6342634 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -35,7 +35,9 @@ Name | Description *TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+). ### Min time interval -A lower limit for the `$__interval` variable. Recommended to be set to write frequency, for example `1m` if your data is written every minute. + +A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables. +Recommended to be set to write frequency, for example `1m` if your data is written every minute. This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported: diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 7f86465312c..31251fd6389 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -245,7 +245,7 @@ Grafana has global built-in variables that can be used in expressions in the que ### The $__interval Variable -This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite). +This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB, MySQL, Postgres, MSSQL), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite). Grafana automatically calculates an interval that can be used to group by time in queries. When there are more data points than can be shown on a graph then queries can be made more efficient by grouping by a larger interval. It is more efficient to group by 1 day than by 10s when looking at 3 months of data and the graph will look the same and the query will be faster. The `$__interval` is calculated using the time range and the width of the graph (the number of pixels). From b2ba9c516626dbf3b87a3d0b5895b3aa84ffcc5a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 14:23:28 +0300 Subject: [PATCH 0120/2611] wrapper for react-custom-scrollbars component --- package.json | 2 + .../components/ScrollBar/withScrollBar.tsx | 53 +++++++++++++++++++ public/sass/components/_scrollbar.scss | 43 +++++++++++++++ yarn.lock | 42 ++++++++++++++- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/package.json b/package.json index 9cc47ff71b8..d7f136cb1b2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/jest": "^21.1.4", "@types/node": "^8.0.31", "@types/react": "^16.0.25", + "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.3", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", @@ -154,6 +155,7 @@ "prop-types": "^15.6.0", "rc-cascader": "^0.14.0", "react": "^16.2.0", + "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.2.0", "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx new file mode 100644 index 00000000000..9f8ad942167 --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface WithScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const withScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +export default function withScrollBar

    (WrappedComponent: React.ComponentType

    ) { + return class extends React.Component

    { + static defaultProps = withScrollBarDefaultProps; + + render() { + // Use type casting here in order to get rest of the props working. See more + // https://github.com/Microsoft/TypeScript/issues/14409 + // https://github.com/Microsoft/TypeScript/pull/13288 + const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this + .props as WithScrollBarProps; + const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; + + return ( +

    } + renderTrackVertical={props =>
    } + renderThumbHorizontal={props =>
    } + renderThumbVertical={props =>
    } + renderView={props =>
    } + {...scrollProps} + > + + + ); + } + }; +} diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 78173b73f47..adb9e0c54c0 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -294,3 +294,46 @@ padding-top: 1px; } } + +// Custom styles for 'react-custom-scrollbars' + +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} diff --git a/yarn.lock b/yarn.lock index c15c77cc45f..54f7572d5d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -473,6 +473,10 @@ add-dom-event-listener@1.x: dependencies: object-assign "4.x" +add-px-to-style@1.0.0: + version "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" @@ -3406,6 +3410,14 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-css@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202" + dependencies: + add-px-to-style "1.0.0" + prefix-style "2.0.1" + to-camel-case "1.0.0" + dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -9137,6 +9149,10 @@ prebuild-install@^2.3.0: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prefix-style@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -9388,7 +9404,7 @@ qw@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" -raf@^3.4.0: +raf@^3.1.0, raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" dependencies: @@ -9496,6 +9512,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-custom-scrollbars@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db" + dependencies: + dom-css "^2.0.0" + 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" @@ -11335,10 +11359,20 @@ to-buffer@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" +to-camel-case@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" + dependencies: + to-space-case "^1.0.0" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +to-no-case@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -11361,6 +11395,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-space-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" + dependencies: + to-no-case "^1.0.0" + toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" From 8db2960d0da06e2178c6d52e18cdad13803d6e89 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:06:54 +0300 Subject: [PATCH 0121/2611] graph legend: use 'react-custom-scrollbars' for legend scroll --- public/app/plugins/panel/graph/Legend.tsx | 12 +++++++----- public/app/plugins/panel/graph/graph.ts | 5 ++--- public/sass/components/_panel_graph.scss | 7 +------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 362ca238e80..15f8c7c982a 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,6 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; +import withScrollBar from 'app/core/components/ScrollBar/withScrollBar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -85,7 +86,7 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); - const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; + const legendClass = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; // Set min-width if side style and there is a value, otherwise remove the CSS property // Set width so it works with IE11 @@ -106,10 +107,8 @@ export class GraphLegend extends React.PureComponent -
    - {this.props.alignAsTable ? : } -
    +
    + {this.props.alignAsTable ? : }
    ); } @@ -309,3 +308,6 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { } return classes.join(' '); } + +export const Legend = withScrollBar(GraphLegend); +export default Legend; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 2a6962d78e7..8d510242dfa 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; import React from 'react'; import ReactDOM from 'react-dom'; -import { GraphLegend, GraphLegendProps } from './Legend'; +import { Legend, GraphLegendProps } from './Legend'; import { GraphCtrl } from './module'; @@ -86,7 +86,6 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); - // console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; @@ -99,7 +98,7 @@ class GraphElement { onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), }; - const legendReactElem = React.createElement(GraphLegend, legendProps); + const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); ReactDOM.render(legendReactElem, legendElem[0]); this.onLegendRenderingComplete(); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 72f3ca3dbbe..0d7d4ff05ed 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -57,9 +57,6 @@ padding-top: 6px; position: relative; - // fix for Firefox (white stripe on the right of scrollbar) - width: calc(100% - 1px); - .popover-content { padding: 0; } @@ -67,11 +64,9 @@ .graph-legend-content { position: relative; - - // fix for Firefox (white stripe on the right of scrollbar) - width: calc(100% - 1px); } +// @TODO: delete unused class .graph-legend-scroll { position: relative; overflow: auto !important; From cf832e7db483743630b8509474f600ebd058d6fb Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 14:23:28 +0300 Subject: [PATCH 0122/2611] wrapper for react-custom-scrollbars component --- package.json | 2 + .../components/ScrollBar/withScrollBar.tsx | 53 +++++++++++++++++++ public/sass/components/_scrollbar.scss | 43 +++++++++++++++ yarn.lock | 42 ++++++++++++++- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/package.json b/package.json index 87ba147fb4d..9ee81d7f8ac 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/jest": "^21.1.4", "@types/node": "^8.0.31", "@types/react": "^16.0.25", + "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.3", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", @@ -154,6 +155,7 @@ "prop-types": "^15.6.0", "rc-cascader": "^0.14.0", "react": "^16.2.0", + "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.2.0", "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx new file mode 100644 index 00000000000..9f8ad942167 --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface WithScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const withScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +export default function withScrollBar

    (WrappedComponent: React.ComponentType

    ) { + return class extends React.Component

    { + static defaultProps = withScrollBarDefaultProps; + + render() { + // Use type casting here in order to get rest of the props working. See more + // https://github.com/Microsoft/TypeScript/issues/14409 + // https://github.com/Microsoft/TypeScript/pull/13288 + const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this + .props as WithScrollBarProps; + const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; + + return ( +

    } + renderTrackVertical={props =>
    } + renderThumbHorizontal={props =>
    } + renderThumbVertical={props =>
    } + renderView={props =>
    } + {...scrollProps} + > + + + ); + } + }; +} diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 78173b73f47..adb9e0c54c0 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -294,3 +294,46 @@ padding-top: 1px; } } + +// Custom styles for 'react-custom-scrollbars' + +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} diff --git a/yarn.lock b/yarn.lock index c15c77cc45f..54f7572d5d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -473,6 +473,10 @@ add-dom-event-listener@1.x: dependencies: object-assign "4.x" +add-px-to-style@1.0.0: + version "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" @@ -3406,6 +3410,14 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-css@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202" + dependencies: + add-px-to-style "1.0.0" + prefix-style "2.0.1" + to-camel-case "1.0.0" + dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -9137,6 +9149,10 @@ prebuild-install@^2.3.0: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prefix-style@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -9388,7 +9404,7 @@ qw@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" -raf@^3.4.0: +raf@^3.1.0, raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" dependencies: @@ -9496,6 +9512,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-custom-scrollbars@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db" + dependencies: + dom-css "^2.0.0" + 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" @@ -11335,10 +11359,20 @@ to-buffer@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" +to-camel-case@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" + dependencies: + to-space-case "^1.0.0" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +to-no-case@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -11361,6 +11395,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-space-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" + dependencies: + to-no-case "^1.0.0" + toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" From 3f4099c4a6bff3369406390861f58ae44276891a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 6 Sep 2018 14:09:27 +0200 Subject: [PATCH 0123/2611] changelog: add notes about closing #13157 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5360035fe..3ba62a921fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh) +* **Postgres/MySQL/MSSQL**: Min time interval support [#13157](https://github.com/grafana/grafana/issues/13157), thx [@svenklemm](https://github.com/svenklemm) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) From bdc3acbd2c15461a604c1a5f4b7ef541de6acc20 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 6 Sep 2018 14:21:36 +0200 Subject: [PATCH 0124/2611] changelog: restructure and add 5.3.0-beta1 header [skip ci] --- CHANGELOG.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba62a921fe..08c51cf97e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 5.3.0 (unreleased) +# 5.3.0-beta1 (2018-09-06) + ### New Major Features * **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz) @@ -21,19 +23,19 @@ ### Minor * **Alerting**: Its now possible to configure the default value for how to handle errors and no data in alerting. [#10424](https://github.com/grafana/grafana/issues/10424) +* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) +* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151) +* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier) * **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) -* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme) -* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) -* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) -* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705) -* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) +* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229) +* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) +* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie) -* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) -* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890) -* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg) -* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg) +* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) +* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927) +* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen) * **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) @@ -43,33 +45,33 @@ * **Postgres/MySQL/MSSQL**: Min time interval support [#13157](https://github.com/grafana/grafana/issues/13157), thx [@svenklemm](https://github.com/svenklemm) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) -* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) -* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) -* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151) * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88) * **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow) * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z) +* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) +* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705) +* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) +* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766) * **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964) * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) -* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) -* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) +* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486) +* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) +* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) +* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890) +* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg) +* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125) -* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) -* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) -* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) -* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) -* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier) -* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927) -* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen) -* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229) -* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486) +* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) +* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme) * **Login**: Show loading animation while waiting for authentication response on login [#12865](https://github.com/grafana/grafana/issues/12865) +* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) +* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) ### Breaking changes From 28cc605e320bf7ea1e0539df220b744e3baf6dda Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:36:22 +0300 Subject: [PATCH 0125/2611] tests for withScrollBar() wrapper --- .../__snapshots__/withScrollBar.test.tsx.snap | 86 +++++++++++++++++++ .../ScrollBar/withScrollBar.test.tsx | 23 +++++ 2 files changed, 109 insertions(+) create mode 100644 public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap create mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap new file mode 100644 index 00000000000..c6b9b5bb37d --- /dev/null +++ b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withScrollBar renders correctly 1`] = ` +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/public/app/core/components/ScrollBar/withScrollBar.test.tsx b/public/app/core/components/ScrollBar/withScrollBar.test.tsx new file mode 100644 index 00000000000..89a24a7db8e --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import withScrollBar from './withScrollBar'; + +class TestComponent extends React.Component { + render() { + return
    ; + } +} + +describe('withScrollBar', () => { + it('renders correctly', () => { + const TestComponentWithScroll = withScrollBar(TestComponent); + const tree = renderer + .create( + +

    Scrollable content

    +
    + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); From a186bc01e0c3c61ef9a44cbc2beb6d332e77d823 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:36:22 +0300 Subject: [PATCH 0126/2611] tests for withScrollBar() wrapper --- .../__snapshots__/withScrollBar.test.tsx.snap | 86 +++++++++++++++++++ .../ScrollBar/withScrollBar.test.tsx | 23 +++++ 2 files changed, 109 insertions(+) create mode 100644 public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap create mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap new file mode 100644 index 00000000000..c6b9b5bb37d --- /dev/null +++ b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withScrollBar renders correctly 1`] = ` +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/public/app/core/components/ScrollBar/withScrollBar.test.tsx b/public/app/core/components/ScrollBar/withScrollBar.test.tsx new file mode 100644 index 00000000000..89a24a7db8e --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import withScrollBar from './withScrollBar'; + +class TestComponent extends React.Component { + render() { + return
    ; + } +} + +describe('withScrollBar', () => { + it('renders correctly', () => { + const TestComponentWithScroll = withScrollBar(TestComponent); + const tree = renderer + .create( + +

    Scrollable content

    +
    + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); From 479e0734518f80ff56d125913632977252b4b2f3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 6 Sep 2018 15:22:03 +0200 Subject: [PATCH 0127/2611] docs: what's new in v5.3 placeholder --- docs/sources/guides/whats-new-in-v5-3.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/sources/guides/whats-new-in-v5-3.md diff --git a/docs/sources/guides/whats-new-in-v5-3.md b/docs/sources/guides/whats-new-in-v5-3.md new file mode 100644 index 00000000000..4a2674c9b39 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5-3.md @@ -0,0 +1,18 @@ ++++ +title = "What's New in Grafana v5.3" +description = "Feature & improvement highlights for Grafana v5.3" +keywords = ["grafana", "new", "documentation", "5.3"] +type = "docs" +[menu.docs] +name = "Version 5.3" +identifier = "v5.3" +parent = "whatsnew" +weight = -9 ++++ + +# What's New in Grafana v5.3 + +## Changelog + +Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list +of new features, changes, and bug fixes. From 44cd738dd9d2d7d7072df96928743684c637d91a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 6 Sep 2018 17:03:43 +0200 Subject: [PATCH 0128/2611] changelog: typo [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c51cf97e9..1555d28a91c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,7 @@ ### New experimental features -These are new features that's still being worked on and are in an experimental phase. We incourage users to try these out and provide any feedback in related issue. +These are new features that's still being worked on and are in an experimental phase. We encourage users to try these out and provide any feedback in related issue. * **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768) From b070784b8a21885620029602603b0a9b68775712 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 6 Sep 2018 21:03:09 +0200 Subject: [PATCH 0129/2611] adds usage stats for alert notifiers (#13173) --- pkg/metrics/metrics.go | 10 ++++++++++ pkg/metrics/metrics_test.go | 22 ++++++++++++++++++++++ pkg/models/stats.go | 9 +++++++++ pkg/services/sqlstore/stats.go | 8 ++++++++ pkg/services/sqlstore/stats_test.go | 6 ++++++ 5 files changed, 55 insertions(+) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index a8d9f7308fa..dcdfbf124e1 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -440,6 +440,16 @@ func sendUsageStats() { metrics["stats.ds_access.other."+access+".count"] = count } + anStats := models.GetAlertNotifierUsageStatsQuery{} + if err := bus.Dispatch(&anStats); err != nil { + metricsLogger.Error("Failed to get alert notification stats", "error", err) + return + } + + for _, stats := range anStats.Result { + metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count + } + out, _ := json.MarshalIndent(report, "", " ") data := bytes.NewBuffer(out) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 8d88e03d106..9fbfd0c26a2 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -115,6 +115,24 @@ func TestMetrics(t *testing.T) { return nil }) + var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery + bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error { + query.Result = []*models.NotifierUsageStats{ + { + Type: "slack", + Count: 1, + }, + { + Type: "webhook", + Count: 2, + }, + } + + getAlertNotifierUsageStatsQuery = query + + return nil + }) + var wg sync.WaitGroup var responseBuffer *bytes.Buffer var req *http.Request @@ -157,6 +175,7 @@ func TestMetrics(t *testing.T) { So(getSystemStatsQuery, ShouldNotBeNil) So(getDataSourceStatsQuery, ShouldNotBeNil) So(getDataSourceAccessStatsQuery, ShouldNotBeNil) + So(getAlertNotifierUsageStatsQuery, ShouldNotBeNil) So(req, ShouldNotBeNil) So(req.Method, ShouldEqual, http.MethodPost) So(req.Header.Get("Content-Type"), ShouldEqual, "application/json") @@ -198,6 +217,9 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.ds_access."+models.DS_PROMETHEUS+".proxy.count").MustInt(), ShouldEqual, 3) So(metrics.Get("stats.ds_access.other.direct.count").MustInt(), ShouldEqual, 6+7) So(metrics.Get("stats.ds_access.other.proxy.count").MustInt(), ShouldEqual, 4+8) + + So(metrics.Get("stats.alert_notifiers.slack.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.alert_notifiers.webhook.count").MustInt(), ShouldEqual, 2) }) }) diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 4cd50d37463..d3e145dedf4 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -40,6 +40,15 @@ type GetDataSourceAccessStatsQuery struct { Result []*DataSourceAccessStats } +type NotifierUsageStats struct { + Type string + Count int64 +} + +type GetAlertNotifierUsageStatsQuery struct { + Result []*NotifierUsageStats +} + type AdminStats struct { Users int `json:"users"` Orgs int `json:"orgs"` diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 6db481bf06b..2cec86e7239 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -13,11 +13,19 @@ func init() { bus.AddHandler("sql", GetDataSourceStats) bus.AddHandler("sql", GetDataSourceAccessStats) bus.AddHandler("sql", GetAdminStats) + bus.AddHandlerCtx("sql", GetAlertNotifiersUsageStats) bus.AddHandlerCtx("sql", GetSystemUserCountStats) } var activeUserTimeLimit = time.Hour * 24 * 30 +func GetAlertNotifiersUsageStats(ctx context.Context, query *m.GetAlertNotifierUsageStatsQuery) error { + var rawSql = `SELECT COUNT(*) as count, type FROM alert_notification GROUP BY type` + query.Result = make([]*m.NotifierUsageStats, 0) + err := x.SQL(rawSql).Find(&query.Result) + return err +} + func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error { var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type` query.Result = make([]*m.DataSourceStats, 0) diff --git a/pkg/services/sqlstore/stats_test.go b/pkg/services/sqlstore/stats_test.go index dae24952d17..6949a0dbda2 100644 --- a/pkg/services/sqlstore/stats_test.go +++ b/pkg/services/sqlstore/stats_test.go @@ -36,5 +36,11 @@ func TestStatsDataAccess(t *testing.T) { err := GetDataSourceAccessStats(&query) So(err, ShouldBeNil) }) + + Convey("Get alert notifier stats should not results in error", func() { + query := m.GetAlertNotifierUsageStatsQuery{} + err := GetAlertNotifiersUsageStats(context.Background(), &query) + So(err, ShouldBeNil) + }) }) } From e67b8a3e1ad449a2d94c1578cd508438e0715222 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:14 +0300 Subject: [PATCH 0130/2611] scrollbar refactor: replace HOC by component with children --- .../ScrollBar/GrafanaScrollbar.test.tsx | 16 ++++++ .../components/ScrollBar/GrafanaScrollbar.tsx | 48 +++++++++++++++++ ...sx.snap => GrafanaScrollbar.test.tsx.snap} | 8 +-- .../ScrollBar/withScrollBar.test.tsx | 23 -------- .../components/ScrollBar/withScrollBar.tsx | 53 ------------------- 5 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx rename public/app/core/components/ScrollBar/__snapshots__/{withScrollBar.test.tsx.snap => GrafanaScrollbar.test.tsx.snap} (94%) delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx new file mode 100644 index 00000000000..7e519acd29d --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import GrafanaScrollbar from './GrafanaScrollbar'; + +describe('GrafanaScrollbar', () => { + it('renders correctly', () => { + const tree = renderer + .create( + +

    Scrollable content

    +
    + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx new file mode 100644 index 00000000000..24e5b0d8828 --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface GrafanaScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const grafanaScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +class GrafanaScrollbar extends React.Component { + static defaultProps = grafanaScrollBarDefaultProps; + + render() { + const { customClassName, children, ...scrollProps } = this.props; + + return ( +
    } + renderTrackVertical={props =>
    } + renderThumbHorizontal={props =>
    } + renderThumbVertical={props =>
    } + renderView={props =>
    } + {...scrollProps} + > + {children} + + ); + } +} + +export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap similarity index 94% rename from public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap rename to public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index c6b9b5bb37d..8e4f51e3587 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`withScrollBar renders correctly 1`] = ` +exports[`GrafanaScrollbar renders correctly 1`] = `
    -
    +

    + Scrollable content +

    ; - } -} - -describe('withScrollBar', () => { - it('renders correctly', () => { - const TestComponentWithScroll = withScrollBar(TestComponent); - const tree = renderer - .create( - -

    Scrollable content

    -
    - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx deleted file mode 100644 index 9f8ad942167..00000000000 --- a/public/app/core/components/ScrollBar/withScrollBar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface WithScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const withScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -export default function withScrollBar

    (WrappedComponent: React.ComponentType

    ) { - return class extends React.Component

    { - static defaultProps = withScrollBarDefaultProps; - - render() { - // Use type casting here in order to get rest of the props working. See more - // https://github.com/Microsoft/TypeScript/issues/14409 - // https://github.com/Microsoft/TypeScript/pull/13288 - const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this - .props as WithScrollBarProps; - const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; - - return ( -

    } - renderTrackVertical={props =>
    } - renderThumbHorizontal={props =>
    } - renderThumbVertical={props =>
    } - renderView={props =>
    } - {...scrollProps} - > - - - ); - } - }; -} From 729cc94dafd884f01806413ccbde55f1a55a02c3 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:56 +0300 Subject: [PATCH 0131/2611] graph legend: scroll component refactor --- public/app/plugins/panel/graph/Legend.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 15f8c7c982a..2a9b60f392f 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; -import withScrollBar from 'app/core/components/ScrollBar/withScrollBar'; +import GrafanaScrollbar from 'app/core/components/ScrollBar/GrafanaScrollbar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -309,5 +309,14 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { return classes.join(' '); } -export const Legend = withScrollBar(GraphLegend); +export class Legend extends React.Component { + render() { + return ( + + + + ); + } +} + export default Legend; From 8fca79e87e6855e433004967146f340b1f7b9659 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:14 +0300 Subject: [PATCH 0132/2611] scrollbar refactor: replace HOC by component with children --- .../ScrollBar/GrafanaScrollbar.test.tsx | 16 ++++++ .../components/ScrollBar/GrafanaScrollbar.tsx | 48 +++++++++++++++++ ...sx.snap => GrafanaScrollbar.test.tsx.snap} | 8 +-- .../ScrollBar/withScrollBar.test.tsx | 23 -------- .../components/ScrollBar/withScrollBar.tsx | 53 ------------------- 5 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx rename public/app/core/components/ScrollBar/__snapshots__/{withScrollBar.test.tsx.snap => GrafanaScrollbar.test.tsx.snap} (94%) delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx new file mode 100644 index 00000000000..7e519acd29d --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import GrafanaScrollbar from './GrafanaScrollbar'; + +describe('GrafanaScrollbar', () => { + it('renders correctly', () => { + const tree = renderer + .create( + +

    Scrollable content

    +
    + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx new file mode 100644 index 00000000000..24e5b0d8828 --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface GrafanaScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const grafanaScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +class GrafanaScrollbar extends React.Component { + static defaultProps = grafanaScrollBarDefaultProps; + + render() { + const { customClassName, children, ...scrollProps } = this.props; + + return ( +
    } + renderTrackVertical={props =>
    } + renderThumbHorizontal={props =>
    } + renderThumbVertical={props =>
    } + renderView={props =>
    } + {...scrollProps} + > + {children} + + ); + } +} + +export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap similarity index 94% rename from public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap rename to public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index c6b9b5bb37d..8e4f51e3587 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`withScrollBar renders correctly 1`] = ` +exports[`GrafanaScrollbar renders correctly 1`] = `
    -
    +

    + Scrollable content +

    ; - } -} - -describe('withScrollBar', () => { - it('renders correctly', () => { - const TestComponentWithScroll = withScrollBar(TestComponent); - const tree = renderer - .create( - -

    Scrollable content

    -
    - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx deleted file mode 100644 index 9f8ad942167..00000000000 --- a/public/app/core/components/ScrollBar/withScrollBar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface WithScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const withScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -export default function withScrollBar

    (WrappedComponent: React.ComponentType

    ) { - return class extends React.Component

    { - static defaultProps = withScrollBarDefaultProps; - - render() { - // Use type casting here in order to get rest of the props working. See more - // https://github.com/Microsoft/TypeScript/issues/14409 - // https://github.com/Microsoft/TypeScript/pull/13288 - const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this - .props as WithScrollBarProps; - const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; - - return ( -

    } - renderTrackVertical={props =>
    } - renderThumbHorizontal={props =>
    } - renderThumbVertical={props =>
    } - renderView={props =>
    } - {...scrollProps} - > - - - ); - } - }; -} From 6b863e3b0f424229de23149d6f31492a89976160 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 7 Sep 2018 10:21:10 +0200 Subject: [PATCH 0133/2611] 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 a1d1c4fb9a310ebb90d0b7bc9d49b935d5e12c90 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 7 Sep 2018 11:06:19 +0200 Subject: [PATCH 0134/2611] fix code formatting --- pkg/services/alerting/notifiers/teams.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 06fc8c1c5c2..7beb71e5c65 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -104,7 +104,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { "@type": "OpenUri", "name": "View Rule", "targets": []map[string]interface{}{ - { + { "os": "default", "uri": ruleUrl, }, }, @@ -114,7 +114,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { "@type": "OpenUri", "name": "View Graph", "targets": []map[string]interface{}{ - { + { "os": "default", "uri": evalContext.ImagePublicUrl, }, }, From 8f054e7c0813fb7907b5d93432603ad5ab72fc62 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 7 Sep 2018 11:30:18 +0200 Subject: [PATCH 0135/2611] changelog: add notes about closing #13121 [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1555d28a91c..b89e925e826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 5.3.0 (unreleased) +### 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) + # 5.3.0-beta1 (2018-09-06) ### New Major Features From 349b2787cbb0ff664d784cb41ae2849a82141e5c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 14:31:56 +0300 Subject: [PATCH 0136/2611] scrollbar: use enzyme for tests instead of react-test-renderer --- .../ScrollBar/GrafanaScrollbar.test.tsx | 17 +- .../GrafanaScrollbar.test.tsx.snap | 176 ++++++++++-------- 2 files changed, 111 insertions(+), 82 deletions(-) diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx index 7e519acd29d..d4d3de6aea7 100644 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -1,16 +1,15 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; import GrafanaScrollbar from './GrafanaScrollbar'; describe('GrafanaScrollbar', () => { it('renders correctly', () => { - const tree = renderer - .create( - -

    Scrollable content

    -
    - ) - .toJSON(); - expect(tree).toMatchSnapshot(); + const tree = mount( + +

    Scrollable content

    +
    + ); + expect(toJson(tree)).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index 8e4f51e3587..7d0af38a6dc 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,86 +1,116 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`GrafanaScrollbar renders correctly 1`] = ` -
    -
    -

    - Scrollable content -

    -
    -
    -
    -
    -
    -
    -
    + > +
    +

    + Scrollable content +

    +
    +
    +
    +
    +
    +
    +
    +
    + + `; From 35ef51dca91639295c171cc2f4337e2f537d00fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 Sep 2018 14:24:33 +0200 Subject: [PATCH 0137/2611] refactoring: custom scrollbars PR updated, #13175 --- .../CustomScrollbar.test.tsx} | 8 +++--- .../CustomScrollbar.tsx} | 25 +++++++++---------- .../CustomScrollbar.test.tsx.snap} | 2 +- yarn.lock | 6 +++++ 4 files changed, 23 insertions(+), 18 deletions(-) rename public/app/core/components/{ScrollBar/GrafanaScrollbar.test.tsx => CustomScrollbar/CustomScrollbar.test.tsx} (63%) rename public/app/core/components/{ScrollBar/GrafanaScrollbar.tsx => CustomScrollbar/CustomScrollbar.tsx} (70%) rename public/app/core/components/{ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap => CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap} (96%) diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx similarity index 63% rename from public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx rename to public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx index 7e519acd29d..4edcf7313db 100644 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ b/public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx @@ -1,14 +1,14 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import GrafanaScrollbar from './GrafanaScrollbar'; +import CustomScrollbar from './CustomScrollbar'; -describe('GrafanaScrollbar', () => { +describe('CustomScrollbar', () => { it('renders correctly', () => { const tree = renderer .create( - +

    Scrollable content

    -
    + ) .toJSON(); expect(tree).toMatchSnapshot(); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx similarity index 70% rename from public/app/core/components/ScrollBar/GrafanaScrollbar.tsx rename to public/app/core/components/CustomScrollbar/CustomScrollbar.tsx index 24e5b0d8828..8be65249808 100644 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx +++ b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import Scrollbars from 'react-custom-scrollbars'; -interface GrafanaScrollBarProps { +interface Props { customClassName?: string; autoHide?: boolean; autoHideTimeout?: number; @@ -9,19 +9,18 @@ interface GrafanaScrollBarProps { hideTracksWhenNotNeeded?: boolean; } -const grafanaScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - /** * Wraps component into component from `react-custom-scrollbars` */ -class GrafanaScrollbar extends React.Component { - static defaultProps = grafanaScrollBarDefaultProps; +class CustomScrollbar extends PureComponent { + + static defaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, + }; render() { const { customClassName, children, ...scrollProps } = this.props; @@ -45,4 +44,4 @@ class GrafanaScrollbar extends React.Component { } } -export default GrafanaScrollbar; +export default CustomScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap similarity index 96% rename from public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap rename to public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 8e4f51e3587..37d8cea45be 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ b/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GrafanaScrollbar renders correctly 1`] = ` +exports[`CustomScrollbar renders correctly 1`] = `
    Date: Fri, 7 Sep 2018 14:32:09 +0200 Subject: [PATCH 0138/2611] 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 e4a488baf1279d9d41afd6a4bbe84e9cb6c9a1b5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 16:12:28 +0300 Subject: [PATCH 0139/2611] graph legend: use refactored version of scrollbar, #13175 --- .../ScrollBar/GrafanaScrollbar.test.tsx | 15 --- .../components/ScrollBar/GrafanaScrollbar.tsx | 48 -------- .../GrafanaScrollbar.test.tsx.snap | 116 ------------------ public/app/plugins/panel/graph/Legend.tsx | 8 +- 4 files changed, 4 insertions(+), 183 deletions(-) delete mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx delete mode 100644 public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx deleted file mode 100644 index d4d3de6aea7..00000000000 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import GrafanaScrollbar from './GrafanaScrollbar'; - -describe('GrafanaScrollbar', () => { - it('renders correctly', () => { - const tree = mount( - -

    Scrollable content

    -
    - ); - expect(toJson(tree)).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx deleted file mode 100644 index 24e5b0d8828..00000000000 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface GrafanaScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const grafanaScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -class GrafanaScrollbar extends React.Component { - static defaultProps = grafanaScrollBarDefaultProps; - - render() { - const { customClassName, children, ...scrollProps } = this.props; - - return ( -
    } - renderTrackVertical={props =>
    } - renderThumbHorizontal={props =>
    } - renderThumbVertical={props =>
    } - renderView={props =>
    } - {...scrollProps} - > - {children} - - ); - } -} - -export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap deleted file mode 100644 index 7d0af38a6dc..00000000000 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GrafanaScrollbar renders correctly 1`] = ` - - -
    -
    -

    - Scrollable content -

    -
    -
    -
    -
    -
    -
    -
    -
    - - -`; diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 2a9b60f392f..e493d7d5020 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; -import GrafanaScrollbar from 'app/core/components/ScrollBar/GrafanaScrollbar'; +import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -193,7 +193,7 @@ function LegendValue(props: LegendValueProps) { return
    {value}
    ; } -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false): React.ReactElement[] { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { @@ -312,9 +312,9 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { export class Legend extends React.Component { render() { return ( - + - + ); } } From c179926a2771490275b08d17fd2bbb55d1769954 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 7 Sep 2018 16:56:31 +0200 Subject: [PATCH 0140/2611] 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 0141/2611] 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 0142/2611] 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 0143/2611] 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 0144/2611] 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 0145/2611] 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 0157/2611] 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 0158/2611] 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 0159/2611] 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 df822a660b129a459d92a9e6f9d30b2224732331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 16:19:28 +0200 Subject: [PATCH 0160/2611] initial render/refresh timing issues --- public/app/features/panel/metrics_panel_ctrl.ts | 1 + public/app/features/panel/panel_ctrl.ts | 6 ++---- public/app/features/plugins/plugin_component.ts | 3 +-- public/app/plugins/panel/graph/graph.ts | 1 + 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index e8f3647f09c..ce65d11b3a1 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -60,6 +60,7 @@ class MetricsPanelCtrl extends PanelCtrl { } private onMetricsPanelRefresh() { + console.log('metrics_panel_ctrl:onRefresh'); // ignore fetching data if another panel is in fullscreen if (this.otherPanelInFullscreenMode()) { return; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 96fcbe8067b..0de55e33ccc 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -57,12 +57,9 @@ export class PanelCtrl { }); } - init() { - this.dashboard.panelInitialized(this.panel); - } - panelDidMount() { this.events.emit('component-did-mount'); + this.dashboard.panelInitialized(this.panel); } renderingCompleted() { @@ -248,6 +245,7 @@ export class PanelCtrl { } render(payload?) { + console.log('panel_ctrl:render'); this.timing.renderStart = new Date().getTime(); this.events.emit('render', payload); } diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index f6df579dc09..47f2fc535e8 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -209,9 +209,8 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ setTimeout(() => { elem.append(child); scope.$applyAsync(() => { + console.log('post appendAndCompile, broadcast refresh', scope.panel); scope.$broadcast('component-did-mount'); - scope.$broadcast('refresh'); - console.log('appendAndCompile', scope.panel); }); }); } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 33db0e7220a..3a27c20e37c 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -80,6 +80,7 @@ class GraphElement { this.annotations = this.ctrl.annotations || []; this.buildFlotPairs(this.data); const graphHeight = this.elem.height(); + console.log('graphHeight', graphHeight); updateLegendValues(this.data, this.panel, graphHeight); this.ctrl.events.emit('render-legend'); From 0cfcf2685e66af76895664c86f562952d63ca812 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 16:58:17 +0200 Subject: [PATCH 0161/2611] 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 c82bf7f67fab093ea730044e45b2a8ac4b10eb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 17:55:06 +0200 Subject: [PATCH 0162/2611] fix: changing edit / view fullscreen modes now work --- public/app/core/directives/dash_class.ts | 1 + .../dashboard/dashgrid/VizTypePicker.tsx | 19 +++++++++++++------ .../app/features/dashboard/view_state_srv.ts | 16 +++------------- public/sass/components/_scrollbar.scss | 5 +++++ public/sass/components/_viz_editor.scss | 15 +++++++++------ 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 645f32de645..37124eb7d4b 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -6,6 +6,7 @@ function dashClass($timeout) { return { link: ($scope, elem) => { $scope.ctrl.dashboard.events.on('view-mode-changed', panel => { + console.log('view-mode-changed', panel.fullscreen); if (panel.fullscreen) { elem.addClass('panel-in-fullscreen'); } else { diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index 8f5690f31d5..9402133df34 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import config from 'app/core/config'; import { PanelPlugin } from 'app/types/plugins'; +import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; import _ from 'lodash'; interface Props { @@ -49,13 +50,19 @@ export class VizTypePicker extends PureComponent { render() { return (
    -
    - +
    +
    + +
    +
    +
    + +
    {this.state.pluginList.map(this.renderVizPlugin)}
    +
    -
    {this.state.pluginList.map(this.renderVizPlugin)}
    ); } diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index a13bc88161e..8805050831e 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -11,7 +11,6 @@ export class DashboardViewState { panelScopes: any; $scope: any; dashboard: DashboardModel; - editStateChanged: any; fullscreenPanel: any; oldTimeRange: any; @@ -72,9 +71,6 @@ export class DashboardViewState { } } - // remember if editStateChanged - this.editStateChanged = (state.edit || false) !== (this.state.edit || false); - _.extend(this.state, state); this.dashboard.meta.fullscreen = this.state.fullscreen; @@ -128,17 +124,11 @@ export class DashboardViewState { return; } - if (this.fullscreenPanel) { - // if already fullscreen - if (this.fullscreenPanel === panel && this.editStateChanged === false) { - return; - } else { - this.leaveFullscreen(); - } - } - if (!panel.fullscreen) { this.enterFullscreen(panel); + } else { + // already in fullscreen view just update the view mode + this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); } } else if (this.fullscreenPanel) { this.leaveFullscreen(); diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index adb9e0c54c0..00bd5f7c94c 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -307,6 +307,7 @@ .view { display: flex; flex-grow: 1; + flex-direction: column; } .track-vertical { @@ -337,3 +338,7 @@ border-radius: 6px; } } + +.scroll-margin-helper { + margin-right: 12px; +} diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss index 3658c869d6d..048e513cfbb 100644 --- a/public/sass/components/_viz_editor.scss +++ b/public/sass/components/_viz_editor.scss @@ -19,12 +19,13 @@ height: 100%; } -.viz-picker-list { - padding-top: $spacer; - display: flex; - flex-direction: column; - overflow: auto; +.viz-picker__search { + flex-grow: 0; +} + +.viz-picker__items { flex-grow: 1; + height: calc(100% - 50px); } .viz-picker__item { @@ -41,13 +42,15 @@ display: flex; flex-shrink: 0; border: 1px solid transparent; + @include left-brand-border; &:hover { background: $card-background-hover; } &--selected { - border: 1px solid $orange; + // border: 1px solid $orange; + @include left-brand-border-gradient(); .viz-picker__item-name { color: $text-color; From 3ceab9484dac87d76cfdc4fd7846458b40a0361f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 19:04:56 +0200 Subject: [PATCH 0163/2611] wip: moving option tabs into viz tab --- public/app/features/panel/panel_ctrl.ts | 22 +++++++++++++++---- public/app/features/panel/panel_editor_tab.ts | 11 +++++----- public/app/features/panel/viz_tab.ts | 6 +++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 0de55e33ccc..82a4a116f89 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -22,6 +22,7 @@ export class PanelCtrl { pluginName: string; pluginId: string; editorTabs: any; + optionTabs: any; $scope: any; $injector: any; $location: any; @@ -96,9 +97,10 @@ export class PanelCtrl { initEditMode() { this.editorTabs = []; - this.addEditorTab('Queries', metricsTabDirective, 0, 'fa fa-database'); - this.addEditorTab('Visualization', vizTabDirective, 1, 'fa fa-line-chart'); - this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); + this.optionTabs = []; + this.addCommonTab('Queries', metricsTabDirective, 0, 'fa fa-database'); + this.addCommonTab('Visualization', vizTabDirective, 1, 'fa fa-line-chart'); + this.addCommonTab('General', 'public/app/partials/panelgeneral.html'); this.editModeInitiated = true; this.events.emit('init-edit-mode', null); @@ -120,7 +122,7 @@ export class PanelCtrl { route.updateParams(); } - addEditorTab(title, directiveFn, index?, icon?) { + addCommonTab(title, directiveFn, index?, icon?) { const editorTab = { title, directiveFn, icon }; if (_.isString(directiveFn)) { @@ -136,6 +138,18 @@ export class PanelCtrl { } } + addEditorTab(title, directiveFn, index?, icon?) { + const editorTab = { title, directiveFn, icon }; + + if (_.isString(directiveFn)) { + editorTab.directiveFn = () => { + return { templateUrl: directiveFn }; + }; + } + + this.optionTabs.push(editorTab); + } + getMenu() { const menu = []; menu.push({ diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts index fe83a892cc7..d16bab3dc55 100644 --- a/public/app/features/panel/panel_editor_tab.ts +++ b/public/app/features/panel/panel_editor_tab.ts @@ -13,11 +13,12 @@ function panelEditorTab(dynamicDirectiveSrv) { }, directive: scope => { const pluginId = scope.ctrl.pluginId; - const tabIndex = scope.index; + const tabName = scope.editorTab.title.toLowerCase(); + console.log('panelEditorTab', pluginId, tabName); if (directiveCache[pluginId]) { - if (directiveCache[pluginId][tabIndex]) { - return directiveCache[pluginId][tabIndex]; + if (directiveCache[pluginId][tabName]) { + return directiveCache[pluginId][tabName]; } } else { directiveCache[pluginId] = []; @@ -25,10 +26,10 @@ function panelEditorTab(dynamicDirectiveSrv) { const result = { fn: () => scope.editorTab.directiveFn(), - name: `panel-editor-tab-${pluginId}${tabIndex}`, + name: `panel-editor-tab-${pluginId}${tabName}`, }; - directiveCache[pluginId][tabIndex] = result; + directiveCache[pluginId][tabName] = result; return result; }, diff --git a/public/app/features/panel/viz_tab.ts b/public/app/features/panel/viz_tab.ts index 972cf9d43cc..d52511b55da 100644 --- a/public/app/features/panel/viz_tab.ts +++ b/public/app/features/panel/viz_tab.ts @@ -27,7 +27,10 @@ const template = `
    -
    Options
    +
    +
    {{tab.title}}
    + +
    `; @@ -37,7 +40,6 @@ export function vizTabDirective() { 'use strict'; return { restrict: 'E', - scope: true, template: template, controller: VizTabCtrl, }; 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 0164/2611] 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 0165/2611] 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 0166/2611] 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 0167/2611] 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 0168/2611] 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 0169/2611] 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 0170/2611] 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 0171/2611] 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 0172/2611] 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 0173/2611] 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 0174/2611] 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 0175/2611] 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 0176/2611] 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 0177/2611] 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 0178/2611] 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 0179/2611] 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 0180/2611] 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 0181/2611] 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 0182/2611] 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 0183/2611] 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 0184/2611] 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 0185/2611] 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 0186/2611] 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 0187/2611] 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 0188/2611] 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 0189/2611] 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 0190/2611] 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 0191/2611] 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 0192/2611] 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 0193/2611] 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 0194/2611] 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 0195/2611] 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 0196/2611] 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 0197/2611] 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 0198/2611] 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 0199/2611] 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 0200/2611] 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 0201/2611] 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 0202/2611] 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 0203/2611] 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 0204/2611] 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 0205/2611] 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 0206/2611] 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 0207/2611] 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 0208/2611] 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 0209/2611] 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 0210/2611] 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 0211/2611] 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 0212/2611] 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 0213/2611] 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 0214/2611] 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 0215/2611] 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 0216/2611] 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 0217/2611] 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 0218/2611] 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 0219/2611] 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 0220/2611] 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 0221/2611] 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 0222/2611] 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 0223/2611] 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 0224/2611] 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 = ` - +